初识puppeteer

大概是去年还在豆腐重构web站点的时候,一项比较重要的工作就是防爬虫。当时做了一些比较简陋的防爬虫手段,也了解到了一些神奇的爬虫方式,puppeteer就是在那时候了解的。恰好最近有一个工作需求,需要为运营搭建自动化的工作流程,因此想到了puppeteer,决定尝试一下。

<!--more-->

参考

1. puppeteer的用途

puppeteer,字面意义是“操纵木偶的人”,是一个Node库,提供了一些高级API来控制Chrome无头(headless)浏览器。那么,puppetter到底能做什么呢?

事实上,我们需要在浏览器中手动进行的(极)大部分事情,都可以通过puppeteer,编写脚本自动完成。

  • 自动化测试,模拟表单提交、鼠标点击、键盘输入等
  • 高级爬虫,它可以抓取网页数据内容,甚至包括异步加载渲染的数据
  • 生成网页截图(可以用来验证CSS样式表)和PDF

2. 基本使用方法

2.1. 环境依赖

puppeteer只是一个提供了无头浏览器接口的node库,因此可以使用npm或yarn等包管理工具进行安装

  • 由于大部分接口是异步的,建议安装node v7.6以上的版本,以便支持async/await
  • 使用npm i puppeteer -S安装时会默认安装最新版本的Chromium,可以使用npm i puppeteer-core跳过该步骤,参考puppeteer vs puppeteer-core
    • 我在第一次安装时遇见了安装失败的情形,建议用梯子进行安装操作
  • 需要注意的是,无头浏览器是无法安装chrome扩展程序和应用的~

2.2. 运行流程

下面代码展示了一个脚本的实现

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代码,这样,我们就可以操作页面内容,实现各种各样的业务需求了。

2.3. 常见的需求

实际上一般的需求并不需要了解puppeteer全部的功能,,下面整理了一些常见需求的实现接口,由于版本变动较多,在你阅读时建议参考官方文档进行查询。

各种各样的模拟事件

  • 点击事件

    page.click(selector[, options])
    page.tap(selector)
  • 鼠标相关的事件封装在page.mouse类中

    mouse.click(x, y[, options])
    mouse.down([options])
    mouse.move(x, y[, options])
    mouse.up([options])
  • 键盘相关的事件封装在page.keyboard类中

    keyboard.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)

3. 一个实现百度自动搜索的例子

下面的代码实现了打开百度,搜索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();
})();

实现自动化任务时,gototypeevaluate等方法是十分常用的,详情用法参考api文档

4. 如何识别无头浏览器

无头浏览器的本意是自动执行各种任务,比如做测试、截屏等,但也可以被用来自动执行恶意任务,如恶意网络爬虫、伪装访问量等。在某些

参考如何用JavaScript检测出当前浏览器是否是无头浏览器(headless browser)?这篇文章。由于其中很多方法,在使用puppeteer时都检测不到,因此这里整理一下

4.1. 验证思路

通过切换puppeteer.launch时的headless,来切换脚本的浏览器运行环境。

通过对比有头模式和无头模式输出的差异,来判断在页面的JavaScript中,是否可以通过对应的方式来检测无头浏览器的访问。

由于无头浏览器运行的特殊性,无法至直接查看输出

  • 可以通过page.on('console')或者page.content()的方法来检测无头模式下的输出
  • 可以直接在有头模式的浏览器控制台中输出对应指令,直接查看对应输出

4.2. 通过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)

4.3. 通过浏览器插件数目可以检测puppeteer

navigator.plugins 会返回一个数组,里面是当前浏览器里的插件信息,

  • 无头模式里,没有任何插件,返回的是个空数组,
  • 有头模式下,默认长度为3

因此可以判断

let isHeadless = navigator.plugins.length

4.4. 通过语言检测puppeteer

文中提到,在无头模式里,navigator.languages 返回的是个空字符串,

  • 无头模式下,经测试发现最新版本Chromium无头模式返回的是字符串en-US
  • 有头模式下,返回的是一个数组["zh-CN", "zh"]

因此暂且可以通过下面判断,

let isHeadless = Array.isArray(navigator.languages)

4.5. 通过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.'

4.6. 通过加载失败的图片无法检测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

因此这种方法不能判断是否为无头浏览器。

5. 小结

最先接触到无头浏览器的时候,就感觉到这是一个非常有用的工具,除了能够实现前端UI测试之外,还可以处理自动任务,完成服务端爬虫无法完成的任务等。

虽然篇头提到的运营搭建自动化的工作流程的需求,由于安全性和使用性的考虑,最后还是采用了Chrome扩展程序来完成一些功能,但是使用puppeteer,应该是最正确的做法。后面如果有类似需求常见,可以再进一步学习相关知识。