本文概述
- 什么是无头浏览器, 为什么需要它?
- 无头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);
让我们回顾一下我们在这里所做的事情:
- 我们在主要的run()函数中添加了一个名为pagesToScrape的参数。我们将使用它来限制脚本抓取的页面数。
- 还有一个名为currentPage的新变量, 它表示我们当前正在查看的结果页面数。最初设置为1。我们还将validate()函数包装在while循环中, 以便只要currentPage小于或等于pagesToScrape, 它就可以继续运行。
- 我们添加了用于移动到新页面并等待页面加载的块, 然后重新启动while循环。
这两种都是可立即使用的高级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等项目以及背后的出色团队, 无头浏览器正变得越来越可访问以处理我们的所有自动化需求!
推荐阅读
- 在Vanilla JS中模拟React和JSX
- 探索SMACSS(CSS的可扩展和模块化架构)
- Angular 6教程(使用新功能)
- 流行的静态站点生成器概述
- 我可以使用`xarray.apply_ufunc`并行化`numpy.bincount`吗()
- 无法解决(com.google.android.support.gms:play-services-map:10.2.0)
- Android - Google地图为位置返回null [重复]
- Android Studio未检测到Pepper Android平板电脑
- 从Android中的Intent中选择时,文件返回空(“”)