用Java进行基本的网络爬取的两种不同方法:HtmlUnit用于爬取基本网站,PhantomJS用于爬取大量使用JavaScript的动态网站。
两者都是巨大的工具,PhantomJS在很长一段时间内碰巧成为该市场的领导者是有原因的。尽管如此,偶尔也会有一些问题,无论是性能还是对网络标准的支持。然后,在2017年,这个领域出现了一个真正的游戏规则改变者,当时谷歌和Mozilla都开始在各自的浏览器中原生支持一个叫做无头模式的功能。
欢迎使用Chrome浏览器的脚本
事实证明,Chrome的无头模式(Chrome Headless)是非常实用和强大的。它允许你拥有与HtmlUnit和PhantomJS相同的控制和自动化,但这次是在你每天都在使用的浏览器引擎的背景下,而且你现在很可能正在使用它来阅读这篇文章。
没有更多的问题,只有部分支持的CSS功能,可能稍微不兼容的JavaScript代码,或过于复杂的HTML页面的性能瓶颈。你最喜欢的浏览器引擎就在你的指尖,只需要几个Java命令就可以了。
好了,我们现在已经对理论惊叹了很久,是时候进入编码阶段了。
让我们来看看
我们将从简单的东西开始,在我们的第一个例子中尝试获取Hacker News的截图。在我们可以直接进入编码并让浏览器为我们服务之前,我们首先需要确保我们有合适的环境和所有必要的工具。
至于浏览器,我们要使用的库(破坏者,Selenium)实际上支持许多不同的浏览器,虽然在这里的例子中,我们将专注于Chrome,但应该很容易就能为不同的浏览器引擎切换驱动程序。
因此,不再赘述,这里列出了你运行我们的代码示例所需要的软件包。
准备条件
- 一个最新的JDK,版本8或更高
- 最新版本的谷歌浏览器
- 匹配你的Chrome浏览器版本的ChromeDriver二进制文件
- Selenium库
设置
我们说的是屏幕截图,对吗?但为了使事情变得更有趣,这将是一个有转折的截图。这将是一个 “认证 “的截图,所以我们将需要有一个用户名和密码,并提供给登录对话。
首先,我们需要获得一个WebDriver
对象的实例(因为我们使用的是Chrome浏览器,所以在实现上将是ChromeDriver
),以便能够首先访问浏览器。我们可以通过在webdriver.chrome.driver
系统属性中指定ChromeDriver二进制文件的路径,并通过几个选项实例化一个ChromeDriver
类来相对容易地实现这一目标。
// Initialise ChromeDriver String chromeDriverPath = "/Path/To/Chromedriver" ; System.setProperty("webdriver.chrome.driver", chromeDriverPath); ChromeOptions options = new ChromeOptions(); options.addArguments("--headless", "--window-size=1920,1200","--ignore-certificate-errors"); WebDriver driver = new ChromeDriver(options);
至于Chrome,ChromeDriver应该会自动找到它的安装目录,但如果你喜欢自定义路径,你可以通过ChromeOptions.setBinary来设置它。
options.setBinary("/Path/to/specific/version/of/Google Chrome");
所有ChromeDriver选项的完整列表可以在这里找到。
截 图
现在,我们已经准备好获得我们的屏幕截图。我们所需要做的是
- 获取登录页面
- 用正确的值填入用户名和密码字段
- 点击登录按钮
- 检查认证是否成功
- 获得我们当之无愧的截图 –PROFIT
由于我们在以前的文章中已经介绍了这些步骤,我们将简单地提供完整的代码。
public class ChromeHeadlessTest { private static String userName = ""; private static String password = ""; public static void main(String[] args) throws IOException { String chromeDriverPath = "/your/chromedriver/path"; System.setProperty("webdriver.chrome.driver", chromeDriverPath); ChromeOptions options = new ChromeOptions(); options.addArguments("--headless", "--window-size=1920,1200","--ignore-certificate-errors", "--silent"); WebDriver driver = new ChromeDriver(options); // Get the login page driver.get("https://news.ycombinator.com/login?goto=news"); // Search for username / password input and fill the inputs driver.findElement(By.xpath("//input[@name='acct']")).sendKeys(userName); driver.findElement(By.xpath("//input[@type='password']")).sendKeys(password); // Locate the login button and click on it driver.findElement(By.xpath("//input[@value='login']")).click(); if (driver.getCurrentUrl().equals("https://news.ycombinator.com/login")) { System.out.println("Incorrect credentials"); driver.quit(); System.exit(1); } System.out.println("Successfully logged in"); // Take a screenshot of the current page File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); FileUtils.copyFile(screenshot, new File("screenshot.png")); // Logout driver.findElement(By.id("logout")).click(); driver.quit(); } }
一旦你编译并运行这段代码,你的工作目录中应该有一个名为screenshot.png的文件,其中有一张Hacker News主页的华丽截图,包括你的用户名和一个注销链接。
还有什么?
从你的浏览器中获得实时屏幕截图是很好的,但你知道什么更棒吗?与网站互动。
关于无限滚动,有很多不同的意见。虽然有人喜欢,有人不喜欢,但它是当今网页设计中的一个共同主题,而且由于其动态内容的性质和JavaScript的大量参与,在网页爬取中处理起来总是有点棘手。而这正是一个成熟的浏览器引擎将大显身手的领域,因为它将允许你以完全自然的方式处理页面,而不需要任何变通。让我们来看看,好吗?
无限滚动
外面有很多使用无限滚动的服务,但由于我们这次真的想把重点放在滚动部分,而不是那么多的处理认证,我们遵循任何半成品电视厨师的传统,使用ScrapingBee已经准备好的滚动例子,https://demo.scrapingbee.com/infinite_scroll.html。
一句话,那个页面真的是无限滚动的定义,只要你滚动到页面的底部,它就会追加更多的内容。
我们现在如何处理这个问题?
首先,我们设置了与前一个例子中使用的相同环境。这包括设置对ChromeDriver的引用,初始化WebDriver实例,以及用WebDriver.load()
加载页面。到目前为止,还不错,没有什么新东西。
第一件新事是,我们要使用新引入的getCurrentHeight()
方法来确定页面的初始高度。
private static long getCurrentHeight(JavascriptExecutor js) { Object obj = js.executeScript("return document.body.scrollHeight"); if (!(obj instanceof Long)) throw new RuntimeException("scrollHeight did not return a long"); return (long)obj; }
在这里,我们基本上是在页面的上下文中运行返回document.body.scrollHeight的JavaScript片段,这为我们提供了页面的当前高度。我们把这个信息保存在prevHeight中,以后用它来检查页面的高度是否有变化。接下来是无穷大!♾
我们现在运行一个无限循环(好吧,就像所有这样的循环一样,我们需要一个相当可靠的退出条款),我们首先用XPath表达式(//div[@class=’box’])[last()]获取我们页面上的最后一个内容元素,然后用WebElement.getText()获取其内容。这将作为我们的内容ID。
WebElement lastElement = driver.findElement(By.xpath("(//div[@class='box'])[last()]")); int id = Integer.parseInt(lastElement.getText());
一旦我们有了我们的内容ID,我们就可以滚动到底部,并希望我们的网站能够加载更多的内容(在我们的例子中,它总是会的)。要做到这一点,我们–再次–运行一个小的JavaScript片段,使用window.scrollTo(),并等待几毫秒。现在我们将检查我们是否已经满足了退出条件,如果没有,继续循环。
js.executeScript("window.scrollTo(0, document.body.scrollHeight);"); Thread.sleep(500); long height = getCurrentHeight(js); if (id > 100 || height == prevHeight || height > 30000) break; prevHeight = height;
我们的退出条件,对吧!?尽管无限滚动真的受到很多人的喜爱,但我们真的想避免从字面上进入一个无限的循环。为了做到这一点,我们假设一旦我们有了至少100个元素或者页面至少有30000像素的高度,我们就会很高兴。当然,一旦高度保持不变,我们也就没有什么可加载的了。
一旦我们退出了我们的循环,我们只需要收集所有的内容就可以了。
List boxes = driver.findElements(By.xpath("//div[@class='box']")); for (WebElement box : boxes) System.out.println(box.getText());
现在是压轴大戏,完整的代码。
public class InfiniteScrolling { public static void main(String[] args) throws IOException, Exception { String chromeDriverPath = "/your/chromedriver/path"; System.setProperty("webdriver.chrome.driver", chromeDriverPath); ChromeOptions options = new ChromeOptions(); options.addArguments("--headless", "--window-size=1920,1200","--ignore-certificate-errors", "--silent"); WebDriver driver = new ChromeDriver(options); JavascriptExecutor js = (JavascriptExecutor)driver; // Load our page driver.get("https://demo.scrapingbee.com/infinite_scroll.html"); // Determine the initial height of the page long prevHeight = getCurrentHeight(js); while (true) { // Get the last div element of class box WebElement lastElement = driver.findElement(By.xpath("(//div[@class='box'])[last()]")); // Consider the element's text content the ID int id = Integer.parseInt(lastElement.getText()); // Scroll to the bottom and wait for 500 milliseconds js.executeScript("window.scrollTo(0, document.body.scrollHeight);"); Thread.sleep(500); // Get the current height of the page long height = getCurrentHeight(js); // Stop if we reached at least 100 elements, the bottom of the page, or a height of 30,000 pixels if (id > 100 || height == prevHeight || height > 30000) break; prevHeight = height; } // Get all div elements of class "box" and print their IDs List boxes = driver.findElements(By.xpath("//div[@class='box']")); for (WebElement box : boxes) System.out.println(box.getText()); driver.quit(); } private static long getCurrentHeight(JavascriptExecutor js) { Object obj = js.executeScript("return document.body.scrollHeight"); if (!(obj instanceof Long)) throw new RuntimeException("scrollHeight did not return a long"); return (long)obj; } }
禁用图像
虽然图片看起来很可爱,并能真正改善整体的用户体验,但对于网络爬取的目的来说,它们往往是有些多余的。特别是在适当的浏览器引擎的背景下,它们将一直被下载,然而,它们会影响性能和网络流量,特别是在更大的范围内。
一个相对容易的解决方法是指示浏览器不要下载图片,只关注实际的HTML内容,以及JavaScript和CSS。而这正是我们现在要做的事情。
让我们以一个图像非常丰富的网站为例,即Pinterest,把我们原来的截图代码改编一下,只加载pinterest.com的主页并进行截图。
String chromeDriverPath = "/your/chromedriver/path"; System.setProperty("webdriver.chrome.driver", chromeDriverPath); ChromeOptions options = new ChromeOptions(); options.addArguments("--headless", "--window-size=1920,1200","--ignore-certificate-errors", "--silent"); WebDriver driver = new ChromeDriver(options); // Load our page driver.get("https://www.pinterest.com/"); // Take a screenshot of the current page File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); FileUtils.copyFile(screenshot, new File("screenshot.png")); driver.quit();
一旦我们编译并运行该代码,我们将得到一个名为screenshot.png的文件。现在,如果我们想运行同样的请求,但让浏览器忽略图像,我们需要做的就是在ChromeOptions列表中添加blink-settings=imagesEnabled参数并将其设置为false。
options.addArguments("--headless", "--window-size=1920,1200","--ignore-certificate-errors", "--silent", "--blink-settings=imagesEnabled=false");[文中代码源自Scrapingbee]
如果我们现在运行这段代码,我们将得到同样的截图,但没有图像。
即使,我们在这里专门拍了一张截图,如果你的项目依赖于精确的截图,这显然会不太有用,但如果你主要是追求文本数据,它可以为你节省相当多的流量成本。
总 结
正如你所看到的,Chrome headless真的很容易使用,它与PhantomJS几乎没有任何区别,因为我们使用Selenium来运行它。代码可以在这个Github资源库中找到。