Skip to content

使用指南

配置:

Puppeteer 中的所有默认配置都可以通过配置文件和环境变量两中方式。

配置文件:

配置文件是配置 Puppeteer 的推荐选择,配置文件的格式支持如下:

  • .puppeteerrc.cjs
  • .puppeteerrc.js
  • .puppeteerrc (YAML、JSON)
  • .puppeteerrc.json
  • .puppeteerrc.config.js
  • .puppeteerrc.config.cjs

以下是支持配置的选项:

选项类型描述
browserRevisionstring指定浏览器版本号,默认值为当前 Puppeteer 内置的浏览器版本号
cacheDirectorystring指定 Puppeteer 使用的缓存目录,默认通过 path.join(os.homedir(), '.cache', 'puppeteer') 配置路径
defaultProduct'chrome'、'firefox'指定浏览器产品,默认为 chrome 浏览器
downloadBaseUrlstring指定下载浏览器的前缀地址,不同的浏览器产品对应的下载路径不同:https://storage.googleapis.com/chrome-for-testing-public or https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central
executablePathstring指定 puppeteer.launch 启动路径,默认会自动查找安装路径
experimentsRecord<string, never>指定 Puppeteer 的实验选项
logLevel'silent'、'error'、'warn'指定日志输出级别,默认为 warn 级别
skipChromeDownloadboolean安装 Puppeteer 时跳过 Chrome 下载
skipChromeHeadlessShellDownloadboolean安装 Puppeteer 时跳过 chrome-headless-shell 下载
skipDownloadboolean安装 Puppeteer 时跳过下载
temporaryDirectorystring指定 Puppeteer 使用的临时文件目录,默认通过 os.tmpdir() 配置路径

PS:https://pptr.dev/api/puppeteer.configuration

配置文件使用示例:

javascript
const { join } = require('path');

/**
 * @type {import('puppeteer').Configuration}
 */
module.exports = {
  	// 修改缓存目录后需要重新安装 Puppeteer,以保证新的缓存目录中包含的运行的必要文件
    cacheDirectory: join(__dirname, '.cache', 'puppeteer')
}

环境变量

环境变量配置的等级要高于配置文件,在使用时要特别注意。

以下是支持的环境变量:

环境变量配置文件选项
PUPPETEER_BROWSER_REVISIONbrowserRevision
PUPPETEER_CACHE_DIRcacheDirectory
PUPPETEER_PRODUCTdefaultProduct
PUPPETEER_DOWNLOAD_BASE_URLdownloadBaseUrl
PUPPETEER_EXECUTABLE_PATHexecutablePath
PUPPETEER_SKIP_CHROME_DOWNLOADskipChromeDownload
PUPPETEER_SKIP_CHROME_HEADLESS_SHELL_DOWNLOADskipChromeHeadlessShellDownload
PUPPETEER_SKIP_DOWNLOADskipDownload
PUPPETEER_TMP_DIRtemporaryDirectory

PS:此外还有 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 用于定义下载和运行浏览器的代理设置

调试:

由于 Puppeteer 设计浏览器的许多不同组件,因此没有统一的方式调试所有的可能得问题,Puppeteer 尽可能的提供多种调试方法来涵盖所有可能得问题。

一般来说在使用 Puppeteer 的时候主要的问题来自两个来源:在 Node.js 上运行的代码(称之为服务端代码)和在浏览器端运行的代码(称之为客户端代码)。

基础配置:

因为调试往往发生在开发环境中,所以提供一个环境变量来动态启动调试的基础配置还是有很帮助的:

  1. 禁用无头模式:可以查看浏览器显示的内容,主观的观察内容变化;
  2. 延长执行时间:通过延长执行时间来观察正在发生的情况;
  3. 启用浏览器调试:调试时会自动启动开发者工具;
  4. 打印浏览器日志:启用后可以接管浏览器意外崩溃或无法正常启动时的日志信息。
javascript
import puppeteer from 'puppeteer'

