0%

Puppeteer配置小记

最近项目中需要实现动态数据的抓取,之前在抓取动态页面的时候都是通过 PhantomjsSelenium 、Chrome 或 Firefox的 Headless 模式等方法来实现,自从 Google Chrome 团队推出官方的 Headless Chrome 工具 Puppeteer 之后,似乎之前使用的那些工具一下都黯淡了。

Puppeteer 是一个 Node 代码库,基于 DevTools 协议,提供高级 API 自动化控制谷歌 Chrome 或 Chromium 浏览器。Puppeteer 默认以 无界面方式 运行。

安装 Puppeteer 过程中会下载完整版的谷歌Chromium浏览器到 node_modules 目录。

1.7.0 版后,谷歌发布了新的 puppeteer-core 安装包,默认不再自动下载谷歌Chromium浏览器。

puppeteer-corePuppeteer 的轻量级版本,复用本地已安装的浏览器,或者连接到远程浏览器。


安装yarn

这里我使用 yarn 来管理依赖包。

Mac
1
2
$ brew install yarn
# 由于地址源的问题,该命令执行失败
1
2
$ sudo npm install -g yarn
# 使用这条命令安装成功
Win

Chocolatey 是一个windows下的包管理器。

1
choco install yarn

使用yarn

查看版本信息
1
2
$ yarn -v
1.15.2
初始化项目
1
$ yarn init
添加一个依赖
1
2
3
$ yarn add [package]
$ yarn add [package]@[version]
$ yarn add [package]@[tag]
全局安装依赖
1
$ yarn global add [package]
更新一个依赖
1
2
3
$ yarn upgrade [package]
$ yarn upgrade [package]@[version]
$ yarn upgrade [package]@[tag]
移除一个依赖
1
$ yarn remove [package]
安装package.json中所有的依赖项
1
2
3
$ yarn
# or:
$ yarn install
查看yarn配置
1
$ yarn config list
更改 registry
1
2
#安装淘宝镜像
$ yarn config set registry "https://registry.npm.taobao.org"

安装Puppeteer

修改地址源

这里我采用的是 puppeteer-core。由于国内网络原因,需要修改仓库源地址:

1
2
3
4
5
$ npm config set registry "https://registry.npm.taobao.org"

$ yarn config set registry "https://registry.npm.taobao.org"

$ yarn add puppeteer-core
配置Chrome路径

使用 puppeteer-core,需要手动指定已安装的Chrome浏览器的安装路径。

Mac电脑上Chrome浏览器的的安装路径,可以通过在浏览器中输入 chrome:\\version 来查看。

我的电脑上的路径为:

1
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome

Puppeteer配置

无界面模式

Puppeteer 默认使用 headless 模式运行,通过设置 headless:false 来显示GUI界面:

1
2
3
const browser = await puppeteer.launch({
headless: false
});
其他配置参数

其他 puppeteer.launch() 配置项:

1
2
3
4
5
6
7
8
9
10
11
12
{
// 若是手动下载的chromium需要指定chromium地址, 默认引用地址为 `/项目目录/node_modules/puppeteer/.local-chromium/`
executablePath: '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome',
//设置超时时间
timeout: 15000,
//如果是访问https页面 此属性会忽略https错误
ignoreHTTPSErrors: true,
// 打开开发者工具, 当此值为true时, headless总为false
devtools: false,
// 关闭headless模式, 不会打开浏览器
headless: false,
}
沙箱、共享内存
1
2
3
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-dev-shm-usage']
});
  • --no-sandbox: 去沙箱运行
  • --disable-dev-shm-usage: 默认情况下,Docker运行一个 /dev/shm 共享内存空间为64MB 的容器。这通常对Chrome来说太小,并且会导致Chrome在渲染大页面时崩溃。要修复,必须运行容器 docker run --shm-size=1gb 以增加 /dev/shm 的容量。从Chrome 65开始,使用 --disable-dev-shm-usage 标志启动浏览器即可,这将会写入共享内存文件 /tmp 而不是 /dev/shm .

Linux沙箱:在计算机安全领域,沙箱(Sandbox)是一种程序的隔离运行机制,其目的是限制不可信进程的权限。沙箱技术经常被用于执行未经测试的或不可信的客户程序。为了避免不可信程序可能破坏其它程序的运行。

跳转到指定页
1
await page.goto('https://github.com/login');
等待
1
2
3
4
5
# 等待指定时间 ,second
await page.waitFor(2*1000);

# 等待某元素显示
await page.waitForSelector('body.blog');
设置视图大小
1
await page.setViewport({width: 1280, height: 600})

