Puppeteer 快速上手:从安装到编写第一个浏览器自动化脚本
Puppeteer 是面向 Chrome 与 Firefox 的官方 JavaScript 自动化 API,通过同一套异步代码即可完成"启动/连接浏览器 → 创建页面 → 模拟用户操作 → 读取结果"的完整流程。本文以仓库内 docs/guides/getting-started.md 为骨架,逐步拆解官方首个示例脚本的每一行,并结合 packages/puppeteer-core 的源码实现,说明 launch/newPage/goto、Locator 与 ::-p- 查询引擎等核心机制的底层原理,读完即可独立编写可运行、可复用的浏览器自动化脚本。
环境准备:安装包与浏览器的关系
本仓库并非单一 npm 包,而是同时产出 puppeteer 与 puppeteer-core 两个包的 monorepo。两者在"谁来负责浏览器"这件事上分工明确(细节见 docs/guides/installation.md):
puppeteer是一个产品:安装后会自动下载与之匹配的 Chrome for Testing 与chrome-headless-shell,开箱即用,适合大多数自动化任务。puppeteer-core是一个纯库:只负责通过 DevTools 协议驱动"任何已经存在的浏览器",安装时不会下载任何浏览器;它适合连接远程浏览器或自己管理浏览器生命周期的场景。
安装命令(项目内选择 puppeteer):
npm i puppeteer
安装过程中浏览器默认被下载到 $HOME/.cache/puppeteer。仓库根目录的 puppeteer.config.js 展示了如何通过 Configuration 控制下载行为——默认对 chrome、chrome-headless-shell、firefox 三个目标均设置了 skipDownload: false,即默认都会参与下载。更完整的配置项与环境变量说明可参考 docs/guides/configuration.md。
注意:如果你的包管理器(npm RFC 新策略、pnpm、Yarn Berry、Bun、Deno 等)默认拦截依赖的 postinstall 脚本,自动下载会被跳过,运行时会报
Could not find Chrome (ver. ...)。此时可在安装完成后手动执行npx puppeteer browsers install,或在package.json中用allowScripts(npm 为例)为puppeteer显式放行脚本。
若选择 puppeteer-core,导入语句需同步修改,且 launch 时必须显式传入 executablePath(或 channel,当浏览器安装在标准位置时):
import puppeteer from 'puppeteer-core';
你的第一个脚本:搜索并抓取文章标题
官方快速上手示例展示了一个完整闭环:打开 Chrome 开发者博客首页 → 用键盘打开站内搜索 → 按可访问名称输入关键词 → 点击第一条结果 → 读取文章完整标题。在浏览器端它完整对应了 page.goto、page.setViewport、keyboard、page.locator、JSHandle.evaluate 等一系列 API。
下面先创建脚本文件(如 search-blog.ts 或转译为 JS 运行),完整代码如下:
import puppeteer from 'puppeteer';
// 使用 puppeteer-core 时改为:
// import puppeteer from 'puppeteer-core';
// 启动浏览器并打开一个新的空白页面。
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 导航页面到目标 URL。
await page.goto('https://developer.chrome.com/');
// 设置视口尺寸。
await page.setViewport({width: 1080, height: 1024});
// 用键盘打开搜索菜单。
await page.keyboard.press('/');
// 依据可访问的输入框名称进行输入。
await page.locator('::-p-aria(Search)').fill('automate beyond recorder');
// 等待并点击第一条搜索结果。
await page.locator('.devsite-result-item-link').click();
// 用唯一字符串定位完整标题。
const textSelector = await page
.locator('::-p-text(Customize and automate)')
.waitHandle();
const fullTitle = await textSelector?.evaluate(el => el.textContent);
// 打印完整标题。
console.log('The title of this blog post is "%s".', fullTitle);
await browser.close();
这段代码体现了 Puppeteer 与其它浏览器测试框架一致的思维模型:先有 Browser,再从 Browser 派生出 Page,之后所有操作都在 Page 维度上进行。仓库中 browser.newPage()、browser.close() 的行为可参考 puppeteer.browser.newpage.md 与 puppeteer.browser.close.md。
下面把每一段拆开讲透。
启动浏览器:launch 与连接两种入口
const browser = await puppeteer.launch();
const page = await browser.newPage();
launch 的实现位于 packages/puppeteer-core/src/node/PuppeteerNode.ts 及其底层的 ChromeLauncher/FirefoxLauncher(见 node/ChromeLauncher.ts 与 node/BrowserLauncher.ts)。它的职责是:定位可执行文件 → 以子进程方式拉起浏览器 → 建立 CDP 连接 → 返回 Browser 实例。不传任何参数时,Puppeteer 会使用随 puppeteer 包一起下载、并被验证过兼容性的浏览器版本。
如果浏览器不在默认位置,或你使用了 puppeteer-core,则必须传入 executablePath/channel:
const browser = await puppeteer.launch({
channel: 'chrome', // 使用系统标准位置安装的 Chrome
// executablePath: '/usr/bin/google-chrome', // 或直接指定二进制路径
headless: true, // 默认即无头模式
});
launch 的完整参数与语义见 puppeteer.puppeteernode.launch.md 与 puppeteer.launchoptions.md。
除了启动自有进程,还可以通过 puppeteer.connect 连接一个已经运行的远程浏览器(例如在 CI 中由独立进程管理的浏览器,或 Docker 中的调试端口),返回的同样是 Browser 实例,后续 API 完全一致:
const browser = await puppeteer.connect({browserWSEndpoint: 'ws://...'});
关于两种入口的取舍,仓库内 examples/cross-browser.js 演示了如何针对不同浏览器分别执行任务,examples/webdriver-bidi.mjs 则展示了基于 WebDriver BiDi 协议的新一代连接方式。
导航与视口:goto 与 setViewport
await page.goto('https://developer.chrome.com/');
await page.setViewport({width: 1080, height: 1024});
page.goto(url)负责页面级导航并等待加载完成,返回 HTTPResponse;它是 page.goto 在 Page 上的封装,深层语义对应Frame.goto(见 puppeteer.frame.goto.md),并支持waitUntil、timeout、referer等选项(puppeteer.gotooptions.md)。page.setViewport({width, height})指定页面渲染的视口尺寸。示例中特意设成 1080×1024,是为了保证后面通过键盘按下/弹出的搜索框、以及搜索结果列表在当前视口内可见可点,这属于可访问性自动化的常见调优手段。
导航阶段的等待策略、超时与重试细节可进一步参考 docs/guides/page-interactions.md 与 puppeteer.waitforoptions.md。
键盘驱动的 UI 进入:keyboard.press
await page.keyboard.press('/');
很多站点(尤其是文档站)注册了 / 快捷键来聚焦搜索框。Keyboard.press 组合了"按下→触发→抬起"的完整键击序列,其完整能力(含 key、text、修饰键组合等)见 puppeteer.keyboard.md。这里它替代了"用鼠标找到搜索图标再点击"这类脆弱步骤——因为快捷键在页面任何位置都生效,为后续的 Locator 查询提供了稳定前提。
定位元素:Locator 与 ::-p- 查询引擎
await page.locator('::-p-aria(Search)').fill('automate beyond recorder');
await page.locator('.devsite-result-item-link').click();
const textSelector = await page
.locator('::-p-text(Customize and automate)')
.waitHandle();
const fullTitle = await textSelector?.evaluate(el => el.textContent);
这是示例中最值得深入的部分。page.locator(selector) 返回 Locator(实现位于 packages/puppeteer-core/src/api/locators/locators.ts),它把"查找元素 + 等待可交互 + 执行动作 + 失败重试"等自动化中最容易踩坑的逻辑收敛为一个可链式调用的对象。示例中按动作顺序使用了三种:
::-p-aria(Search):按可访问名称(ARIA name)定位,是表单输入框这类"没有稳定文本、CSS 又易变"的元素的首选方案。对应实现见 injected/ARIAQuerySelector.ts。.devsite-result-item-link:普通 CSS 选择器,直接透传给页面自身的querySelectorAll,用于点击结构稳定的第一条结果。::-p-text(Customize and automate):按可见文本内容定位标题所在的文本节点,配合.waitHandle()等待元素出现并返回可执行evaluate的句柄。文本匹配实现见 injected/TextQuerySelector.ts。
这些 ::-p- 前缀选择器并不是浏览器原生语法,而是由 Puppeteer 注入到页面里的 PSelector 查询引擎负责解析执行的。核心分发逻辑在 injected/PQuerySelector.ts 中:引擎逐段消费复合选择器,遇到 text/xpath/aria 分别路由到对应的查询器,遇到自定义名字则查 customQuerySelectors 表并抛出 Unknown selector type;它还支持两组深组合子——>>>(后代穿透,可跨 open shadow root)与 >>>>(直接子节点穿透)。这意味着 ::-p- 查询天然具备穿透 Shadow DOM 的能力,这是普通 CSS 选择器做不到的。
示例中 textSelector?.evaluate(el => el.textContent) 是典型的"拿到句柄后在页面上下文里执行函数"模式:evaluate 的参数会被序列化到浏览器端执行,返回 Promise 后再回传 Node 端(可参考 JSHandle.evaluate 与 puppeteer.evaluatefunc.md)。
Locator 的等待与点击行为可配置:例如 .setTimeout() 控制超时、click 前默认确保元素在视口内且稳定。更多动作(fill/click/hover/wait 及选项)见 puppeteer.locator.md、puppeteer.locator.fill.md 与 puppeteer.locatorclickoptions.md。
收尾:打印结果与释放浏览器
console.log('The title of this blog post is "%s".', fullTitle);
await browser.close();
browser.close() 会关闭浏览器进程并断开连接,是脚本必须的收尾动作,否则 Node 进程可能因残留的子进程而无法退出。对于更长期的自动化任务,browser.disconnect()(断开但保留浏览器继续运行)则对应另一类场景,详见 puppeteer.browser.close.md 与 puppeteer.browser.disconnect.md。
运行脚本
保存上述代码后直接运行(若用 TypeScript 需先转译或使用支持 TS 的运行时):
node search-blog.mjs
# 或 npx tsx search-blog.ts
预期输出为抓取到的文章完整标题,例如 The title of this blog post is "Customize and automate..."。将 URL、关键词与选择器替换为你自己的目标站点即可复用到真实场景。
从第一个脚本继续深入
看完上面这 20 多行代码,你已经掌握了 Puppeteer 的最小可用闭环。围绕同一套 Browser → Page → Locator 模型,可以继续扩展的能力包括:
- 更多页面交互:点击、输入、表单、键盘/鼠标/触摸的完整事件模拟,见 docs/guides/page-interactions.md;
- 浏览器管理:多上下文(context)、多页面、屏幕与窗口管理,见 docs/guides/browser-management.md 与 docs/guides/screen-configuration.md;
- 截图与 PDF:
page.screenshot/page.pdf,对应示例 examples/screenshot.js、examples/pdf.js; - 网络层:请求拦截、模拟弱网等,见 docs/guides/network-interception.md;
- 调试与配置:见 docs/guides/debugging.md 与 docs/guides/configuration.md;
- 源码级参考:本仓库的
docs/api/目录下按类/方法组织了一份完整的 API 文档,例如入口 docs/api/index.md、Browser、Page、Locator。
仓库 examples/ 目录还提供了从截图、PDF、图片拦截到扩展/浏览器内运行 Puppeteer 等大量可直接运行的最小示例,适合作为逐个功能实验的起点。结合 docs/guides/installation.md 中 puppeteer 与 puppeteer-core 的取舍说明,你可以根据"是否需要自动下载浏览器""是否连接远程浏览器"这两个问题,快速为自己的项目选定正确的包与启动方式。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00