使用无头浏览器进行网页爬取(Puppeteer教程)

本文概述

  • 什么是无头浏览器, 为什么需要它?
  • 无头Chrome和Puppeteer
  • 准备环境
  • 设置无头Chrome和Puppeteer
  • 使用Puppeteer API进行自动Web爬网
  • 第二个操纵up的例子
  • 优化我们的Puppeteer脚本
  • 通过速率限制保持安全
  • Puppeteer在快速发展的网络中的地位
在本文中, 我们将看到使用无头浏览器的某种非传统方法来执行网络抓取(网络自动化)有多么容易。
什么是无头浏览器, 为什么需要它?在过去的几年中, Web从使用裸露的HTML和CSS构建的简单网站演变而来。现在, 有更多具有精美UI的交互式Web应用程序, 这些应用程序通常使用Angular或React之类的框架构建。换句话说, 如今JavaScript统治着网络, 几乎包括你在网站上与之互动的所有内容。
就我们的目的而言, JavaScript是一种客户端语言。服务器返回注入HTML响应中的JavaScript文件或脚本, 然后浏览器对其进行处理。现在, 如果我们要进行某种形式的Web抓取或Web自动化操作, 这将是一个问题, 因为通常情况下, 我们希望看到或抓取的内容实际上是由JavaScript代码呈现的, 并且无法从原始HTML响应中访问服务器提供的。
如上所述, 浏览器确实知道如何处理JavaScript和呈现漂亮的网页。现在, 如果我们可以利用此功能来满足我们的抓取需求, 并且有办法以编程方式控制浏览器, 该怎么办?这正是无头浏览器自动化介入的地方!
无头吗劳驾?是的, 这仅意味着没有图形用户界面(GUI)。你无需使用通常的方式(例如, 使用鼠标或触摸设备)与视觉元素进行交互, 而是可以使用命令行界面(CLI)来自动化用例。
无头Chrome和Puppeteer有许多Web抓取工具可用于无头浏览, 例如Zombie.js或使用Selenium的无头Firefox。但是今天, 我们将通过Puppeteer探索无头的Chrome, 因为它是相对较新的播放器, 于2018年初发布。编者注:值得一提的是Intoli的远程浏览器, 这是另一款新播放器, 但这将是另一个主题文章。
【使用无头浏览器进行网页爬取(Puppeteer教程)】Puppeteer到底是什么?它是一个Node.js库, 它提供了高级API来控制无头Chrome或Chromium或与DevTools协议进行交互。它由Chrome DevTools团队和一个很棒的开源社区维护。
足够多的讨论, 让我们进入代码, 探索如何使用Puppeteer的无头浏览功能自动进行网络抓取!
准备环境首先, 你需要在计算机上安装Node.js 8+。你可以在这里安装它, 或者如果你是像我这样的CLI爱好者并且喜欢在Ubuntu上工作, 请遵循以下命令:
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs

你还需要某些软件包, 这些软件包在你的系统上可能会或可能不会提供。为了安全起见, 请尝试安装以下程序:
sudo apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3

设置无头Chrome和Puppeteer我建议使用npm安装Puppeteer, 因为它还将包含稳定的最新Chromium版本, 该版本可以与该库一起使用。
在你的项目根目录中运行以下命令:
npm i puppeteer --save

注意:这可能需要一段时间, 因为Puppeteer需要在后台下载并安装Chromium。
好的, 现在我们已经完成所有设置和配置, 让我们开始乐趣吧!
使用Puppeteer API进行自动Web爬网让我们以一个基本示例开始我们的Puppeteer教程。我们将编写一个脚本, 该脚本将使无头浏览器截取所选网站的屏幕截图。
在项目目录中创建一个名为screenshot.js的新文件, 然后在你喜欢的代码编辑器中将其打开。
首先, 让我们在脚本中导入Puppeteer库:
const puppeteer = require('puppeteer');

接下来, 让我们从命令行参数中获取URL:
const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }

现在, 我们需要记住, Puppeteer是一个基于Promise的库:它对后台的无头Chrome实例执行异步调用。让我们使用async / await保持代码干净。为此, 我们需要先定义一个异步函数, 然后在其中放置所有Puppeteer代码:
async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();

