初识puppeteer
大概是去年还在豆腐重构web站点的时候,一项比较重要的工作就是防爬虫。当时做了一些比较简陋的防爬虫手段,也了解到了一些神奇的爬虫方式,puppeteer就是在那时候了解的。恰好最近有一个工作需求,需要为运营搭建自动化的工作流程,因此想到了puppeteer,决定尝试一下。
参考
puppeteer的用途
puppeteer,字面意义是“操纵木偶的人”,是一个Node库,提供了一些高级API来控制Chrome无头(headless)浏览器。那么,puppetter到底能做什么呢?
事实上,我们需要在浏览器中手动进行的(极)大部分事情,都可以通过puppeteer,编写脚本自动完成。
- 自动化测试,模拟表单提交、鼠标点击、键盘输入等
- 高级爬虫,它可以抓取网页数据内容,甚至包括异步加载渲染的数据
- 生成网页截图(可以用来验证CSS样式表)和PDF
基本使用方法
环境依赖
puppeteer只是一个提供了无头浏览器接口的node库,因此可以使用npm或yarn等包管理工具进行安装
- 由于大部分接口是异步的,建议安装
node v7.6
以上的版本,以便支持async/await
- 使用
npm i puppeteer -S
安装时会默认安装最新版本的Chromium
,可以使用npm i puppeteer-core
跳过该步骤,参考puppeteer vs puppeteer-core- 我在第一次安装时遇见了安装失败的情形,建议用梯子进行安装操作
- 需要注意的是,无头浏览器是无法安装chrome扩展程序和应用的~
运行流程
下面代码展示了一个脚本的实现
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
// 获取page对象后我们就可以进行各种各样的操作了
});
launch
方法用于生成一个无头浏览器的实例,可以在构建时传入一些配置参数,其中headless模式关闭后,可以查看浏览器的运行过程,在开发模式下方便调试browser.newPage
方法 新开一个新标签页并返回标签页的实例page- page 对象包含了一系列可对页面进行的操作,比如打开url,点击某个选择器,选择某个输入框输入文字、页面滚动等等操作
page还有一个非常重要page.evaluate方法,可以向打开的页面上注入js代码,这样,我们就可以操作页面内容,实现各种各样的业务需求了。
常见的需求
实际上一般的需求并不需要了解puppeteer全部的功能,,下面整理了一些常见需求的实现接口,由于版本变动较多,在你阅读时建议参考官方文档进行查询。
各种各样的模拟事件
点击事件
jspage.click(selector[, options]) page.tap(selector)
鼠标相关的事件封装在
page.mouse
类中jsmouse.click(x, y[, options]) mouse.down([options]) mouse.move(x, y[, options]) mouse.up([options])
键盘相关的事件封装在
page.keyboard
类中jskeyboard.down(key[, options]) keyboard.press(key[, options]) keyboard.sendCharacter(char) keyboard.type(text[, options]) keyboard.up(key)
这比之前在chrome扩展 程序中实现模拟表单参数选择流程要简单得多,
// 在扩展程序中模拟点击,
// 当流程比较复杂时,需要模拟的操作和编写的代码就十分繁琐,很显然page.click要方便很多
function triggerClick(dom){
var event = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': true
});
dom.dispatchEvent(event);
}
监听页面事件
提供了一系列事件,用于监听页面上不同的事件,具体使用参考事件列表,几个比较常用的事件包括
// 关闭页面弹窗
page.on('dialog', dialog => {
dialog.accept('test');
});
// 页面网络请求
page.on('request', request => {
request.continue(); // pass it through.
});
// 页面网络响应
page.on('response', response => {
const req = response.request();
console.log(req.method, response.status, req.url);
});
操作cookie、请求头等
正常情况下,我们可以通过通过模拟表单登录,自动获取cookie等信息,但在某些时候可能需要手动填写一些cookie或其他header信息,可以通过下面接口处理
page.cookies([...urls])
page.deleteCookie(...cookies)
page.emulate(options)
一个实现百度自动搜索的例子
下面的代码实现了打开百度,搜索puppeteer关键字,获取页面第一页的所有搜索标题,然后跳转到第一条搜索记录的一系列自动任务
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
// 打开百度
await page.goto('https://baidu.com');
// 在搜索框输入puppeteer,每个字符间隔100ms
await page.type('#kw', 'puppeteer', { delay: 100 });
// 点击搜索按钮
page.click('#su')
// 等待1s
await page.waitFor(1000);
// 获取页面搜索内容的标题
let resultTitle = await page.evaluate(() => {
return [...document.querySelectorAll("#content_left .t a")].map(item => {
// 这里的console在无头浏览器的控制台输出
console.log(item.innerText);
return item.innerText;
}
);
});
// 这里的console在执行当前脚本的系统终端里输出
console.log(resultTitle);
// 点击第一条搜索记录
await page.click("#content_left .t a");
// 等待跳转
await page.waitFor(1000);
// 取消无头浏览器的自动关闭
// browser.close();
})();
实现自动化任务时,goto
、type
、evaluate
等方法是十分常用的,详情用法参考api文档。
如何识别无头浏览器
无头浏览器的本意是自动执行各种任务,比如做测试、截屏等,但也可以被用来自动执行恶意任务,如恶意网络爬虫、伪装访问量等。在某些
参考如何用JavaScript检测出当前浏览器是否是无头浏览器(headless browser)?这篇文章。由于其中很多方法,在使用puppeteer时都检测不到,因此这里整理一下
验证思路
通过切换puppeteer.launch
时的headless
,来切换脚本的浏览器运行环境。
通过对比有头模式和无头模式输出的差异,来判断在页面的JavaScript中,是否可以通过对应的方式来检测无头浏览器的访问。
由于无头浏览器运行的特殊性,无法至直接查看输出
- 可以通过
page.on('console')
或者page.content()
的方法来检测无头模式下的输出 - 可以直接在有头模式的浏览器控制台中输出对应指令,直接查看对应输出
通过userAgent可以检测puppeteer
userAgent可以获取浏览器的基本信息
无头模式下,浏览器为HeadlessChrome
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWeb Kit/537.36 (KHTML, like Gecko) HeadlessChrome/72.0.3617.0 Safari/537.36
有头模式下,浏览器为Chrome
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3617.0 Safari/537.36"
因此可以在业务代码中通过下面判断
let isHeadlessChrome = /HeadlessChrome/.test(navigator.userAgent)
通过浏览器插件数目可以检测puppeteer
navigator.plugins
会返回一个数组,里面是当前浏览器里的插件信息,
- 无头模式里,没有任何插件,返回的是个空数组,
- 有头模式下,默认长度为3
因此可以判断
let isHeadless = navigator.plugins.length
通过语言检测puppeteer
文中提到,在无头模式里,navigator.languages
返回的是个空字符串,
- 无头模式下,经测试发现最新版本Chromium无头模式返回的是字符串
en-US
, - 有头模式下,返回的是一个数组
["zh-CN", "zh"]
因此暂且可以通过下面判断,
let isHeadless = Array.isArray(navigator.languages)
通过WebGL检测puppeteer
文中提到可以使用webgl图形驱动的 vendor 和 renderer 来进行检测
var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl');
var debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
var vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
var renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
console.log(vendor)
- 无头模式下,输出值为
Google Inc.
- 有头模式下,输入值为
Intel Inc.
由于该值跟系统硬件有关系,比较容易误伤正常用户,下面可以作为保留检测手段
let isHeadless = vendor === 'Google Inc.'
通过加载失败的图片无法检测puppeteer
文中最后还提到了一种通过判断加载失败的图片尺寸为0的方式来检测无头浏览器,
var body = document.getElementsByTagName("body")[0];
var image = document.createElement("img");
image.src = "http://iloveponeydotcom32188.jg";
image.setAttribute("id", "fakeimage");
body.appendChild(image);
image.onerror = function(){
if(image.width == 0 && image.height == 0) {
console.log("Chrome headless detected");
}
}
经测试发现,这种方式貌似不生效
- 在无头模式下,加载失败图片的宽高均为16
- 在有头模式下,加载失败图片的宽高均为16
因此这种方法不能判断是否为无头浏览器。
小结
最先接触到无头浏览器的时候,就感觉到这是一个非常有用的工具,除了能够实现前端UI测试之外,还可以处理自动任务,完成服务端爬虫无法完成的任务等。
虽然篇头提到的运营搭建自动化的工作流程的需求,由于安全性和使用性的考虑,最后还是采用了Chrome扩展程序来完成一些功能,但是使用puppeteer,应该是最正确的做法。后面如果有类似需求常见,可以再进一步学习相关知识。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。