Puppeteer

Puppeteer(词的原意是操纵木偶)是一个通过DevTools Protocol控制headless chrome或chromium的node库,也可以通过配置来使用(non-headless)chrome或chromium。

chromium是谷歌开源的web浏览器项目,chrome是基于chromium开发的。

Puppeteer可以做什么?用户在浏览器中手动执行的大部分操作都可以通过puppeteer完成。

安装puppeteer时会下载一个和puppeteer提供的API兼容的最新版本的chromium。如果不想默认下载chromium,可以安装puppeteer-core,这是一个轻量级的puppetter,是通过启动现有的已安装的浏览器或连接一个远程的浏览器,需确保是互相兼容的。

1
Puppeteer = Puppeteer-core + Chromium

Puppeteer和其他浏览器测试框架一样,创建一个Browser实例,打开页面,通过API操作页面。

一个简单的Puppeteer样例:

1
2
3
4
5
6
7
8
9
10
11
// example.js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: false, // 关闭无头模式,可以看到无头浏览器的执行过程
});
const page = await browser.newPage();
const res = await page.goto('https://example.com');
await page.screenshot({path: './example.png'});
await browser.close();
})();

上面代码实现的是访问网页并截图保存。运行命令node example.js,就可以看到当前目录下生成的截图图片。

Puppeteer对抗检测

Puppeteer开发的初衷其实是用来做测试,而不是给坏人用。

想要检测Puppeteer模拟器,很直接的就是去看request headers和浏览器运行时window的属性,但是请求头和静态属性又容易模拟,有很多在研究如何对抗检测的,如何Bypass看这篇https://intoli.com/blog/not-possible-to-block-chrome-headless/,不过静态属性对抗检测貌似都是针对headless的检测,目前发现的在non-headless下的可检测项貌似只有window.navigator.webdriver === true

自己尝试去伪造了一下navigator.webdriver

1
2
3
4
5
6
7
8
9
10
11
12
13
// puppeteer.js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: false,
});
const page = await browser.newPage();
const res = await page.goto('http://localhost:3000/test');
await page.evaluate(async () => {
Object.defineProperty(navigator, 'webdriver', { get: ()=> false });
});
// await browser.close();
})();
1
2
3
4
5
6
7
// server/index.js
const Koa = require('koa');
const app = new Koa();
const router = require('./router');
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000);
console.log('server listen on port 3000');
1
2
3
4
5
6
7
8
9
10
11
12
// server/router.js
const Router = require('koa-router');
const router = new Router();
const fs = require('fs');
const path = require('path');
router.get('/test', (ctx, next) => {
console.log('received request....');
ctx.response.type = 'html';
console.log(path.resolve(__dirname, '../template/index.html'));
ctx.response.body = fs.createReadStream(path.resolve(__dirname, '../template/index.html'));
})
module.exports = router;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// template/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<p>Hello World!</p>
<script>
const ele = document.createElement('p');
ele.innerText = navigator.webdriver;
document.body.appendChild(ele);
</script>
</body>
</html>

启node服务:node server/index.js,跑起来测试代码code runner - puppeteer.js,然后发现non-headless chrome页面上显示true,控制台里输出是false,原因是Page.evaluate方法是在页面加载之后执行的脚本,而对抗检测是在页面加载时,在puppeteer中篡改已经无用,篡改可以通过charles这样的代理工具完成js注入,在检测js前注入篡改的js。JS注入的检测,低级一点的注入之后在dom中留下了节点就容易检测,做的好的注入之后删除节点就基本没法检测了。

还有专门防御检测的插件puppeteer-extra-plugin-stealth,它的对抗衡量标准就是window静态属性检测;还可以通过reCaptcha score衡量,分数越接近1越像用户行为,不过这个分数不能作为严格的衡量标准。这款插件待分析。看了一下这个插件借助的是puppetter的page.evaluateOnNewDocument api ,而这个api是对devTools protocol的Page.addScriptToEvaluateOnNewDocument 命令的包装。下面会提到chrome-remote-interface和puppeteer都是对devTools protocol的包装。

Puppeteer原理

想探究一下puppeteer.launch()的背后,也就是想知道puppeteer是怎么和chromium交互的,找到了这个Using Chrome DevTools Protocol,然后大概看了下Puppeteer的源码,梳理了一下puppeteer的工作原理