总而言之, 最终代码如下所示:
const puppeteer = require('puppeteer'); const url = process.argv[2]; if (!url) { throw "Please provide URL as a first argument"; } async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();

你可以通过在项目的根目录中执行以下命令来运行它:
node screenshot.js https://github.com

请稍等, 然后开始!我们的无头浏览器刚刚创建了一个名为screenshot.png的文件, 你可以看到其中呈现的GitHub主页。太好了, 我们有一个工作正常的Chrome网络抓取工具!
让我们停一会儿, 看看上面的run()函数会发生什么。
首先, 我们启动一个新的无头浏览器实例, 然后打开一个新页面(选项卡)并导航到命令行参数中提供的URL。最后, 我们使用Puppeteer的内置方法来截取屏幕截图, 我们只需要提供保存路径即可。完成自动化之后, 我们还需要确保关闭无头浏览器。
既然我们已经介绍了基础知识, 那么让我们继续进行一些复杂的工作。
第二个操纵up的例子对于我们的Puppeteer教程的下一部分, 假设我们要抓取Hacker News的最新文章。
创建一个名为ycombinator-scraper.js的新文件, 并粘贴以下代码片段:
const puppeteer = require('puppeteer'); function run () { return new Promise(async (resolve, reject) => { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let urls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url:item.getAttribute('href'), text: item.innerText, }); }); return results; }) browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run().then(console.log).catch(console.error);

好的, 与上一个示例相比, 这里还有更多事情要做。
你可能会注意到的第一件事是, run()函数现在返回了一个Promise, 因此异步前缀已移至Promise函数的定义。
我们还将所有代码包装在try-catch块中, 以便我们处理导致承诺被拒绝的任何错误。
最后, 我们使用的是Puppeteer的内置方法, 名为validate()。通过此方法, 我们可以像在DevTools控制台中执行自定义JavaScript代码一样运行它。从该函数返回的任何内容都可以由promise解决。当涉及到抓取信息或执行自定义操作时, 此方法非常方便。
传递给valuate()方法的代码是非常基本的JavaScript, 该JavaScript构建了一个对象数组, 每个对象都有表示我们在https://news.ycombinator.com/上看到的故事URL的URL和文本字段。
脚本的输出看起来像这样(但最初有30个条目):
[ { url: 'https://www.nature.com/articles/d41586-018-05469-3', text: 'Bias detectives: the researchers striving to make algorithms fair' }, { url: 'https://mino-games.workable.com/jobs/415887', text: 'Mino Games Is Hiring Programmers in Montreal' }, { url: 'http://srobb.net/pf.html', text: 'A Beginner\'s Guide to Firewalling with pf' }, // ... { url: 'https://tools.ietf.org/html/rfc8439', text: 'ChaCha20 and Poly1305 for IETF Protocols' } ]

我要说的很整洁!
好吧, 让我们前进。我们只退回了30个项目, 而还有更多可用项目-它们仅在其他页面上。我们需要单击” 更多” 按钮以加载下一页结果。
让我们稍微修改一下脚本以添加对分页的支持:
const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage < = pagesToScrape) { let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url:item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);

让我们回顾一下我们在这里所做的事情:
  1. 我们在主要的run()函数中添加了一个名为pagesToScrape的参数。我们将使用它来限制脚本抓取的页面数。
  2. 还有一个名为currentPage的新变量, 它表示我们当前正在查看的结果页面数。最初设置为1。我们还将validate()函数包装在while循环中, 以便只要currentPage小于或等于pagesToScrape, 它就可以继续运行。
  3. 我们添加了用于移动到新页面并等待页面加载的块, 然后重新启动while循环。
