总结一些使用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'})
...

执行优化与状态管理

使用单例浏览器实例

在同一个程序中使用多个爬虫对象时,某些情况下可以选择复用同一个浏览器实例,而不用每启动一个爬虫都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

这样,就可以掌握爬虫内的具体情况了。为了更好的管理爬虫状态,也可以针对情况设计一些传递时的消息格式。