要注意:这里的视图大小指的是网页页面显示的大小,和浏览器界面的大小是两个概念。

截屏

fullPage 可以控制是否截取整个页面:

1
2
3
4
await page.screenshot({
path: 'jd.png',
fullPage: true
});
设置UserAgent
1
await page.setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 9_0_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13A404 Safari/601.1')
获取页面内容

返回页面的完整 html 代码,包括 doctype

1
page.content();
拦截请求页面中的图片
1
2
3
4
5
6
7
8
9
10
// 拦截请求页面中的图片
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
let url = interceptedRequest.url();
if (url.indexOf('.png') > -1 || url.indexOf('.jpg') > -1) {
interceptedRequest.abort();
} else {
interceptedRequest.continue();
}
});
跳转等待页面加载完毕

有时候使用 timeout:3000 这样的方式,并不能完全确定页面能在相应的时间范围内加载完而导致异常,可以通过如下的参数来实现,而尽量少用 timeout 这种限定性的方式:

1
await page.goto('https://discordbots.org', {waitUntil: 'domcontentloaded'});
getBoundingClientRect()

getBoundingClientRect()用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。


一个例子

这里,实现了一个简单的页面截屏的功能:

get_png.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const puppeteer = require('puppeteer-core');
const execPath = '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome';

(async () => {
const browser = await puppeteer.launch({
// 关闭headless模式, 会打开浏览器
headless: false,
executablePath: execPath,
args: ['--no-sandbox', '--disable-dev-shm-usage'],
// 超时时间
timeout: 30000,
});
const page = await browser.newPage();
await page.setViewport({
width: 1280,
height: 800,
// deviceScaleFactor: 1,
// isMobile: true
});

await page.goto('https://www.cnblogs.com/', { waitUntil: 'domcontentloaded' });

// wait for some seconds
await page.waitFor(3000);

let title = await page.title();
console.log(title);

// 截屏
await page.screenshot({ path: 'test.png', fullPage: true });

await browser.close();
})();

执行 node get_png.js 看效果。


遇到的问题

ERR_NAME_RESOLUTION_FAILED

使用 puppeteer 测试时报错:

1
2
3
4
5
6
7
8
9
10
11
2019-04-07 19:41:50: received 12260073
2019-04-07 19:41:50: https://item.jd.com/12260073.html
2019-04-07 19:41:56: the task data for 12260073 get result error: Error: net::ERR_NAME_RESOLUTION_FAILED at https://item.jd.com/12260073.html
2019-04-07 19:41:56: at navigate (/app/node_modules/puppeteer-core/lib/FrameManager.js:101:37)
2019-04-07 19:41:56: at processTicksAndRejections (internal/process/task_queues.js:86:5)
2019-04-07 19:41:56: -- ASYNC --
2019-04-07 19:41:56: at Frame.<anonymous> (/app/node_modules/puppeteer-core/lib/helper.js:110:27)
2019-04-07 19:41:56: at Page.goto (/app/node_modules/puppeteer-core/lib/Page.js:656:49)
2019-04-07 19:41:56: at Page.<anonymous> (/app/node_modules/puppeteer-core/lib/helper.js:111:23)
2019-04-07 19:41:56: at Object.runPuppet (/app/src/puppet.js:51:16)
2019-04-07 19:41:56: at processTicksAndRejections (internal/process/task_queues.js:86:5)

找到一种说法:设置 UserAgent ,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
(async function main() {
try {
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3419.0 Safari/537.36');
await page.goto('http://example.com');
//your code
await browser.close();
}
catch(e){
console.log(e);
}
})();

经测试观察,添加了 UserAgent 设置后确实不会报错了。

Promise中的console.log

在函数内有 console.log('按f12,我出现在浏览器的console中,并不在node命令行')

你会发现node命令行看不到这句话,而在Chromium的console中看见。

1
2
3
4
5
6
7
8
9
const result = await page.$eval(selector, el => {
//如果需要赋值要返回Promise
return new Promise(resolve => {
//...一波骚操作
//可以用Dom api啦
reslove(obj)
})
});
await iframe.$$eval(selector, el => {...});

在page.evaluate中用console是不能在node命令行打印出来的,不过有了监听事件就可以改变这个规则了。也可以在监听事件里面做容错处理。

如下的方式实现监听事件:

1
2
3
page.on('console', msg => {
console.log(msg);
});

Docker镜像

为了部署的方便,我实现了 puppeteer 环境的Docker镜像,具体可查看:


相关参考

如有疑问或需要技术讨论,请留言或发邮件到 service@itfanr.cc