const production = process.env.NODE_ENV === 'production';

(async () => {
    const browser = await puppeteer.launch({
        // 开发环境中不使用无头模式
        headless: production,
        // 开发环境中延长执行时间
        slowMo: production ? 0 : 250,
        // 开发环境中打开开发者工具
        devtools: production ? false : true,
        // 开发环境中输出浏览器进程信息
        dumpio: production ? false : true,
    })
})()

客户端代码调试:

  1. 捕获客户端代码中的 console.* 的输出:

    javascript
    // 监听页面的 console.* 输出 
    page.on('console', msg => {
        console.log('PAGE LOG: ', msg.text())
    })
    
    // 模拟浏览器环境中 console.* 输出
    await page.evaluate(() => {
        console.log('PAGE EVAL: ', 'Hello World!')
    })
  2. 添加 debugger; 关键字中断代码:

    javascript
    // 注意启用 devtools 选项
    await page.evaluate(() => {
      	// 模拟客户端代码中使用 debugger; 关键字中断代码执行
      	debugger;
        console.log('PAGE EVAL: ', 'Hello World!')
    })

服务端代码调试:

在 Node.js 中使用调试器仅限于 Chrome 和 Chromium 中使用。

  1. 在关闭无头模式的前提下,需要在运行服务端代码的脚本中添加 --inspect-brk 选项,如:
shell
npm pkg set scripts.debug="cross-env NODE_ENV=development node --inspect-brk index.mjs" // v7.24.2 +
  1. 在 Chrome 或 Chromium 中打开 chrome://inspect/#devices ,在新页面中的 Remote Target 菜单下找到对应的 Target 并启动调试。
  2. 在新打开的浏览器中,按 F8 可以恢复测试执行;
  3. 添加的 debugger; 关键字也会被命中并中断程序执行;

记录 DevTools 协议流量:

以上的调试方法都不起作用时,则可能是 Puppeteer 和 DevTools 协议之间可能存在着问题,那这时候可以通过设置 DEBUG 环境变量来进一步调试:

javascript
# 基本详细日志记录
cross-env DEBUG="puppeteer:*" node script.js

# 防止截断长消息
cross-env DEBUG="puppeteer:*" env DEBUG_MAX_STRING_LENGTH=null node script.js

# 协议通信可能相当繁杂。此示例过滤掉所有网络域消息
cross-env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network'

# 过滤掉所有协议消息,但保留所有其他日志记录
cross-env DEBUG="puppeteer:*,-puppeteer:protocol:*" node script.js

记录待处理的协议调用:

如果遇到 Puppeteer 异步任务未能变为 Fulfilled 状态时,可以尝试使用 debugInfo 借口记录被挂起的回调,并查看导致的原因:

javascript
console.log(browser.debugInfo.pendingProtocolErrors);

请求拦截:

请求拦截需要调用 await page.setRequestInterception(true) 主动启用,启用后每个请求都将被停止,除非你主动该变该请求为继续、响应或中止状态。

JavaScript
import puppeteer from 'puppeteer';

(async () => {
    const browser = await puppeteer.launch({
        headless: false
    });
    const page = await browser.newPage();
    await page.setViewport({ width: 1920, height: 1080 });
    await page.setRequestInterception(true);
    page.on('request', request => {
        // 判断是否已经处理过请求
        if (request.isInterceptResolutionHandled()) return;
        if (
            request.url().endsWith('.png') ||
            request.url().endsWith('.jpg')
        )
            request.abort(); // 拦截请求
        else request.continue(); // 继续请求
    });
    await page.goto('https://taobao.com');
    await browser.close();
})();

多拦截器和异步执行

默认情况下,如果 request.abort()、request.continue() 或 request.respond() 在已调用其中任一 API 后被调用,会引发 Request is already handled! 异常,这也是在上面案例在一开始要判断请求是否已经被处理过的主要原因。

在同步代码块中使用 request.isInterceptResolutionHandled() API 是绝对安全的,下面的示例演示了两个同步拦截程序一起工作:

