几个Puppeteer使用小技巧
总结一些使用puppetter的小技巧,从下面几个角度:
- 浏览器的启动与请求
- 页面的加载与渲染
- 执行优化与状态管理
浏览器的启动与请求
自定义chromium/chrome路径
默认的puppeteer会在module内部下载一个满足当前版本的chromium,有时由于网络的原因还经常下载失败,民生怨道。
从v1.7.0开始,有了puppeteer-core这个轻量级使用puppeteer的方案,可以用它来指定chromium/chrome路径。这样就可以使用系统中所安装的chrome了(puppeteer内部会使用child_process.spawn()
开启使用指定可执行文件的子进程)。
需要注意以下几点:
- 若指定了系统中的chrome,需要注意它的版本是否满足puppetter要求
- puppeteer-core不会自动下载chromium
- 会忽略所有
PUPPETEER_*
环境变量
import puppeteer from 'puppeteer-core'
const getDefaultOsPath = () => {
if (process.platform === 'win32') {
return 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
} else {
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
}
}
let browser = await puppeteer.launch({
executablePath: getDefaultOsPath()
}))
UA相关
获取UA
async function getPuppeteerChromeUA() {
const browser = await puppeteer.launch();
const ua = await browser.userAgent();
await browser.close();
return ua;
}
使用匿名UA
封装一个函数来设置匿名UA:
async function setAnonymizeUA (page, opts) {
let ua = await page.browser().userAgent()
// 1. 替换headless标识
if (opts.stripHeadless) {
ua = ua.replace('HeadlessChrome/', 'Chrome/')
}
// 2. 设为win10平台
if (opts.makeWindows) {
ua = ua.replace(/\(([^)]+)\)/, '(Windows NT 10.0; Win64; x64)')
}
// 3. 使用自定义函数处理ua
if (opts.customFn) {
ua = opts.customFn(ua)
}
await page.setUserAgent(ua)
}
页面的加载与渲染
屏蔽指定类型资源的请求
使用setRequestInterception()
拦截请求,屏蔽指定类型请求来加快加载速度
...
const blockTypes = new Set(['image', 'media', 'font'])
await page.setRequestInterception(true)
page.on('request', request => {
const type = request.resourceType()
const shouldBlock = blockedTypes.has(type)
this.debug('onRequest', { type, shouldBlock })
return shouldBlock ? request.abort() : request.continue()
})
...
注意: 启用请求拦截会使页面缓存不可用
控制page对象加载页面的阶段
通过设置goto
函数中的waitUntil
参数,使page在DOMContentLoaded
事件触发时就返回结果,而无需等到Load事件,这样就节省了等待构建渲染树与页面绘制的时间。
对应CDP中的Page.lifecycleEvent
...
let page = await browser.newPage()
await page.goto('http://some.site', {waitUntil: 'domcontentloaded'})
...
执行优化与状态管理
记录页面内部的console信息
通过Page对象的console事件监听页面内部打印的console日志并记录下来
export const recordInnerLog = (page: Page, filePath: string): void => {
const timestamp = new Date().valueOf();
const logFile = fs.createWriteStream(`${filePath}/log/${timestamp}.txt`, 'utf-8');
page.on('console', msg => {
for (const arg of msg.args()) {
arg.jsonValue().then(v => {
v && logFile.write(JSON.stringify(v) + '\n');
});
}
});
};
使用单例浏览器实例
在同一个程序中使用多个爬虫对象时,某些情况下可以选择复用同一个浏览器实例,而不用每启动一个爬虫都new一个browser实例出来。
// instance.js
const pptr = require('puppeteer');
let instance = null;
module.exports.getBrowserInstance = async function() {
if (!instance)
instance = await pptr.launch();
return instance;
}
使用:
const {getBrowserInstance} = require('./instance');
async function doWork() {
// ....
const browser = await getBrowserInstance(); // this will reuse single browser
// ....
}
也可以使用如下简便方法:
let browserInstance = null
const getSingleBrowser = async option => {
if (!browserInstance) {
browserInstance && browserInstance.close()
browserInstance = await puppeteer.launch()
}
return browser
}
有一种情况:若同时启动多个爬虫,需要等第一个执行完成后,接下来的任务再一起执行,否则同时执行会启动多个浏览器实例。如下:
async searchHandle() {
await bing('hello world') // 创建了browser instance
duckduckgo('hello world') // 使用上面的browser instance
google('hello world') // 使用上面的browser instance
}
使用Transform Stream掌握爬虫执行进度
若使用promise封装爬虫对象后,想知道爬虫内部执行到哪一步了,可以使用自定义的Transform Stream来统一接收状态信息,在electron中使用还可以与渲染进程同步状态信息。
初始化Stream
// main.js
export const statusStream = new Transform({
// 读写流均开启对象模式
writableObjectMode: true,
readableObjectMode: true,
transform(chunk, encoding, callback) {
callback(null, chunk)
}
})
// 设置编码类型与回调
stream.setEncoding('utf-8')
stream.on('data', chunk => {
handle_func(chunk) // 处理数据
})
// 若在electron中使用,需要在BrowserWindow创建后进行设置
const initStatusPipe = (stream, win) => {
stream.setEncoding('utf-8')
stream.on('data', chunk => {
// 通过ipc发送给渲染进程
win.webContents.send(IPC_RENDERER_SIGNAL.MESSAGE, { message: chunk })
})
}
// 主窗口创建后初始化流
app.on('ready', () => {
let mainWindow = new BrowserWindow(...)
...
initStatusPipe(statusStream, mainWindow)
})
若在electron中使用,可以在渲染进程中监听对应事件
this.$electron.ipcRenderer.on(IPC_RENDERER_SIGNAL.MESSAGE, (e, arg) => {
console.log(arg.message)
})
在爬虫中使用stream
传入之前定义的stream对象,使用write方法将状态信息写入stream中。
// crawler.js
const google = (pipe, option) => {
return new Promise(async(resolve, reject) => {
try {
...
await page.goto(url, {waitUntil: 'domcontentloaded'})
pipe.write(`page: open ${url}`)
...
pipe.write(`page: crwaled ${number} results from google`)
...
await page.close()
pipe.write('page: closed')
// return results
resolve(...)
} catch (err) {
reject(...)
}
})
}
export default google
这样,就可以掌握爬虫内的具体情况了。为了更好的管理爬虫状态,也可以针对情况设计一些传递时的消息格式。
- 原文作者:yrq110
- 原文链接:http://yrq110.me/post/front-end/some-tips-of-using-puppetter/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。