本节开始基于Selenium框架编写一些简单的爬虫程序示例。在编写具体的功能示例之前,我们需要做一些准备工作。
(1)在IDE中创建一个Maven工程,并将它命名为java-webcrawler。
(2)在pom.xml中添加相关的Selenium Java Client Library依赖项。
<dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-api</artifactId> <version>${selenium.version}</version> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-chrome-driver</artifactId> <version>${selenium.version}</version> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-remote-driver</artifactId> <version>${selenium.version}</version> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-support</artifactId> <version>${selenium.version}</version> </dependency>
现在我们尝试通过爬虫程序访问百度,并打印页面内容。示例代码如下:
System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, "/path/to/chromedriver"); ChromeDriver webDriver = new ChromeDriver(); webDriver.get("https://www.baidu.com"); Thread.sleep(2000); System.out.println(webDriver.getPageSource()); webDriver.quit();
上述代码的逻辑很简单,主要分为以下3个步骤:
步骤01 将chromedriver可执行文件的路径绑定到系统变量webdriver.chrome.driver,以便在创建ChromeDriver对象时使用,当然,也可以使用WebDriverManager自动安装和管理WebDriver。
步骤02 创建ChromeDriver对象并访问百度网页地址。
步骤03 获取百度网页的HTML内容并打印。
接下来,我们尝试利用表达式来获取指定的页面元素。在本小节的示例中,使用的是XPath表达式。XPath全称是XML Path,用于查找XML文档中的元素或节点。在网络爬虫中,它通常用于查找Web元素。关于XPath表达式的具体语法和使用技巧,我们将在后续章节中单独讲解。现在,我们先来看一下如何使用Chrome浏览器获取目标元素的XPath表达式。
(1)打开Chrome浏览器的开发者工具。
(2)在开发者工具的Tab栏中切换至Elements选项卡,在该页面中找到自己感兴趣的目标元素,如图1-6所示。
图1-6 在开发者工具中选择目标元素
(3)右击该元素,并且在弹出的快捷菜单中依次选择Copy→Copy XPath命令,如图1-7所示。这样,我们就可以得到相关元素的XPath表达式了。
图1-7 复制XPath表达式
得到Xpath表达式之后,就可以开始编写相关代码了。
ChromeDriver webDriver = new ChromeDriver(); webDriver.get("https://www.bing.com"); Thread.sleep(2000); WebElement webElement = webDriver.findElement(By.xpath("//*[@id=\"sb_form_q\"]")); webElement.sendKeys("Java网络爬虫精解与实践"); webDriver.quit();
上面的例子通过Selenium WebDriver去访问www.bing.com网站,使用XPath表达式定位了bing.com页面上的搜索输入框元素,并在搜索框中输入了搜索词“Java网络爬虫精解与实践”。
在该示例中,我们在必应搜索引擎中输入搜索词“Java网络爬虫精解与实践”,然后单击搜索按钮来完成页面搜索操作。具体代码示例如下:
ChromeDriver webDriver = new ChromeDriver(); webDriver.get("https://www.bing.com"); Thread.sleep(2000); WebElement input = webDriver.findElement(By.xpath("//*[@id=\"sb_form_q\"]")); input.sendKeys("Java网络爬虫精解与实践"); WebElement searchBtn = webDriver.findElement(By.id("sb_form_go")); searchBtn.submit(); webDriver.quit();
在这个例子中,我们利用Selenium WebDriver获取了必应搜索引擎的搜索输入框和搜索按钮,并实现了输入框内容的填充和搜索按钮的模拟单击功能。
Selenium WebDriver主要通过元素定位器来操作网页上的各个元素,只有定位到元素的位置,才能进一步对其进行操作。
我们可以通过findElement(By.locator())方法来查找页面上的元素。如果页面上存在定位器指定的元素,该方法会返回一个WebElement对象。
Selenium WebDriver共支持8种元素定位器。除了前面使用的Xpath定位器和ID定位器外,还可以使用Name、TagName、CSS等多种元素定位器。其他类型的元素定位器的使用方式将在后面的章节中详细介绍。
iframe元素是可以将一个页面的内容嵌入到另一个页面的容器中。在数据采集过程中,如果需要定位的元素在iframe元素中,那么在使用Selenium WebDriver定位和采集元素数据之前,我们需要先将WebDriver切换到对应的iframe容器中,才能正确采集数据。首先,我们来看一个使用iframe容器的网页实例(https://chercher.tech/practice/frames)。该网页的iframe容器嵌套关系如图1-8所示。
图1-8 iframe容器嵌套关系
在本次实验中,我们将使用爬虫技术来选中frame3容器中的复选框(checkbox)。具体实现代码如下:
String url = "https://chercher.tech/practice/frames"; WebDriver = new ChromeDriver(); webDriver.get(url); Thread.sleep(2000); WebElement iframe = webDriver.findElement(By.id("frame1")); webDriver = webDriver.switchTo().frame(iframe); iframe = webDriver.findElement(By.id("frame3")); webDriver = webDriver.switchTo().frame(iframe); WebElement checkBox = webDriver.findElement(By.id("a")); checkBox.click(); webDriver.quit();
执行结果如图1-9所示。
图1-9 frame3容器复选框被选中的结果
目前,大部分网页内容都是通过Ajax或JavaScript异步加载的。这样,当用户在浏览器中打开一个网页时,用户想要交互的网页元素可能会在不同的时间间隔内加载出来。在之前的例子中,我们使用了Thread.sleep()这种固定时间的等待方式。然而,这种方式有个弊端:网页加载的速度受到网络质量、服务器状态等多种因素的影响,网页内容的加载速度很难准确评估。如果等待时间较短,用户想要的网页元素可能还没有加载完成;如果等待时间过长,则会降低数据采集的效率。在Selenium WebDriver中,等待方式可以分为显式等待和隐式等待两种,具体情况如图1-10所示。
图1-10 Selenium等待方式
隐式等待(ImplicitWait)通常用于全局的等待设置。设置成功后,在接下来的Selenium命令执行过程中,如果无法立即获取到目标元素,Selenium将会等待一段时间后再抛出NoSuchElementException异常。下面来看一个隐式等待的例子:
ChromeDriver webDriver = new ChromeDriver(); webDriver.manage().timeouts().implicitlyWait(Duration.ofSeconds(2)); webDriver.get("https://www.bing.com"); WebElement searchBtn = webDriver.findElement(By.id("search-icon")); //注:ID为search-icon的页面元素并不存在,在等待2秒钟后程序将会抛出NoSuchElementException
相对于隐式等待,显式等待可以设置更加合理的等待时间。Selenium框架提供了两种显式等待方式,分别是WebDriverWait和FluentWait。接下来,我们来看一个WebDriverWait的应用实例。
String url = "http://www.bing.com"; ChromeDriver webDriver = new ChromeDriver(); WebDriverWait wait = new WebDriverWait(webDriver, Duration.ofSeconds(10)); webDriver.get(url); WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[@id=\"sb-form_q\" ]"))); element.sendKeys("Java网络爬虫精解与实践"); element.click(); webDriver.quit();
在上面的例子中,我们使用WebDriverWait代替了之前的Thread.sleep方式来等待元素加载完成。在使用WebDriverWait时,我们创建了一个WebDriverWait对象,并设置了期望等待的最大时间,WebDriverWait对象的until方法会不断轮询期望的条件是否完成,轮询时间间隔是500毫秒。假设某个网站中的目标元素期望的最长加载时间是4秒,但由于某段时间内该网站的响应速度较快,在等待1秒后目标元素就已加载完成。在这种情况下,使用WebDriverWait显式等待方式可以节约出3秒的时间。
WebDriverWait继承自FluentWait,且两者都实现了Wait接口。因此,WebDriverWait和FluentWait在功能上基本相同。例如,它们都支持自定义轮询时间间隔,并允许重写apply方法。相较于FluentWait, WebDriverWait提供了更简便的显式等待操作方法。
在采集数据过程中,有时需要对网页内容进行截图保存,以便后续进行数据校验。本节将提供一个基于Selenium屏幕截图的简单示例程序。
WebDriver driver = new ChromeDriver(); driver.get("https://top.baidu.com"); File srcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); FileUtils.copyFile(srcFile, new File("$PIC_PATH/top-baidu-page.png")); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[@class=\"the me-hot category-item_1fzJW\"]"))); WebElement topSearchElement = driver.findElement(By.xpath("//*[@class=\"theme-hot category-item_1fzJW\"]")); srcFile = topSearchElement.getScreenshotAs(OutputType.FILE); FileUtils.copyFile(srcFile, new File("$PIC_PATH/top-baidu-element.png"));
在上面的例子中,我们提供了两种网页内容截图方式:对整个网页内容进行截图和针对特定元素进行截图。然而,网页内容的截图仅覆盖当前视窗内容的截图,而不是整个网页。如果我们期望获取整个网页的截图,可以通过对网页内容进行滚动操作来逐步截取当前屏幕的内容,最后将所有截图拼接在一起。此外,还有一个更简单的实现方式,即使用Ashot这个第三方开源库来完成这项任务。Ashot是由Yandex公司开发的Java组件,它的主要功能之一就是获取整个网页内容的屏幕快照。
Selenium的RemoteWebDriver类实现了JavaScriptExecutor接口,该接口的主要功能是在浏览器中执行JavaScript代码,从而使得WebDriver可以实现更高级的浏览器页面交互操作。
虽然Selenium WebDriver本身已经提供了一些接口来操作Web元素,例如发送数据、单击按钮等。不过,如前所述,有些更加高级的操作需要JavaScriptExecutor的帮助,例如滚动页面操作、获取浏览器窗口innerHeight值等。
JavaScriptExecutor接口主要提供了两个用于执行JavaScript脚本的方法。
● Object executeScript(String script, Object…args):在当前页面执行JavaScript脚本,并支持返回结果。
● Object executeAsyncScript(String script, Object…args):执行JavaScript异步脚本,并支持返回结果。
接下来,我们来看一些executeScript方法的使用示例。
ChromeDriver webDriver = new ChromeDriver(); webDriver.get("https://top.baidu.com"); // 在控制台输出信息 webDriver.executeScript("console.log('Hello, This is message from WebDriver!')"); // 获取浏览器窗口的视口高度和宽度 int innerHeight = (int)webDriver.executeScript("return window.innerHeight;"); int innerWidth = (int)webDriver.executeScript("return window.innerWidth;"); // 垂直向下滚动页面500像素 webDriver.executeScript("window.scrollBy(0,500)");