Puppeteer + BotBrowser:启动与配置文件常见错误
避开会破坏 BotBrowser 配置文件一致性的 Puppeteer 启动错误,包括 defaultViewport 覆盖、内置 Chromium 和浏览器级 CDP 会话用错。
BotBrowser Team
如果你想要最短的 BotBrowser 与 Puppeteer 配置,请先看 Puppeteer 的文档页面。首次启动成功后,三类集成错误最容易破坏配置文件一致性;每一类都有可识别的现象,也都能在不改动页面代码的前提下修复。
Puppeteer 是控制层。它决定启动哪个二进制文件、应用哪些启动选项,以及通过 DevTools 连接发送哪些命令。BotBrowser 则是加载配置文件并把它呈现给页面的浏览器。两者接得正确时,你的选择器、导航逻辑和数据提取与以前完全一样。接得不正确时,配置文件仍然可能加载成功,但周围某个环节与它互相矛盾,而这种矛盾比直接崩溃更难排查。
摘要
- 用显式
executablePath启动puppeteer-core;puppeteer包自带的 Chromium 不会加载任何配置文件。 - 保留
defaultViewport: null;新页面量出来是 800x600,说明 Puppeteer 的视口覆盖仍然生效。 - BotBrowser 命令(如
BotBrowser.setCustomHeaders)要通过browser.target().createCDPSession()发送,不要用页面会话。 - 把
--bot-profile、--proxy-server、--bot-timezone、--bot-locale和--bot-languages放进args,再把navigator.languages和screen.width与配置文件对照。
本文目录
- 破坏配置文件一致性的三个启动错误
- 用 puppeteer-core 启动 BotBrowser
- 在浏览器级会话上发送 BotBrowser CDP 命令
- 添加代理、时区和语言设置
- 验证启动结果并排查常见故障
- BotBrowser 覆盖什么,不覆盖什么
- 实践结论
破坏配置文件一致性的三个启动错误
大多数失败的 Puppeteer 集成都可以追溯到三个原因之一。每个原因的现象不同,所以最好按看到的现象来认识它们,而不是按你配置了什么来认识。
- 启动了错误的二进制文件。常规的
puppeteer包会下载自带的 Chromium,没有显式executablePath的启动就会使用它。浏览器能打开,页面能加载,但完全没有 BotBrowser 配置文件参与。 - Puppeteer 覆盖了视口。除非你关闭这个行为,Puppeteer 会给每个新页面应用 800x600 的默认视口。描述较大显示器的配置文件旁边,页面却报告一个很小的窗口,两个数字互相矛盾。
- BotBrowser 命令发到了错误的会话。BotBrowser 增加了自己的 CDP 命令,这些命令在浏览器级会话上可用。页面级会话不认识它们,调用会以 method not found 错误失败。
这三种情况都不会产生明显的崩溃。第一种得到的是一个完全正常的 Chromium,第二种得到的是窗口尺寸奇怪的正常页面,第三种得到的是一次容易被当作拼写错误的失败调用。这正是它们经常通过代码审查的原因。
配置文件一致性的意思很简单:本应一起出现的值要彼此吻合。一个会话报告的显示器、窗口尺寸、语言列表和时区,应该是相互匹配的组合。这些值由配置文件提供,所以配置文件之外任何改动其中某一项的东西,比如一个 Puppeteer 选项或一段遗留脚本,都会制造配置文件从未设想过的不一致。
一次良好的启动有一个简短的特征:启动的进程是 BotBrowser 二进制文件,配置文件路径作为启动参数传入,默认视口被关闭,所有 BotBrowser 专用命令都通过由浏览器目标创建的会话发送。下面各节依次讲这四点,然后说明如何从外部确认它们。
请记住这份清单是什么、不是什么。它只覆盖启动配置:哪个二进制文件、哪些参数、什么视口行为、哪个会话。它不会改变 Puppeteer 驱动页面的方式,也不对任何具体网站如何对待这个会话作出声明。
用 puppeteer-core 启动 BotBrowser
安装所需组件
BotBrowser 文档为 Puppeteer 列出四项前置条件:已安装的 BotBrowser 二进制文件、Node.js 18 或更高版本、puppeteer-core 包,以及一个配置文件。生产环境使用加密的 .enc 配置文件,并且配置文件必须与你运行的 BotBrowser 版本匹配。
- BotBrowser 二进制文件,从 BotBrowser 仓库下载,并按安装指南安装。
- Node.js 18 或更高版本。
puppeteer-corenpm 包。- 配置文件,通常使用
.enc扩展名。
包的选择比看上去更重要。完整的 puppeteer 包会附带浏览器下载,而这恰恰是你不想意外启动的东西。puppeteer-core 提供同样的自动化 API,但不含任何内置浏览器,所以它强制你指明要启动哪个可执行文件。这个要求就是集成点:你传入 BotBrowser 二进制文件的路径,Puppeteer 就启动它。
npm init -y
npm install puppeteer-core
最小启动示例
文档中的最小启动示例设置了四项内容,普通 Puppeteer 脚本通常会省略它们,或保持默认值。
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.launch({
browser: 'chrome',
executablePath: process.env.BOTBROWSER_EXEC_PATH,
headless: true,
defaultViewport: null,
args: [`--bot-profile=${process.env.BOT_PROFILE_PATH}`],
});
const page = await browser.newPage();
await page.goto('https://example.com');
await browser.close();
每个选项都有各自的作用。
browser: "chrome"选择 Chrome 模式启动,这是 BotBrowser 文档对puppeteer-core的要求。如果在puppeteer-core内启动失败,首先检查这个选项和可执行文件路径。executablePath指向 BotBrowser 二进制文件。从环境变量读取它,可以让与机器相关的路径不出现在脚本里,同一份代码在笔记本和服务器上都能工作。headless: true以无可见窗口的方式运行。调试时如果想观察会话,可以把它关掉。defaultViewport: null关闭 Puppeteer 的视口覆盖,下一节会解释。args携带 BotBrowser 的参数。--bot-profile加载配置文件,使用绝对路径可以避免不同机器上工作目录不同带来的意外。
启动调用之后的页面代码就是普通的 Puppeteer。newPage、goto、选择器和 close 中没有任何 BotBrowser 专用内容,这正是重点:迁移现有项目应该只改动启动代码块。
为什么 defaultViewport 必须是 null
Puppeteer 的启动选项参考把 defaultViewport 描述为让每个页面拥有一致视口的设置,默认值是 800x600。把它设为 null 会禁用这个默认值。在普通测试套件里这是一种便利。配合 BotBrowser 配置文件时它就变成冲突,因为配置文件已经描述了会话应该报告的显示器和窗口尺寸。
覆盖仍然生效时,Puppeteer 会在浏览器加载配置文件之后,把每个页面调整为 800x600。配置文件说的是一回事,页面量出来的是另一回事。任何人把屏幕尺寸与窗口内部尺寸对比,都会看到真实设备不会产生的组合。这是首次测试结果看起来不对、而配置文件加载又没有报错时最常见的原因。
修复只需一行,现象也很容易确认:如果新页面的窗口内部尺寸量出来是 800x600,说明默认视口仍然生效。设置 defaultViewport: null 并重新启动,页面就会改用配置文件中的尺寸。每一处启动调用都要保留这一行,包括从旧 Puppeteer 示例复制来的脚本,因为那些示例大多依赖默认值。
迁移现有项目
现有的 Puppeteer 项目需要三处修改,并且都在启动代码块里。把 puppeteer 依赖换成 puppeteer-core,这样就不会下载内置浏览器,每次启动都必须指明可执行文件。把启动选项换成上面的最小启动示例,填入 BotBrowser 二进制文件路径和配置文件路径。然后删除所有 defaultViewport 取值,并对显式的视口调用保持谨慎,因为手动设置页面尺寸会覆盖同样的配置文件尺寸。
启动代码块之后的内容保持原样:导航、等待、选择器、数据提取、截图和清理。如果项目有多个启动位置,比如测试辅助函数里一个、独立工具里一个,要逐个更新。只要遗留一处沿用旧配置的启动,就足以让内置浏览器或视口覆盖卷土重来。
启动了什么就要关闭什么。每次调用 puppeteer.launch 都会启动一个独立的浏览器进程,所以请用 try 和 finally 包住工作流程,并在 finally 中调用 browser.close(),否则一次失败的运行会留下残留进程。
不要把与机器相关的值写进源码。二进制文件路径、配置文件路径和代理凭据适合作为环境变量,就像上面的示例那样,这样同一份脚本在笔记本、CI 运行器和服务器上都不用改,凭据也不会进入版本控制。
在浏览器级会话上发送 BotBrowser CDP 命令
Puppeteer 通过 CDPSession 对象暴露 Chrome DevTools Protocol。标准协议命令在任何会话中都能使用。BotBrowser 自己的命令,比如 BotBrowser.setCustomHeaders,属于整个浏览器,所以它们在附加到浏览器目标的会话上可用。
const cdp = await browser.target().createCDPSession();
await cdp.send('BotBrowser.setCustomHeaders', {
headers: { 'x-client-app': 'bb' },
});
要看的调用是 browser.target().createCDPSession()。它在浏览器本身上创建会话。更常见的 page.createCDPSession() 在单个页面目标上创建会话,而该会话不认识 BotBrowser 命令。结果是一个 method not found 错误,看起来像命令名拼错了,即使名字完全正确。
遇到这个错误时,先检查会话从哪里创建,再检查拼写。如果会话来自页面,就从浏览器目标重新创建一个,再发送命令。整个脚本只需要一个浏览器级会话,所以可以在启动后创建一次,并用于所有 BotBrowser 命令。
给变量命名可以让作用范围在代码审查中一目了然。从 browser.target() 创建的 browserCdp 很难和页面会话混淆,而在页面辅助函数里创建的泛称变量 client,几个月后很容易被不熟悉代码的人用错。
标准的 Puppeteer 功能保持原来的路径。只有 BotBrowser 专用命令需要浏览器级会话,脚本里的其他部分继续使用常规的页面方法。
添加代理、时区和语言设置
配置文件描述一台设备。网络和地区设置描述这台设备看起来位于哪里。BotBrowser 文档把这两类设置都放在 args 中,紧挨着 --bot-profile。
const browser = await puppeteer.launch({
browser: 'chrome',
executablePath: process.env.BOTBROWSER_EXEC_PATH,
headless: true,
defaultViewport: null,
args: [
`--bot-profile=${process.env.BOT_PROFILE_PATH}`,
'--proxy-server=socks5://user:[email protected]:1080',
'--bot-timezone=Asia/Tokyo',
'--bot-locale=ja-JP',
'--bot-languages=ja-JP,ja,en-US,en',
],
});
请通过 args 中的 --proxy-server 传入代理。BotBrowser 接受代理 URL 中嵌入的凭据,文档建议避免仅限框架的代理认证方式,因为如果代理是在浏览器之外配置的,由出口地址推导出的地理值就无法对齐。如果时区与代理所在地区不一致,先检查代理是怎么配置的,再看其他地方;凭据和语法形式在代理配置里有更详细的说明。
设置代理后,文档说明的默认行为是:BotBrowser 根据代理出口地址推导时区、区域设置和语言。显式的 --bot-timezone、--bot-locale 和 --bot-languages 参数会覆盖各自负责的推导值。想要固定地区时使用它们,想让这些值跟随代理时就不要写。其中一些参数属于更高的 BotBrowser 层级,所以在依赖它们之前,请先在 CLI 参数参考中确认你的层级是否适用。
如果在一台机器上运行多个浏览器,请为每个浏览器指定各自的 --user-data-dir。文档把实例之间的冲突列为已知问题,并把唯一的目录作为解决办法,因为它让存储和会话状态保持独立。
通过 browser.createBrowserContext() 设置按上下文的代理,需要仔细核对你的版本。那里是否接受代理选项,取决于 Puppeteer 版本和 BotBrowser 层级,所以请把它当作需要在自己环境中测试的功能,而不是每种组合都支持的功能。
验证启动结果并排查常见故障
验证应当从页面一侧进行,因为网站看到的就是页面一侧。打开一个页面,读取少量取值,并与你加载的配置文件对比。
const page = await browser.newPage();
await page.goto('https://example.com');
const reported = await page.evaluate(() => ({
userAgent: navigator.userAgent,
languages: navigator.languages,
screenWidth: screen.width,
screenHeight: screen.height,
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
pixelRatio: window.devicePixelRatio,
}));
console.log(reported);
分三步阅读输出。User-Agent 和语言列表应当与配置文件及你的地区参数描述一致。屏幕尺寸应当与配置文件的显示器一致。窗口内部尺寸应当与该显示器相符,并且不应恰好是 800x600。如果是 800x600,请回到启动代码块查找视口覆盖。
这段代码是对你自己集成的健全性检查,不是测试套件,也不是对任何网站的承诺。请把输出与你加载的配置文件对比,而不是与你自己的机器对比。检查的意义在于这些值来自配置文件,所以与宿主机完全吻合反而是需要再看一遍的理由。修改启动代码块之后运行一次,把脚本迁移到另一台机器时再运行一次,因为不同的二进制文件路径或复制来的示例,正是多数回归的来源。
出现异常时,现象通常直接指向原因。
| 现象 | 可能原因与修复 |
|---|---|
| 视口量出来是 800x600 | 默认视口仍然生效。设置 defaultViewport: null。 |
| 在 puppeteer-core 内启动失败 | 添加 browser: "chrome",并确认 executablePath 指向 BotBrowser 二进制文件。 |
| 配置文件似乎没有生效 | 可能启动了错误的二进制文件。确认可执行文件路径,并使用绝对的配置文件路径。 |
| BotBrowser 命令返回 method not found | 会话来自页面。请用 browser.target().createCDPSession() 创建。 |
| 时区或语言与代理不一致 | 通过 args 中的 --proxy-server 传入代理,不要用仅限框架的选项。 |
| 多个实例互相干扰 | 为每个实例指定唯一的 --user-data-dir。 |
如果配置文件似乎完全没有生效,请由外向内排查,这些示例用到的参数组合收录在CLI 使用手册里。确认你启动的进程是 BotBrowser 二进制文件而不是普通 Chromium,确认配置文件路径是绝对路径且文件存在,并确认配置文件包与你安装的 BotBrowser 主版本匹配,因为文档要求版本一致。每次只改一处再重新启动,这样你就知道是哪一处修改起了作用。
最后一个常被问到的问题是,是否还需要通过 JavaScript 给页面打补丁的插件。它们不属于文档中的 BotBrowser 配置。配置文件由你启动的浏览器二进制文件应用,上面的启动清单不依赖页面级补丁。加上它们会让你在启动结果异常时多一层需要分析的东西,所以请先测试文档中的配置,只在有明确理由时再添加其他内容。
证明配置来自 profile 而不是本机
把上报值和自己的机器对比,回答的是错误的问题。悄悄启动了普通 Chromium 的会话上报的就是本机数值;而配置正确时,如果 profile 本身就是按相近设备生成的,数值也可能碰巧接近。两种结果都无法说明 profile 是否生效。
能回答这个问题的方法只用两个 profile。同一份脚本跑两次,每次加载不同的 profile,打印同一批字段,其余条件完全一致:相同的可执行文件路径、相同的视口设置、相同的页面。
// launch-report.mjs: 每个 profile 运行一次,分别保存输出
const fields = await page.evaluate(() => ({
ua: navigator.userAgent,
langs: navigator.languages.join('|'),
screen: `${screen.width}x${screen.height}`,
inner: `${window.innerWidth}x${window.innerHeight}`,
dpr: window.devicePixelRatio,
}));
console.log(JSON.stringify(fields));
BOT_PROFILE_PATH=/path/profile-a.enc node launch-report.mjs > a.json
BOT_PROFILE_PATH=/path/profile-b.enc node launch-report.mjs > b.json
diff a.json b.json
三种结果对应三个不同的问题。
| 结果 | 含义 | 下一步 |
|---|---|---|
diff 打印出差异字段 | 两个 profile 都已加载,数值随 profile 变化 | 启动环节无需修改 |
diff 完全没有输出 | 没有任何 profile 到达浏览器 | 确认 executablePath,并确认启动的进程是 BotBrowser 二进制文件 |
只有 ua 不同,screen 与 inner 相同 | 视口覆盖仍生效,或两个 profile 描述的是同一块屏幕 | 检查 defaultViewport: null,再对比 profile 的显示字段 |
ua 相同但缺少 profile 特有字段 | profile 文件未加载,或与二进制文件主版本不匹配 | 核对 profile 包与已安装的 BotBrowser 主版本 |
双 profile 对比的成本很低:不需要目标网站,除一个空白页外也不需要网络。第一次接好 Puppeteer 时跑一次,之后每当脚本指向新的二进制文件或复制来的示例,再跑一次,因为这两处最容易在无人察觉的情况下改变启动配置。
如果对比完全没有输出,就不要再调试页面了。newPage、goto 或任何选择器都无法让 profile 生效,这个决定在启动调用里就已经做完。
BotBrowser 覆盖什么,不覆盖什么
BotBrowser 文档说明了通过 puppeteer-core 启动的方式:browser 设为 chrome、提供 executablePath、使用 --bot-profile 以及 defaultViewport: null,这样配置文件的视口尺寸而不是 Puppeteer 默认的 800x600 保持权威,同时 BotBrowser CDP 命令在浏览器级会话上发送。对你来说,这意味着一份简短、可检查的启动步骤:如果会话看起来不一致,可以对照这四点找出不同的那一项。BotBrowser 不能修复被 Puppeteer 覆盖的启动,比如保持默认值的 defaultViewport,也不能替代正确的 Puppeteer 启动配置,更不保证任何目标网站如何对待这个会话。
同样的限制也适用于相邻的话题。代理和地区参数取决于代理本身和你的 BotBrowser 层级,按上下文设置代理的支持则同时取决于 Puppeteer 版本和层级。把 BotBrowser 与页面级补丁插件相比较,只是说明两种做法各自在哪一层起作用:一个通过浏览器二进制文件,另一个通过注入的 JavaScript。这并不说明任何网站会如何对待其中任何一种。
相关的配置可以阅读:同时使用 Playwright 的话看Playwright 入门,参数组合看CLI 使用手册,在服务器上运行看无头服务器配置,代理语法和凭据看代理配置。
公开来源
实践结论
在跑任何页面代码之前,先确认四个启动决策:executablePath 指向 BotBrowser 二进制文件,--bot-profile 指向与所装主版本匹配的绝对 .enc 文件,defaultViewport: null 让 Puppeteer 的 800x600 覆盖不落到页面上,BotBrowser 命令走由 browser.target() 创建的会话。会话看起来不一致时,先对照这四点,再改动选择器或导航逻辑。BotBrowser 负责配置启动;它无法纠正被 Puppeteer 覆盖的选项,也不决定目标网站如何对待这个会话。
本文属于指南: 浏览器自动化入门:首次运行地图