你会注意到, 我们使用了page.click()方法让无头浏览器单击” 更多” 按钮。我们还使用了waitForSelector()方法来确保逻辑被暂停, 直到页面内容被加载。
这两种都是可立即使用的高级Puppeteer API方法。
在使用Puppeteer进行抓取时, 你可能会遇到的问题之一是等待页面加载。 Hacker News具有相对简单的结构, 等待页面加载完成相当容易。对于更复杂的用例, Puppeteer提供了广泛的内置功能, 你可以在GitHub的API文档中进行探索。
一切都很酷, 但是我们的Puppeteer教程尚未涵盖优化。让我们看看如何使Puppeteer更快地运行。
优化我们的Puppeteer脚本总体思路是不要让无头浏览器做任何额外的工作。这可能包括加载图像, 应用CSS规则, 触发XHR请求等。
与其他工具一样, Puppeteer的优化取决于确切的用例, 因此请记住, 其中一些想法可能不适合你的项目。例如, 如果我们在第一个示例中避免加载图像, 则我们的屏幕截图可能看起来并不像我们想要的那样。
无论如何, 这些优化可以通过在第一个请求上缓存资产或在网站发起HTTP请求时直接取消它们来实现。
让我们先看看缓存的工作原理。
你应该意识到, 当启动新的无头浏览器实例时, Puppeteer为其配置文件创建一个临时目录。当浏览器关闭时, 它将被删除, 并且在启动新实例时将无法使用它-因此将不再可访问所有存储的图像, CSS, Cookie和其他对象。
我们可以强制Puppeteer使用自定义路径来存储cookie和缓存之类的数据, 每次我们再次运行它们时, 它们将被重用-直到它们过期或被手动删除。
const browser = await puppeteer.launch({ userDataDir: './data', });

这会给我们带来很大的性能提升, 因为在第一次请求时, 许多CSS和图像都会缓存在数据目录中, 而Chrome不需要一次又一次地下载它们。
但是, 呈现页面时仍将使用这些资产。在我们搜寻Y Combinator新闻文章的需求中, 我们真的不需要担心任何视觉效果, 包括图像。我们只关心裸露的HTML输出, 因此让我们尝试阻止每个请求。
幸运的是, 在这种情况下, Puppeteer非常好用, 因为它附带了对自定义钩子的支持。我们可以为每个请求提供拦截器, 并取消我们真正不需要的请求。
拦截器可以通过以下方式定义:
await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });

如你所见, 我们对启动的请求具有完全控制权。我们可以编写自定义逻辑, 以根据请求的resourceType允许或中止特定请求。我们还可以访问许多其他数据, 例如request.url, 因此如果需要, 我们可以仅阻止特定的URL。
在上面的示例中, 我们仅允许资源类型为” document” 的请求通过我们的过滤器, 这意味着我们将阻止所有图像, CSS以及除原始HTML响应之外的所有其他内容。
这是我们的最终代码:
const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } }); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage < = pagesToScrape) { await page.waitForSelector('a.storylink'); let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url:item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.waitForSelector('a.morelink'), await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);

通过速率限制保持安全无头浏览器是非常强大的工具。他们几乎可以执行任何类型的网络自动化任务, 而Puppeteer则使这一工作变得更加轻松。尽管有各种可能性, 我们必须遵守网站的服务条款, 以确保我们不会滥用该系统。
由于这方面与架构更多相关, 因此我不会在本Puppeteer教程中对此进行深入介绍。也就是说, 减慢Puppeteer脚本速度的最基本方法是向其添加sleep命令:
js await page.waitFor(5000);
此语句将强制脚本休眠五秒钟(5000毫秒)。你可以将其放在browser.close()之前的任何位置。
就像限制第三方服务的使用一样, 还有许多其他更健壮的方法来控制对Puppeteer的使用。一个示例是建立一个工作人员数量有限的队列系统。每次你想要使用Puppeteer时, 都会将一个新任务推送到队列中, 但是只有少数工作人员能够处理其中的任务。在处理第三方API速率限制时, 这是一种相当普遍的做法, 也可以应用于Puppeteer Web数据抓取。
Puppeteer在快速发展的网络中的地位在此Puppeteer教程中, 我已经演示了其基本功能作为网络抓取工具。但是, 它具有更广泛的用例, 包括无头浏览器测试, PDF生成和性能监控等。
Web技术正在快速发展。有些网站非常依赖JavaScript呈现, 以致几乎不可能执行简单的HTTP请求来抓取它们或执行某种自动化。幸运的是, 得益于Puppeteer等项目以及背后的出色团队, 无头浏览器正变得越来越可访问以处理我们的所有自动化需求!

    推荐阅读