用spawn启了一个node子进程来跑chromium.app,默认通过参数--remote-debugging-port指定端口启动,这样会启一个Chrome DevTools Protocol server同时返回一个WebSocket URL,puppeteer会通过这个urlDevTools host建立一个websocket connection,这样puppeteer就可以发出指令来操作chromium。(如果理解的没错,应该是这个逻辑,哈哈~)


下面整理了一下过程中发现的一些知识点:

  1. 通过puppeteer.launch()得到browser,输出browser.wsEndPoint可以得到websocket url,是类似这样的信息:

    1
    DevTools listening on ws://127.0.0.1:54497/devtools/browser/e3a9e795-b207-46ae-b9d8-51ee1b752716
  2. --remote-debugging-port=0参数会随机启一个端口,想到之前在启前端服务时监听的ip如果设置了0.0.0.0则会监听本机上的所有ip地址,通过任何一个ip地址都能访问到该服务,同理这里的端口0已经不是一个真正意义上的端口而是一个集合。通过wsEndpoint可以得到跑的端口,在浏览器中访问localhost:<port>可以查看所有的Inspectable pages。

  3. 建立websocket连接之后,看看发送了什么样的指令:

    1
    2
    3
    4
    5
    6
    7
    {
    id: 1,
    method: 'Target.setDiscoverTargets',
    params: {
    discover: true
    }
    }

    每条指令都会有一个唯一的id,在websocket建立的通信通道中对发送指令的响应依据的就是id。如果websocket响应内容没有id则是协议事件信息。所有的操作都有着对应的命令,也就是说,puppeteer提供出来的api是对遵循 Chrome DevTools Protocol协议的指令的包装。

  4. 在puppeteer之前,想要控制headless chrome是通过chrome-remote-interface来实现,它是比 Puppeteer API 更接近底层的实现。

  5. 在关闭无头模式后可以看到浏览器上方有Chrome is being controlled by automated test software.的提示(关闭infobar的操作是添加启动参数--disable-infobars,看这里)。

Puppeteer VS PhantomJS

至于puppeteer为什么会取代phantomjs,是因为puppeteer的爸爸是google?哈哈,开个玩笑。

phantomjs本身已经有一些问题,比如不能处理ES6规范,也不能处理一些新的移动端友好的css特性,跨平台兼容不友好等等。PhantomJS是基于webkit内核,Puppeteer则基于Blink分支内核。PhantomJS的maintainer也在2017年4月份宣布不再维护,找到了邮件原文:

Title: [Announcement] Stepping down as maintainer

Hi

I want to make an announcement.

Headless Chrome is coming - https://www.chromestatus.com/features/5678767817097216

(https://news.ycombinator.com/item?id=14101233)

I think people will switch to it, eventually. Chrome is faster and more stable than PhantomJS. And it doesn’t eat memory like crazy.

I don’t see any future in developing PhantomJS. Developing PhantomJS 2 and 2.5 as a single developer is a bloody hell.

Even with recently released 2.5 Beta version with new and shiny QtWebKit, I can’t physically support all 3 platforms at once (I even bought the Mac for that!). We have no support.

From now, I am stepping down as maintainer. If someone wants to continue - feel free to reach me.

I want to give credits to Ariya, James and Ivan! It was the pleasure to work with you. Cheers!

I also want to say thanks to all people who supported and tried to help us. Thank you!

With regards,

Vitaly.

有关的一些博文分析可以看这里:

phantomJs之殇,chrome-headless之生

Migration from PhantomJS to Chrome DevTools Protocol

自己之前也有用过PhantomJS,只记得貌似很容易被反爬拦截就结合使用了selenium,但是看到开源项目维护者发的这个邮件心里还是挺五味杂陈的,尤其看到这句I don't see any future in developing PhantomJS时自己都有点伤感。但是看到那么多人用过、还在用PhantomJS,作为一个开源贡献者也一定是欣慰的。

期待自己也有能力去做开源贡献的那一天。

Ref

Blog: Chrome DevTools Protocol

木偶浏览器

IT IS NOT POSSIBLE TO DETECT AND BLOCK CHROME HEADLESS

无头浏览器 Puppeteer 初探

puppeteer-extra-plugin-stealth——detection bypass

reCaptcha score

Fingerprint tests

Getting Started with Headless Chrome

文章目录
  1. 1. Puppeteer对抗检测
  2. 2. Puppeteer原理
  3. 3. Puppeteer VS PhantomJS
  • Ref
  • |