JavaScript
/*
	请求将首先进入此拦截函数,并正常通过 if 判断(因为请求还没有被处理过),最终由执行 request.continue() 允许请求继续执行 
*/
page.on('request', request => {
  if (request.isInterceptResolutionHandled()) return;
  request.continue();
});

/*
	请求进入此拦截函数之前已经被上一个拦截函数处理,所以并不会通过 if 判断,此处就体现了同步代码块中 API 调用绝对安全的特性
*/
page.on('request', request => {
  if (request.isInterceptResolutionHandled()) return;
  request.abort();
});

下面通过一个异步执行的示例演示多拦截器的协同工作:

JavaScript
page.on('request', request => {
   	// 请求依然是首先进入此拦截函数,并正常通过 if 判断(因为请求还没有被处理过)
    if (request.isInterceptResolutionHandled()) return;

    // 返回 Promise 虽然不是必须的,但这样可以允许 Puppeteer 等待此函数处理
    return new Promise(resolve => {
        // 请求将在 500 毫秒后被允许继续执行
        setTimeout(() => {
            // 因为请求可能在 500 毫秒内被另一个拦截器处理,所以需要再次检查
            if (request.isInterceptResolutionHandled()) {
                resolve();
                return;
            }
            request.continue();
            resolve();
        }, 500);
    });
});
page.on('request', async request => {
    // 由于上个拦截器存在 500 毫秒的延迟处理,所以请求很有可能在没有被处理过就进入了此拦截器,所以请求可能会通过 if 判断
    if (request.isInterceptResolutionHandled()) return;

  	// 这里可以是一个较长时间的异步处理函数
    await someLongAsyncOperation();

  	// 在经过一个较长时间的异步处理函数执行到此行代码是,请求又可能被第一个拦截器处理过了,所以需要再次检查
    if (request.isInterceptResolutionHandled()) return;
    request.continue();
});

下面的示例是使用 request.interceptResolutionState() API 重写多拦截器异步协同工作的重写:

JavaScript
page.on('request', request => {
    const { action } = request.interceptResolutionState();
    if (action === InterceptResolutionAction.AlreadyHandled) return;

    return new Promise(resolve => {
        setTimeout(() => {
            const { action } = request.interceptResolutionState();
            if (action === InterceptResolutionAction.AlreadyHandled) {
                resolve();
                return;
            }
            request.continue();
            resolve();
        }, 500);
    });
});

page.on('request', async request => {
    if (
        request.interceptResolutionState().action ===
        InterceptResolutionAction.AlreadyHandled
    )
        return;

    await someLongAsyncOperation();

    if (
        request.interceptResolutionState().action ===
        InterceptResolutionAction.AlreadyHandled
    )
        return;
    request.continue();
});

协作拦截模式

request.abort、request.continue 和 request.respond 可以接受一个可选的 priority 值在协作拦截模式下工作。当所有处理程序都使用协作拦截模式时,Puppeteer 保证所有拦截处理程序将按注册顺序运行并等待。拦截被解析为最高优先级。以下是协作拦截模式的规则:

  1. 所有处理程序都必须提供优先级(priority)数值;
  2. 如果为提供优先级数值,则”传统模式“处于活动状态,而”协作拦截模式“处于非活动状态;
  3. 异步处理程序会在最终处理程序截获之前完成;
  4. 最高优先级的处理函数会被执行,但遇到优先级相同时,将按 abort > respond > continue 顺序执行;

在指定协作拦截模式时,除非要设置更高的优先级,否则请使用 0 或 HTTPRequest.DEFAULT_INTERCEPT_RESOLUTION_PRIORITY

下面的示例演示了传统模式占据最高优先级,请求会立即中止,因为在解析拦截器时只有有一个处理程序省略了 priority

javascript
page.on('request', request => {
  if (request.isInterceptResolutionHandled()) return;
  request.continue({}, 0);
});

