在上面的章节中,我们介绍了几个目前比较活跃的Java爬虫框架。在今天的章节中,我们会参考开源爬虫框架,开发我们自己的Java爬虫软件。
首先,我们下载本章节要使用到的源代码,本章节主要提供了基于HTTPClient和WebDriver两种方式的数据抓取器。在运行该库之前,我们还需要准备一下我们的开发环境。
首先,我要给大家介绍一下Selenium webdriver这个开源组件,Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera,Edge等。Selenium webdriver是编程语言和浏览器之间的通信工具,它的工作流程如下图所示。
我们这里选择的是chrome浏览器,在正式开始编写代码之前,我们需要安装两个重要的程序,一个是chromedriver,一个是chrome。
chrome浏览器的下载地址:https://chrome.en.softonic.com/
chromedriver的下载地址:http://chromedriver.storage.googleapis.com/index.html
注意:在安装这两个软件的时候,它们的版本需要对应起来才能正常工作。
下面的UML类图是我们本章节所使用程序的主要类结构。
例1:创建HTTP采集器和WebDriver采集器,访问URL,打印网页内容
java
Page page = new Page("http://www.oracle.com/");
//创建基于httpclient的网页内容采集器
IHttpFetcher httpFetcher = new DefaultHttpFetcher();
httpFetcher.fetch(page);
String htmlContentStr = new String(page.getContentData());
System.out.println(htmlContentStr);
//创建基于webdriver的网页内容采集器
httpFetcher = new WebDriverHttpFetcher();
httpFetcher.fetch(page);
htmlContentStr = new String(page.getContentData());
System.out.println(htmlContentStr);
上面的例子中分别创建了两个HttpFetcher,一个是基于httpclient的网页内容采集器DefaultHttpFetcher,另一个是基于webdriver的网页内容采集器WebDriverHttpFetcher。WebDriverHttpFetcher相对于DefaultHttpFetcher的主要优势在于WebDriverHttpFetcher可以捕获异步加载内容的网页详情。
例2:利用XPath表达式获取网页指定元素
XPath(XML Path) 是一个表达式,用于查找XML文档中的元素或节点。在网络爬虫中,它通常用于查找Web元素。对于XPath表达式的具体语法,可以自行查阅。
java
Page page = new Page("http://www.bing.com");
IHttpFetcher httpFetcher = new WebDriverHttpFetcher();
httpFetcher.fetch(page);
String htmlContentStr = new String(page.getContentData());
Document document = Jsoup.parse(htmlContentStr);
Element element = XSoupUtil.getCombineElement("//*[@id=\"sb_form_q\"]", document, "http://www.bing.com");
上面的例子创建了WebDriverHttpFetcher来访问bing.com,使用XPath表达式定位了bing.com页面上面的搜索输入框元素。
例3:模拟输入框内容填写和按钮点击
java
Page page = new Page("http://www.bing.com");
WebDriverHttpFetcher httpFetcher = new WebDriverHttpFetcher();
WebDriver webDriver = httpFetcher.getWebDriver();
webDriver.get(page.getUrl());
Thread.sleep(2000);
WebElement element = webDriver.findElement(By.xpath("//*[@id=\"sb_form_q\"]"));
element.sendKeys("网络爬虫");
element = webDriver.findElement(By.xpath("//*[@id=\"search_icon\"]"));
element.click();
Thread.sleep(2000);
System.out.println(webDriver.getPageSource());
在这个例子里面我们利用Selenium WebDriver获取到必应搜索引擎的搜索输入框和搜索按钮,并且实现了输入框内容的填充和搜索按钮的模拟点击功能。
Selenium WebDriver主要是通过元素定位器来操作网页上面的各个元素,只有我们定位到了元素的位置,我们才可能进一步对其进行操作。
Selenium WebDriver主要使用"findElement(By.locator())"方法来查找页面上面的元素。如果页面上面存在定位器指定的元素,该方法会返回一个WebElement对象。
Selenium WebDriver共支持8种元素定位器,除了我们上面使用的Xpath定位器外,还可以使用ID,Name,TagName,CSS等多种元素选择定位器。
例4:采集IFrame元素中的数据
IFrame是可以将一个页面的内容嵌入到另一个页面中的容器,在使用Selenium WebDriver定位和采集元素数据之前,我们需要先将WebDriver切换到对应的IFrame容器下才可以。首先我们看一个使用IFrame容器的网页实例。该网页的IFrame容器嵌套关系如下图所示:
在本例中,我们将使用爬虫技术来选中frame3容器中的checkbox复选框。
java
Page page = new Page("https://chercher.tech/practice/frames");
WebDriverHttpFetcher httpFetcher = new WebDriverHttpFetcher();
WebDriver webDriver = httpFetcher.getWebDriver();
webDriver.get(page.getUrl());
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();
例5:使用更加优雅的等待方式
目前,大部分的网页内容都是通过Ajax或者JavaScript异步加载的。这样,当用户在浏览器中打开一个网页的时候,用户想要交互的网页元素可能会在不同的时间间隔内加载出来。在之前的例子中,我们简单使用了Thread.sleep()这种方式来等待固定的时间。实际上,这种方式有个弊端:网页加载的速度受到网络质量,服务器状态等多因素的影响,网页内容的加载速度很难准确评估。如果等待时间短了,用户想要的网页元素可能还没有加载完成。如果等待时间设置很长,则会降低数据采集的速度。在Selenium WebDriver中,等待方式可以分为显示等待和隐式等待两类。具体情况看下图:
隐式等待(ImplicitWait)通常用于全局的等待设置,设置成功以后接下来的Selenium命令执行时候如果无法立即获取到目标元素,那么它会等待一段时间后再抛出NoSuchElementException异常。让我们看一个隐式等待的例子:
java
Page page = new Page("http://www.bing.com");
WebDriverHttpFetcher httpFetcher = new WebDriverHttpFetcher();
ChromeDriver webDriver = (ChromeDriver)httpFetcher.getWebDriver();
webDriver.manage().timeouts().implicitlyWait(Duration.ofSeconds(2));
webDriver.get(page.getUrl());
WebElement element = webDriver.findElement(By.xpath("//*[@id=\"sb_form_q\"]"));
element.sendKeys("网络爬虫");
element = webDriver.findElement(By.xpath("//*[@id=\"search_icon\"]"));
element.click();
webDriver.quit();
相对于隐式等待,显示等待可以设置更加合适的等待时间,Selenium框架提供了两种显示等待的方式,分别是WebDriverWait和FluentWait,首先我们来看一个WebDriverWait的应用实例。
java
Page page = new Page("http://www.bing.com");
WebDriverHttpFetcher httpFetcher = new WebDriverHttpFetcher();
ChromeDriver webDriver = (ChromeDriver)httpFetcher.getWebDriver();
WebDriverWait wait = new WebDriverWait(webDriver, Duration.ofSeconds(10));
webDriver.get(page.getUrl());
WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[@id=\"sb_form_q\"]")));
element.sendKeys("网络爬虫");
element = wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//*[@id=\"search_icon\"]")));
element.click();
webDriver.quit();
在使用WebDriverWait的时候我们需要手动创建一个WebDriverWait对象,并且设置我们需要等待的时间,WebDriverWait对象的until会帮助我们不断轮询我们期望的条件是否完成,轮询时间间隔是500ms。WebDriverWait对象实际上继承于FluentWait对象。如果我们直接使用Fluent对象,则可以有更加灵活的等待策略和轮询时间间隔。可以假设这样一个场景,我们希望获取到页面中的某个元素,但是这个元素并不是必须的,即使最终这个元素没有加载成功也不影响我们接下来的处理流程。这个时候,FluentWait会是一个不错的选择。
例6:配置WebDriver参数
一般来讲,当我们在使用Selenium WebDriver采集数据的时候,我们需要对WebDriver进行一些设置,例如:隐身模式,关闭弹窗,忽略SSL证书错误等等。在这个例子中,我们来看一看常用的ChromeDriver配置项,更多的配置信息可以参考配置项列表。
java
ArrayList<String> arguments = Lists.newArrayList(
"--allow-running-insecure-content",
"--allow-insecure-localhost", //忽略SSL/TLS errors
"--disable-gpu", // 关闭GPU硬件加速
"--headless", //开启无头浏览器模式
"--window-size=1920,1050", //设置浏览器窗口大小
"--disable-blink-features=AutomationControlled",
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36", //自定义user-agent名称
"--cache-control=no-cache");
ChromeOptions options = new ChromeOptions();
options.addArguments(arguments);
例7:Selenium WebDriver设置自定义请求头(custom headers)
很多的时候,我们在采集数据的时候需要设置一些自定义的HTTP请求头,例如:我们想指定对某个网站链接访问是从什么地方跳转过来的,我们就需要设置referrer header。但很遗憾的是Selenium WebDriver并没有给我们提供设置HTTP请求头的接口。
今天就给大家介绍一款可以设置自定义请求头的利器BrowserMob Proxy(简称BMP)。它是一款开源软件,它不仅可以用来监控网络通信而且可以控制网络请求和响应的内容,它还可以将页面内容加载的性能数据导出到HAR文件中以便开发者可以进一步分析优化自己的网站响应速度。CMP不仅可以作为一个独立的代理服务器工作,而且可以和Selenium框架结合在一起使用。因为CMP是基于Java语言开发的,所以它可以很方便地嵌入到我们的Java程序中。CMP的下载地址是:https://github.com/lightbody/browsermob-proxy。
在本例中,我们主要示范下如何使用BMP来设置HTTP请求头,具体示例代码如下:
java
Page page = new Page("http://www.cnblogs.com/kaiblog/");
Map<String, String> headers = new HashMap<String, String>();
headers.put("Referer", "http://www.baidu.com/");
// start the proxy
BrowserMobProxy proxy = new BrowserMobProxyServer();
proxy.addHeaders(headers);
proxy.setMitmDisabled(true);
proxy.start(0);
// get the Selenium proxy object
Proxy seleniumProxy = ClientUtil.createSeleniumProxy(proxy);
// configure it to webdriver
WebDriver webDriver = WebDriverFactory.createWebDriver(seleniumProxy);
webDriver.get(page.getUrl());
webDriver.quit();
proxy.stop();
通过抓取网络数据包我们可以看到Referer请求头已经设置生效了。
总结
在本章节中,我们分别基于HttpClient和Selenium框架对网页内容进行了采集,基于Selenium框架的采集器相对于基于HttpClient的采集器在采集动态网页数据上面具有不小的优势,也避免了对加密Javascript的逆向分析。但是这并意味着使用Selenium WebDriver去采集网页内容就不会被目标网站发现了,使用Selenium WebDriver模拟驱动的浏览器与真实的浏览器在指纹特征上面还是有诸多不同的,在下面的章节中,我们会进一步详细介绍。