Puppeteer简单使用
Puppeteer是 Google Chrome 团队官方的 Headless Chrome 工具,平时常用它来完成一些烦杂的重复性工作,也写过一些爬虫,在浏览器中手动完成的大部分事情都可以使用 Puppeteer 完成。也算是测试同学手中的一大利器吧。
安装
就按管方文档中来吧,主要就是设置两个环境变量:
const browser = await puppeteer.launch({
executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
headless: false,
slowMo: 500,
devtools: true
});
在Docker上运行
const puppeteer = require('puppeteer');
// 从 puppeteer.launch() 为:
const browser = await puppeteer.connect({ browserWSEndpoint: 'ws://localhost:3000' });
const page = await browser.newPage();
...
await page.goto(...);
...
await browser.disconnect();
注意:
因为Chrome默认使用 /dev/shm 共享内存,但是 docker 默认 /dev/shm 很小。所以启动Chrome要添加参数 -disable-dev-shm-usage ,不用/dev/shm共享内存。
获取Console内容
await page.evaluate(() => {debugger;});
添加自定义函数
添加MD5函数
const puppeteer = require('puppeteer');
const fs = require('fs');
puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.exposeFunction('readfile', async filePath => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, text) => {
if (err)
reject(err);
else
resolve(text);
});
});
});
await page.evaluate(async () => {
// 使用 window.readfile 读取文件内容
const content = await window.readfile('/etc/hosts');
console.log(content);
});
await browser.close();
});
向中 window 添加方法的功能很强大,可以避免浏览器的一些限制。
页面加载前定制处理
evaluateOnNewDocument 可以指定函数在所属的页面被创建,并且所属页面的任意 script 执行之前被调用。可以用这个办法修改页面的javascript环境,比如给 Math.random 设定种子等。
下面是在页面加载前重写 navigator.languages 属性的例子:
//Firstly, we need to override the permissions
//so we don't have to click "Allow Location Access"
const context = browser.defaultBrowserContext();
await context.overridePermissions(url, ['geolocation']);
...
const page = await browser.newPage();
//whenever the location is requested, it will be set to our given lattitude, longitude
await page.evaluateOnNewDocument(function () {
navigator.geolocation.getCurrentPosition = function (cb) {
setTimeout(() => {
cb({
'coords': {
accuracy: 21,
altitude: null,
altitudeAccuracy: null,
heading: null,
latitude: 0.62896,
longitude: 77.3111303,
speed: null
}
})
}, 1000)
}
});
请求拦截
举个例子,通过请求拦截器取消所有图片请求,这样可以加快执行的速度:
const puppeteer = require('puppeteer')
const parallel = 5;
(async () => {
puppeteer.launch().then(async browser => {
const promises = []
for (let i = 0; i < parallel; i++) {
console.log('Page ID Spawned', i)
promises.push(browser.newPage().then(async page => {
await page.setViewport({ width: 1280, height: 800 })
await page.goto('https://en.wikipedia.org/wiki/' + i)
await page.screenshot({ path: 'wikipedia_' + i + '.png' })
}))
}
await Promise.all(promises)
await browser.close()
})
})();
前端运行的代码
在运用Puppeteer过程中,免不得大量的运行在前端的代码,即运行在浏览器中的代码。主要用于查找元素、获取元元素的属性等,以下举几个例子说明:
定位元素
for (let h = 1; h <= numPages; h++) {
// 跳转到指定页码
await page.goto(`${searchUrl}&p=${h}`);
// 执行爬取
const users = await page.evaluate((sInfo, sName, sEmail) => {
return Array.prototype.slice.apply(document.querySelectorAll(sInfo))
.map($userListItem => {
// 用户名
const username = $userListItem.querySelector(sName).innerText;
// 邮箱
const $email = $userListItem.querySelector(sEmail);
const email = $email ? $email.innerText : undefined;
return {
username,
email,
};
})
// 不是所有用户都显示邮箱
.filter(u => !!u.email);
}, USER_LIST_INFO_SELECTOR, USER_LIST_USERNAME_SELECTOR, USER_LIST_EMAIL_SELECTOR);
// 获取 html
// 获取上下文句柄
const bodyHandle = await page.$('body');
// 执行计算
const bodyInnerHTML = await page.evaluate(dom => dom.innerHTML, bodyHandle);
// 销毁句柄
await bodyHandle.dispose();
console.log('bodyInnerHTML:', bodyInnerHTML);
而 page.$eval看上去简洁得多:
// 截屏
await page.screenshot({
path: './full.png',
fullPage: true
// 也可截部分
// clip: {x: 0, y: 0, width: 1920, height: 800}
});
// 截元素
let [el] = await page.$x('#order-item');
await el.screenshot({
path: './part.png'
});
避免页面中DOM变化
如果页面中DOM会被javascript改动时,可以考虑合并多个 async ,不要用:
await page.evaluate(() => {
const $atag = document.querySelector('a.order-list');
const text = $atag.href;
$atag.click();
});
两个运行环境
Puppeteer代码是分别跑在Node.js和浏览器两个javascript运行时中的。Puppeteer脚本是运行在Node.js中的,但是 evaluate 、 evaluateHandle 等操作DOM的代码却是运行在浏览器中的。同样,Puppeteer也提供了提供了 ElementHandle 和 JsHandle 将 页面中元素和DOM对象封装成对应的 Node.js 对象,这样可以直接这些对象的封装函数进行操作 Page DOM。理解这些概念很重要。
所以在执行前端代码时,前端代码函数会先被序列化传给浏览器再运行。所以,两个运行时不能共享变量:
const atag = 'a';
await page.goto(...);
const clicked = await page.evaluate(($sel) => document.querySelector($sel).click(), atag);
等待
等待页面加载
几个打开页面的函数,如goto、waitForNavigation、reload等函数内置有等待参数:waitUtil 和 timeout,可以用它来等待页面打开:
await page.goto(...);
await Promise.all([
page.click('a'),
await page.waitForNavigation()
]);
等待元素或响应
- page.waitForXPath:用XPath等待页面元素,返回对应的 ElementHandle 实例
- page.waitForSelector :用CSS选择器等待页面元素,返回对应的 ElementHandle 实例
- page.waitForResponse :等待响应结束,返回 Response 实例
- page.waitForRequest:等待请求发起,返回 Request 实例
await page.goto('...', {
timeout: 60000,
waitUntil: 'networkidle2'
});
// 业务代码中设定window中的对象,存在表示加载完成
let acquireHandle = await page.waitForFunction('window.ACQUIREDONE', {
polling: 120
});
const acquireResult = await acquireHandle.jsonValue();
console.info(acquireResult);
基于Puppeteer的框架
从上面看出Puppeteer编写脚本并不是很直观,可以考虑用其它更好的框架,比如Rize 。比如,用Rize写的代码类似于下面这样的,明显比原生的Puppeteer代码要简洁、直观的多。
原生的Puppeteer代码:
const Rize = require('rize')
const rize = new Rize()
rize
.goto('https://github.com')
.saveScreenshot('github.png')
.end()
而且用Rize写代码时,仍然可以用原生Puppeteer的Api来写。
性能优化
- 如有可能尽量使用同一个浏览器实例,或多个实例指定相同的缓存路径,这样缓存可以共用
- 通过请求拦截没必要加载的资源,比如图片或媒体等
- 减少打开的 tab 页数量,以免占用太多的资源,长时间运行的Puppeteer脚本,最好定时重启 Chrome 实例
- 启动Chrome时关闭没必要的配置,比如:-no-sandbox(沙箱功能),--disable-extensions(扩展程序)等
作者:OF656
链接:https://www.jianshu.com/p/2f233e60bf33
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。