page.on('request', request => {
  if (request.isInterceptResolutionHandled()) return;

  // 传统模式:立即中止
  request.abort('failed');
});

PS:这个案例将在控制台收到类似 Error: net::ERR_FAILED at https://taobao.com 的异常信息,因为请求全部被中止掉了,更多的优先级示例见 https://pptr.dev/guides/network-interception#cooperative-request-continuation

无头模式:

默认情况下,Puppeteer 会以 Headless 模式启用浏览器。

javascript
const browser = await puppeteer.launch();
// 相当于
const browser = await puppeteer.launch({headless: true});

在 v22 版本之前,Puppeteer 默认启用旧的 Headless 模式(chrome-headless-shell 独立二进制文件)。chrome-headless-shell 于通常的 Chrome 的行为不完全匹配,但目前对于不需要完整 Chrome 功能集的自动化任务来说,性能会更有优势。如果对性能有更高的要求可以切换到 chrome-headless-shell

javascript
const browser = await puppeteer.launch({headless: 'shell'});

要启动 Chrome 的 headful 版本,可以进行 headless 选项:

javascript
const browser = await puppeteer.launch({headless: false});

截图:

要捕获屏幕截图可以使用:

javascript
import puppeteer from 'puppeteer'

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto('https://developer.mozilla.org/zh-CN/', {
      	// Waits till there are no more than 2 network connections for at least `500` ms.
        waitUntil: 'networkidle2'
    })
    await page.screenshot({
        path: 'screenshot.png',
    });
    await browser.close();
})()

要捕获特定元素的截图可以使用:

javascript
const element = await page.waitForSelector('div');
await element.screenshot({
  path: 'screenshot.png',
});

默认情况下,如果元素处于 hidden 状态,ElementHandle.screenshot() 会尝试将其滚动到视图中。

PDF 生成:

要打印 PDF 可以使用 page.pdf() 方法,默认情况下这个方法会等待字体文件的加载。

JavaScript
import puppeteer from 'puppeteer'

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto('https://developer.mozilla.org/zh-CN/', {
        waitUntil: 'networkidle2'
    })
    await page.pdf({
        path: 'mozilla.pdf',
    });
    await browser.close();
})()

Chrome 扩展:

Puppeteer 可以用于测试 Chrome 扩展程序,但需要注意的是 headless: 'shell' 模式中不可用。

首先准备一个仅包含 service_worker 的后台脚本,并配置好 manifest.json

json
{
    "name": "Hello World",
    "version": "0.1",
    "manifest_version": 3,
    "background": {
        "service_worker": "background.js"
    }
}
JavaScript
// background.js
console.log("background.js loaded");

将插件放到项目目录的 my-extension 文件夹中,接着通过配置 args 选项,加载插件:

javascript
import puppeteer from 'puppeteer'
import path from 'path'
import process from 'process'

const extensiondir = path.join(process.cwd(), 'my-extension');

(async () => {
    const browser = await puppeteer.launch({
        args: [
            `--disable-extensions-except=${extensiondir}`,
            `--load-extension=${extensiondir}`,
        ],
    })

    await browser.close();
})()

最后通过 evaluate() 函数在后台脚本中通过 chrome.runtime.getManifest() 获取插件的版本信息:

javascript
const workerTarget = await browser.waitForTarget(
    target => target.type() === 'service_worker' && target.url().endsWith('background.js')
);
const worker = await workerTarget.worker();
const version = await worker.evaluate(() => chrome.runtime.getManifest().version);
console.log(version);

PS:Puppeteer 文档显示目前尚无法测试扩展程序的内容脚本。

Cookies:

Puppeteer 提供了设置 Cookie 的函数 await page.setCookie({}) 和提取页面所设置的 Cookie 的函数 await page.cookies()

文件:

Puppeteer 不提供以编程方式处理文件下载的方法,要上传文件,需要找到一个文件输入元素并调用 ElementHandle.uploadFile('./local-file')

在浏览器中运行:

在Chrome扩展中运行: