Puppeteer:从零出发,全面掌握浏览器自动化神器

我是小鑫同学,在北京工作的一位前端开发工程师。我擅长使用 Vue.jsAngularTypescriptNode.js 构建 Web 应用程序和网站。同时我也是一位乐于分享的程序员,我经常利用休息时间写写技术文章、分享自己经验及学习心得。

近两年主要利用 MicroApp 微前端框架维护和迭代公司的项目,保证历史的 Angular 工程逐步向 Vue.js 的平稳迁移。

座右铭: 😇 所有付出都将是沉淀,所有美好终会如期而至

框架介绍

Puppeteer 译为木偶,是一个 Node.js 库,内部通过 DevTools 协议提供控制 Chrome 或 Firefox 的一系列 API。通过定义可以看出 Puppeteer 的核心在于提供用户控制浏览器行为的方法,以下是一些自动化入门示例:

  • 自动提交表单、UI 测试、键盘输入等;
  • 使用最新的 JavaScript 和 浏览器特性创建自动化环境;
  • 捕获网站的时间线跟踪,帮助诊断性能问题;
  • 测试 Chrome 扩展程序;
  • 对页面截图和生成 PDF;
  • 对 SPA 应用爬取并生成预渲染内容;

安装指引

Puppeteer 从 v1.7.0+ 开始同时提供 puppeteerpuppeteer-core 两个包:

  • puppeteer 是在 puppeteer-core 基础上提供了更加完整的浏览器自动化产品:

    • 安装期间会下载与 Puppeteer 版本匹配的 Chrome for Testing;
    • puppeteer@v21.6.0+ 会同时下载 chrome-headless-shell 二进制文件;
    • 默认安装位置:$HOME/.cache/puppeteer
    • 提供合理的默认选项;
  • puppeteer-core 是通过 DevTools 协议提供编程接口驱动的核心库:

    • 安装期间不会下载 Chrome for Testing 及 chrome-headless-shell
    • 不提供任何默认选项;
shell 复制代码
npm i puppeteer # 完整版
npm i puppeteer-core # 核心库,需要显示指定远程/本地浏览器的连接地址

入门示例:

先快速初始化一个示例项目:

shell 复制代码
mkdir caniuse-puppeteer && cd caniuse-puppeteer
npm init -y && npm i puppeteer
echo 'console.log("Hello World!");' > index.mjs
npm pkg set type="module" 
npm pkg set scripts.dev="node index.mjs" 

PS:在使用 npm pkg set 时要注意 npm 的版本。

在示例中我尝试模拟用户在 caniuse.com 检索 Flexible 关键词,并打印出的第一条信息的描述内容:

javascript 复制代码
import puppeteer from 'puppeteer';

(async () => {
  	// ① 启动浏览器并打开一个新的页签
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.setViewport({ width: 1920, height: 1080 })
  	// ② 跳转到 https://caniuse.com/ 地址
    await page.goto('https://caniuse.com/')
		
  	// ③ 通过 locator 定位 input 元素,并输入 Flexible 关键词
    await page.locator('input[name="search"]').fill('Flexible')
  	// ④ 等待查询结果对应的元素出现
    await page.waitForSelector('ciu-feature-list >>> .section__tables .feature-list-wrap ciu-feature >>> header div p')

  	// ⑤ 利用 evaluate 方法到目标页面的上下文获取第一条描述内容
    const textContent = await page.evaluate(() => {
        const element = document.querySelector("#main > main > ciu-feature-list")
            .shadowRoot.querySelector("#flexbox")
            .shadowRoot.querySelector("header > div > p")
        return element.textContent.trim()
    })
    console.log(textContent)
  
		// ⑥ 关闭浏览器
    await browser.close()
})()

PS:>>> 是 Puppeteer 提供的查询 Shadow DOM 元素的组合器。

运行环境:

  • Node 18+,Puppeteer 遵循 Node 最新维护的 LTS 版本;
  • 如果要使用 TypeScript,需要安装 TypeScript 4.7.4 +;
  • 可以在 Windows(x64)、MacOS(x64/arm64)、Debian/Ubuntu Linux(x64);

核心概念

Puppeteer 拥有 4 个核心概念,分别是:

核心概念 描述
浏览器管理 Puppeteer 提供了启动、关闭和连接已启动的浏览器等主要功能。
JavaScript 执行 Puppeteer 在其驱动的页面上下文中执行 JavaScript 函数。
网络日志 Puppeteer 默认监听所有的网络请求和响应,并在 page 上派发对应的事件
页面交互 Puppeteer 允许使用鼠标、触摸事件和键盘输入与页面元素交互,通常应首先使用 CSS 选择器查询 DOM 元素。

浏览器管理:

入门示例中已经使用过了启动和关闭浏览器的 API,这里主要了解一下浏览器上下文(包含权限)和如何连接到正在运行的浏览器两部分。

  1. 浏览器上下文及上下文权限:

    • 浏览器上下文的作用是隔离自动换任务,保证 Cookie 和本地存储不会在浏览器上下文之间共享;
    • 浏览器上下文所关联的页面会在关闭上下文时一同被关闭;
    • 浏览器上下文支持权限配置,例如在首次访问高德地图需要提供 geolocation 权限;

    获取和创建浏览器上下文 API:

    javascript 复制代码
     // 获取默认的浏览器上下文
     await browser.defaultBrowserContext()
     
     // 创建一个新的浏览器上下文
     await browser.createBrowserContext()

    为默认浏览器上下文重写访问高德地图的定位权限:

    javascript 复制代码
    import puppeteer from 'puppeteer';
    
    (async () => {
        const browser = await puppeteer.launch({
            headless: false,
        })
        const context = await browser.defaultBrowserContext()
    
        const url = 'https://ditu.amap.com/'
        await context.overridePermissions(url, ['geolocation'])
    
        const page = await context.newPage()
        await page.goto(url)
    })()
  2. 如何连接到正在运行的浏览器:

    • 除了入门示例是用到的启动浏览器的方式外,还可以使用 connect 直接连接到已启动的浏览器。

    使用 launch 启动浏览器:

    javascript 复制代码
    import puppeteer from 'puppeteer';
    
    (async () => {
        const browser = await puppeteer.launch({
            headless: false,
            // 设置远程调试端口
            args: ['--remote-debugging-port=9222']
        })
        const page = await browser.newPage()
        // /json/version 获取 webSocketDebuggerUrl
        await page.goto('http://localhost:9222/json/version')
    })()

    连接上一个浏览器并打印 caniuse.com 网站的 news 元素:

    JavaScript 复制代码
    import puppeteer from 'puppeteer';
    
    (async () => {
        const browser = await puppeteer.connect({
            browserWSEndpoint: "ws://localhost:9222/devtools/browser/5bce4998-1ba5-4f96-b512-d0fb8d49187d"
        })
        const page = await browser.newPage()
        await page.goto('https://caniuse.com/')
        const textContext = await page.locator('.news').map(el => el.textContent).wait()
        console.log(textContext)
    
        // 断开连接并不会关闭浏览器
        await browser.disconnect()
    })()

JavaScript 执行:

在 Puppeteer 驱动的页面上下文中执行 JavaScript 函数同样在入门示例中有过使用,但没有提到如何传递参数和其中的一个缺陷。

传参:evaluate 第二个参数支持传递一个 ElementHandle 对象:

javascript 复制代码
import puppeteer from 'puppeteer';

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto('https://caniuse.com/')

    const handle = await page.locator('.news').waitHandle()
    const textContent = await page.evaluate(el => el.textContent, handle)
    console.log(textContent)

    await browser.close()
})()

缺陷:上面示例中 textContent 被成功的输出,说明 el 是个有效的对象,但如果直接返回 el 对象,你会看到不一样的结果,终端输出了 {} 。造成这个现象的原因是 Puppeteer 会将对象序列化导致得到了不正确的结果,为了处理返回的对象,Puppeteer 提供了通过引用返回对象的方法:

javascript 复制代码
import puppeteer from 'puppeteer';

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto('https://caniuse.com/')

    const handle = await page.locator('.news').waitHandle()
    const element = await page.evaluateHandle(el => el, handle)
    console.log(element instanceof ElementHandle)

    await browser.close()
})()

PS:在实际使用时要格外注意,多加测试。

网络日志:

page 提供了一个 on(event, handler) 函数,允许对 Puppeteer 派发的事件进行监听。

javascript 复制代码
import puppeteer from 'puppeteer';

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto('https://caniuse.com/')

    page.on('request', request => {
        console.log('request : ', request.url())
    })

    page.on('response', response => {
        console.log('response : ', response.url())
    })
})()

页面交互:

前面的示例中或多或少都使用到了Puppeteer 提供与页面交互的 API,页面交互也是 Puppeteer 核心概念中内容最多的一块,所以放到这个小节的最后来讲。

定位器:

Puppeteer 推荐使用定位器 API 选择元素并与之交互,定位器 API 会等待元素在 DOM 中处于可操作的正确状态。但是如果定位器 API 无法满足时仍可以使用低级别的 API,如:page.waitForSelector()ElementHandle

普通操作:

操作类型 API 示例 默认检查项目
点击元素 await page.locator('button').click(); 1 确保元素位于视口中 2 等待元素可见或隐藏 3 等待元素启用 4 等待元素在两个连续的动画帧上具有稳定边界框
录入文本 await page.locator('input').fill('hello world'); 1 确保元素位于视口中 2 等待元素可见或隐藏 3 等待元素启用 4 等待元素在两个连续的动画帧上具有稳定边界框
鼠标悬停 await page.locator('div').hover(); 1 确保元素位于视口中 2 等待元素可见或隐藏 3 等待元素在两个连续的动画帧上具有稳定边界框
滚动元素 await page.locator('div').scroll({ scrollTop: 10, scrollLeft: 20 }); 1 确保元素位于视口中 2 等待元素可见或隐藏 3 等待元素在两个连续的动画帧上具有稳定边界框
等待元素可见 await page.locator('.loading').wait(); 等待元素可见或隐藏

配置自检项:

javascript 复制代码
await page.locator('button')
  .setEnsureElementIsInTheViewport(false) // 禁用后无法保证操作前元素位于视口中
  .setVisibility(null)                    // 设置忽略操作前检查元素可见或隐藏状态
  .setWaitForEnabled(false)               // 禁用后无法保证操作前元素可用
  .setWaitForStableBoundingBox(false)     // 禁用后将不等待元素在两个连续动画帧上具有稳定边界框
  .click();

配置超时时间:

javascript 复制代码
await page.locator('button').setTimeout(5 * 1000).click();

PS:由于网页的响应速度存在差异,默认的超时时间不满足需要的情况下,可使用 setTimeout() 函数适当延长,超时时将抛出 TimeoutError 异常。

获取元素值或 ElementHandle

javascript 复制代码
// 使用 map 函数将元素映射为 JavaScript 值,调用 wait() 将返回序列化的 JavaScript 值
const enabled = await page.locator('button').map(el => !el.disabled).wait();

// 调用 waitHandle() 函数返回 ElementHandle
const buttonHandle = await page.locator('button').waitHandle();
await buttonHandle.click();

添加过滤器:

javascript 复制代码
await page.locator('button')
	.filter(el = el.innerText().includes('Click Me'))
  .click();

PS:通过过滤器来匹配所有按钮元素中符合特定文本的按钮元素。

添加事件监听:

javascript 复制代码
await page.locator('button')
  .on(LocatorEvent.Action, () => {
      console.log('clicked');
  }).click();

PS:��前定位器仅支持一个单独的事件,事件会在定位器准备执行动作前触发,以此表示所有前提条件已经得到满足。

自定义等待函数:

javascript 复制代码
await page.locator(() => {
    let resolve;
    const promise = new Promise((res) => {
        return (resolve = res)
    });
    const observer = new MutationObserver((records) => {
        for (const record of records) {
            if (record.target instanceof HTMLCanvasElement) {
                resolve(record.target);
            }
        }
    });
    observer.observe(document);
    return promise;
}).wait()

PS:示例中借助 MutationObserver 订阅 document 中出现 HTMLCanvasElement 元素的功能。

等待选择器:

等待选择器(waitForSelector)与定位器相比是一个较低级别的 API,允许等待元素在 DOM 中可用。如果操作失败不具备重试特性,且需要手动释放生成 ElementHandle 以防止内存泄漏。

javascript 复制代码
import pprt from "puppeteer"

(async () => {
    const browser = await pprt.launch()
    const page = await browser.newPage()
    await page.goto("URL_ADDRESS")

    // 使用 waitForSelector 查询句柄
    const element = await page.waitForSelector("div > .class-name")
    await element.click();

    // 注意释放资源
    await element.dispose();

    await browser.close();
})()

立即选择器:

在明确已知元素位于页面上时,可以直接使用立即选择器。

API 描述
page.$() 返回与选择器匹配的单个元素
page.$$() 返回与选择器匹配的多个元素
page.$eval() 返回与选择器匹配的第一个元素上运行 JavaScript 函数的结果
page.$$eval() 返回与选择器匹配的每一个元素上运行 JavaScript 函数的结果

扩展选择器:

XPath 选择器(-p-path):

javascript 复制代码
import pptr from 'puppeteer'
      
(async () => {
    const browser = await pptr.launch({ headless: false })
    const page = await browser.newPage()
    await page.setViewport({ width: 1080, height: 1024 })
    await page.goto('https://developer.mozilla.org/zh-CN/')
      
  	// XPath 选择器
    const textContent = await page.locator('::-p-xpath((//*[@class="tile-container"]/div/h3/a)[1])')
      .map(el => el.textContent)
      .wait()
    console.log(textContent)
      
    await page.screenshot({ path: 'screenshot.png' })
    await browser.close()
})()

Text 选择器(-p-text):

javascript 复制代码
import pptr from 'puppeteer'
      
(async () => {
    const browser = await pptr.launch({ headless: false })
    const page = await browser.newPage()
    await page.setViewport({ width: 1080, height: 1024 })
    await page.goto('https://developer.mozilla.org/zh-CN/')
      		
  	// 文本选择器
    const textContent = await page.locator('::-p-text(Developer essentials: JavaScript console methods)')
    	.map(el => el.textContent)
    	.wait()
		console.log(textContent)
      
    await page.screenshot({ path: 'screenshot.png' })
    await browser.close()
})()

ARIA 选择器(-p-aria):

javascript 复制代码
import pptr from 'puppeteer'
      
(async () => {
    const browser = await pptr.launch({ headless: false })
    const page = await browser.newPage()
    await page.setViewport({ width: 1080, height: 1024 })
    await page.goto('https://developer.mozilla.org/zh-CN/')
      
  	// 无障碍属性选择器
    const innerHTML = await page.locator('::-p-aria(MDN homepage)')
      .map(el => el.innerHTML)
      .wait()
    console.log(innerHTML)
      
    await page.screenshot({ path: 'screenshot.png' })
    await browser.close()
})()

Pierce 选择器(pierce/):

javascript 复制代码
import pptr from 'puppeteer'
      
(async () => {
    const browser = await pptr.launch({ headless: false })
    const page = await browser.newPage()
    await page.setViewport({ width: 1080, height: 1024 })
    await page.goto('https://mdn.github.io/web-components-examples/composed-composed-path/')
        
  	// 穿透 shadow DOM 选择器,深度组合器,同 pierce/p
    const textContent = await page.locator('& >>> p')
    	.map(el => el.textContent)
    	.wait()
    console.log(textContent)
      
    await page.screenshot({ path: 'screenshot.png' })
    await browser.close()
})()

自定义选择器(如 -p-class):

JavaScript 复制代码
import puppeteer, { Puppeteer } from "puppeteer"
      
(async () => {
    // 注册 class 选择器 
    Puppeteer.registerCustomQueryHandler('class', {
        queryOne: (node, selector) => {
            return node.querySelector(`.${selector}`)
        },
        queryAll: (node, selector) => {
            return [...node.querySelectorAll(`.${selector}`)]
        }
    })
      
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto("https://developer.mozilla.org/zh-CN/")
      
    // 使用 class 选择器
    const innerHTML = await page.locator('::-p-class(tile-title)').map(el => el.innerText).wait()
    console.log(innerHTML)
      
    await browser.close()
})()

配置说明

Puppeteer 支持通过配置文件和环境变量两种方式来改变默认配置项,且环境变量的优先级要高于配置文件。配置文件支持的格式如下:

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

配置文件使用示例:

JavaScript 复制代码
const { join } = require('path');

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

配置选项及对应的环境变量:

配置项 类型 环境变量 描述
browserRevision string PUPPETEER_BROWSER_REVISION 指定浏览器版本号,默认值为当前 Puppeteer 内置的浏览器版本号
cacheDirectory string PUPPETEER_CACHE_DIR 指定 Puppeteer 使用的缓存目录,默认通过 path.join(os.homedir(), '.cache', 'puppeteer') 配置路径
defaultProduct chrome、firefox PUPPETEER_PRODUCT 指定浏览器产品,默认为 chrome 浏览器
downloadBaseUrl string PUPPETEER_DOWNLOAD_BASE_URL 指定下载浏览器的前缀地址,不同的浏览器产品对应的下载路径不同:storage.googleapis.com/chrome-for-... or archive.mozilla.org/pub/firefox...
executablePath string PUPPETEER_EXECUTABLE_PATH 指定 puppeteer.launch 启动路径,默认会自动查找安装路径
experiments Record<string, never> -- 指定 Puppeteer 的实验选项
logLevel silent、error、warn -- 指定日志输出级别,默认为 warn 级别
skipChromeDownload boolean PUPPETEER_SKIP_CHROME_DOWNLOAD 安装 Puppeteer 时跳过 Chrome 下载
skipChromeHeadlessShellDownload boolean PUPPETEER_SKIP_CHROME_HEADLESS_SHELL_DOWNLOAD 安装 Puppeteer 时跳过 chrome-headless-shell 下载
skipDownload boolean PUPPETEER_SKIP_DOWNLOAD 安装 Puppeteer 时跳过下载
temporaryDirectory string PUPPETEER_TMP_DIR 指定 Puppeteer 使用的临时文件目录,默认通过 os.tmpdir() 配置路径

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 环境变量来进一步调试:

shell 复制代码
# 基本详细日志记录
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) 主动启用请求拦截,启用后每个请求都将被停止,除非主动将请求切换为继续、响应或中止状态。

传统模式

示例中访问了 taobao 主页,并启用的请求拦截,当请求 url 包含 .png.jpg 后缀时,请求将被中止:

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();
})();

PS:在处理拦截到的请求前要调用 isInterceptResolutionHandled()interceptResolutionState() 检查请求的状态,处理过的请求被再次处理会引发Request is already handled! 异常。

协作拦截模式

协作拦截主要在存在多个请求拦截处理的时候通过给 request.abortrequest.continuerequest.respond 设置可选的 priority 来调控它们的处理顺序。

协作拦截模式规则:

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

PS:在指定协作拦截模式时,除非要设置更高的优先级,否则请使用 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 的异常信息,因为请求全部被中止掉了,更多的优先级示例见 pptr.dev/guides/netw...

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 文档显示目前尚无法测试扩展程序的内容脚本。

更多功能

屏幕截图:

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

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();
})()

Cookies:

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

文件上传:

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

总结

综上所述,Puppeteer 作为一款功能全面的浏览器自动化工具,为网页抓取、自动化测试和浏览器操作提供了坚实基础。无论是自动填写表单、捕获性能数据,还是生成页面截图和PDF,Puppeteer 都以其丰富的API和强大的控制能力,助力开发者实现自动化需求。掌握Puppeteer,意味着解锁了网页自动化世界的无限可能,为你的开发工作带来更高效率和更多创新。希望本文能成为你驾驭Puppeteer的起点,开启自动化之旅的精彩篇章。

PS:本文内容及示例代码部分直接来自『Puppeteer』文档,也是对阅读文档几天下来的一份总结,后续有时间会补充 Puppeteer 在 Chrome 或 Chrome Extension 运行的案例。

相关推荐
百万蹄蹄向前冲32 分钟前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5811 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路1 小时前
GeoTools 读取影像元数据
前端
ssshooter2 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry2 小时前
Jetpack Compose 中的状态
前端
dae bal3 小时前
关于RSA和AES加密
前端·vue.js
柳杉3 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog4 小时前
低端设备加载webp ANR
前端·算法
LKAI.4 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
刺客-Andy4 小时前
React 第七十节 Router中matchRoutes的使用详解及注意事项
前端·javascript·react.js