Headless 浏览器采集:实际会发生什么变化
比较可视 Chrome、统一 headless 和 headless shell。记录 renderer、viewport 与 webdriver 信号,但不据此推断网站判定。
BotBrowser Team
BotBrowser 支持使用带 profile 的 Chrome 启动可视或 headless 采集,但 profile 不会替换 Chrome renderer,也不能保证远程网站的响应。即使可视运行和 headless 运行访问同一个 URL,数据提取脚本也可能拿到不同页面内容。首先要确认实际启动了哪种浏览器实现和运行模式;其次要确认 viewport、设备缩放、字体、存储或 fixture 状态是否同时改变。这些是不同原因,不应把它们混成“浏览器可见或不可见”的同义标签。
要控制采集模式的比较,需要区分三种情况:可视 Chrome 窗口、Chrome 的统一 headless 模式,以及 Puppeteer 可选的 headless shell。Chrome 文档说明统一 headless 使用常规 Chrome 实现;shell 则是单独的可执行文件。这些说明都不保证页面输出一致,也不能预测远程服务是否会接受请求。自有本地 fixture 可以帮助记录:在明确的配置下,哪些值发生了变化。
摘要
headless: false打开可视浏览器窗口;当前 Puppeteer 中headless: true选择统一 headless;headless: 'shell'选择单独安装的 shell 构建。- 在把差异归因于模式之前,固定浏览器版本、fixture、viewport、设备缩放、存储和启动参数。
- 把
window.innerWidth、screen.width、window.devicePixelRatio、navigator.webdriver和浏览器版本记录为观察值,而不是身份或接受度分数。 - 使用自有本地 fixture,每次只改变一个变量。页面能观察到差异,不等于第三方检测器接受或拒绝了这次运行。
目录
三种运行模式
“Headless”描述浏览器如何启动,不代表一种通用的渲染引擎。浏览器二进制、浏览器版本、启动参数、操作系统、显示服务、viewport、字体和 profile 都可能影响结果。有效比较应记录模式和可执行文件,而不是只保存一个布尔值。
可视 Chrome 窗口
可视运行会创建一个出现在屏幕上的顶层 Chrome 窗口。窗口管理器和显示环境可能影响窗口外部尺寸、可用屏幕大小、缩放、焦点和可见状态。测试 harness 可以单独设置页面 viewport,不必依赖外部窗口尺寸;因此,“可视”不表示页面一定读取到物理显示器的尺寸。
在 Puppeteer 中,headless: false 请求可视模式。页面可以通过 window.innerWidth 和 window.innerHeight 读取内部 viewport;screen.width 和 screen.height 则描述文档可见的屏幕尺寸。它们回答不同问题。固定 viewport 可以让第一组尺寸保持稳定,但显示器或窗口管理器仍可能改变第二组。
统一 headless Chrome
当前 Chrome headless 模式使用常规 Chrome 浏览器实现,而不是早期独立的 headless 实现。Chrome 文档把这一变化标记为从 Chrome 112 开始。这是实现层面的边界,不表示 headless 会创建可见的桌面窗口,也不要求任何主机上的页面可见值都与可视运行一致。
Puppeteer 用 headless: true 选择统一模式。命令行也可以要求 headless 启动。实际运行的二进制和版本仍然重要:旧版自动化包、固定版本的二进制或自定义启动参数都可能改变最终启动的内容。应将 browser.version() 或协商得到的对应版本与模式一起记录,不要假设已安装的包一定选择了预期浏览器。
Headless shell
Puppeteer 也提供 headless: 'shell',它会选择独立的 chrome-headless-shell 可执行文件。它并不是统一 headless Chrome 的另一个名称。Chrome 文档将 shell 描述为单独的 headless 二进制,Puppeteer 指南也为它定义了独立的模式选择项。如果没有同时记录运行模式和二进制实现,shell 与可视模式的比较可能同时改变这两个因素。
这个字符串选项属于 Puppeteer;其他自动化库可能使用不同名称,也可能不通过相同 API 提供 shell 模式。不要把 Puppeteer 字符串选项直接当成 Playwright 设置,应该先核对对应库的当前文档。浏览器启动配置属于另一问题,Puppeteer 集成指南和 headless 服务器部署说明讲的是不同的运维事项。
| 运行标签 | Puppeteer 选择项 | 应记录的实现信息 | 该标签不能证明什么 |
|---|---|---|---|
| 可视窗口 | headless: false | Chrome 可执行文件及版本 | 页面输出等同于真实用户设备 |
| 统一 headless | headless: true | 常规 Chrome 实现及版本 | 页面无法区分运行环境 |
| Headless shell | headless: 'shell' | chrome-headless-shell 可执行文件及版本 | shell 与常规 Chrome 行为一致 |
Chrome 132 使旧 headless 实现可以通过单独的 shell 二进制使用。因此版本号必须写入记录:模式名称看起来相近,自动化选项背后的可执行文件却可能已改变。更新 runner 时应查阅 Chrome Headless 文档和 Puppeteer headless 模式指南,不要仅凭旧文章里的参数推断当前实现。
页面可以读取什么
页面不会收到一个标准化的 isHeadless 通用属性。它读取浏览器 API、布局结果,以及配置环境所公开的行为。好的 fixture 会记录少量具体字段并保留它们各自的含义,而不是把它们压缩成一个“headless 指纹”。
window.innerWidth 和 window.innerHeight 表示 CSS 像素下的布局 viewport。screen.width 和 screen.height 表示文档可见的屏幕尺寸。window.devicePixelRatio 表示当前显示配置下 CSS 像素与设备像素的比例。即使浏览器实现和 URL 不变,响应式页面也可能因 viewport 变化而选择另一种布局。MDN 的 innerWidth 说明和 devicePixelRatio 说明定义了这些 API 的含义;它们没有说这些值只由 headless 模式决定。
navigator.userAgent 以及可用时的 navigator.userAgentData 会暴露浏览器标识信息,但其内容受缩减规则和可用性条件影响。这些字符串不能可靠地将会话标注为可视或 headless。User-Agent 可能保持不变,而 viewport 或渲染结果已经变化;页面代码不应把它当成物理设备的证明。
navigator.webdriver 是 WebDriver 标准化的自动化相关信号。MDN 文档说明浏览器何时报告这一自动化状态;Chrome 在包括 headless 的自动化配置中可能暴露该属性。它不是完整的模式分类器:true 表示浏览器报告受 WebDriver 控制,不代表某个网站已经作出判定。属性契约应以 MDN 的 Navigator.webdriver 文档为准。Headless 信号对比则讨论如何记录多个浏览器可见面,而不把单个字段当成检测结果。
其他输出也受启动模式之外的因素影响。字体可用性会改变换行和元素尺寸。语言和时区会改变格式化文本。已存 cookie 会影响页面显示登录提示还是数据表格。网络路由会改变服务端响应或时间。若一次采集同时改变多项配置,就无法把最终 DOM 差异归因于 headless 模式。
部署环境是另一个独立维度。BotBrowser headless 服务器设置记录该产品的虚拟显示和 profile 要求。BotBrowser 自动化一致性说明描述基于 profile 的浏览器值。这些是 BotBrowser 的运行信息,不是对上游 Chrome 架构的描述,也不保证页面在每种模式下都收到相同值。
受控的双模式差异比较
只使用自己拥有或获准检查的页面。本地 fixture 可以避免把启动模式观察与远程服务的访问授权、请求限额、挑战、内容变化、服务策略或站点状态混为一谈。下面示例假设 fixture 地址为 http://127.0.0.1:4173/fixture.html;这里没有实际运行示例,也没有提供测量输出。
每个进程只运行一种模式。保持 Puppeteer 安装、可执行文件、浏览器版本、本地页面、profile 策略、viewport、设备缩放、语言和存储状态一致。前两次运行中,模式名是唯一主动改变的变量。shell 的可执行文件不同,因此应单独比较。
import puppeteer from 'puppeteer';
const modeName = process.argv[2];
const modes = {
headed: false,
headless: true,
shell: 'shell',
};
if (!Object.hasOwn(modes, modeName)) {
throw new Error('Choose one mode: headed, headless, or shell');
}
const fixture = 'http://127.0.0.1:4173/fixture.html';
const browser = await puppeteer.launch({
headless: modes[modeName],
defaultViewport: {
width: 1365,
height: 768,
deviceScaleFactor: 1,
},
});
try {
const page = await browser.newPage();
await page.goto(fixture, { waitUntil: 'domcontentloaded', timeout: 10000 });
const pageFields = await page.evaluate(() => ({
origin: location.origin,
userAgent: navigator.userAgent,
webdriver: navigator.webdriver,
viewport: { width: innerWidth, height: innerHeight },
screen: { width: screen.width, height: screen.height },
devicePixelRatio,
visibilityState: document.visibilityState,
}));
process.stdout.write(
`${JSON.stringify(
{
mode: modeName,
browserVersion: await browser.version(),
pageFields,
},
null,
2
)}\n`
);
} finally {
await browser.close();
}
将同一脚本保存为 mode-diff.mjs,分别保存每次运行的输出:
node mode-diff.mjs headed > headed.json
node mode-diff.mjs headless > headless.json
node mode-diff.mjs shell > shell.json
diff -u headed.json headless.json
代码输出的是记录,不是通过/失败结论。它不会访问第三方域名、不比较账号,也不声称任何模式会被接受。非零退出码或缺失 JSON 文件表示运行问题;字段值不同则是待分类的观察。严格比较可视和统一 headless 时,比较前两个文件并核对 browserVersion。shell 输出应被视为实现不同的独立对比,并在记录中保留其版本。
示例中的 viewport 是固定输入,不代表任何通用 viewport。defaultViewport 设置的是 Puppeteer 页面 viewport,并不控制操作系统窗口的全部尺寸。脚本也记录屏幕尺寸和设备缩放,因为它们可暴露主机环境变化。如果 fixture 需要指定 viewport 才能复现某个响应式分支,请在每次运行中保持该值一致,并与 JSON 文件一同记录。
如果本地 fixture 含动态数据,先让测试页面保持确定性,再比较模式。固定 fixture 版本,使用静态本地资源,冻结应用状态,并避免依赖响应可能在两次采集中变化的在线 API。如果字体从网络加载,即使 renderer 未变,加载时间差也可能改变布局。应将测试页源代码版本和资源集合与采集记录一同保存,避免未来浏览器升级时拿不同 fixture 作比较。
给差异分类
先检查记录中的浏览器版本与启动配置。如果它们不符合比较目标,这就不是受控的模式比较。然后查看 viewport、屏幕尺寸和设备缩放。innerWidth 不同可能选择不同 CSS breakpoint;devicePixelRatio 不同可能改变光栅化和 canvas 后备分辨率。这些结果可以通过具体输入解释,而不是归结为笼统的 headless 标签。
将结果分为三类:
| 类别 | 示例 | 下一步诊断 |
|---|---|---|
| 已声明输入差异 | shell 运行报告了另一版本或可执行文件 | 作为独立的二进制比较保留,不要只归因于可见性 |
| 环境介导的差异 | 屏幕尺寸、字体、语言或存储不同 | 对齐输入,或把差异记录为环境的一部分 |
| 页面观察到的差异 | 本地 fixture 读到不同 navigator.webdriver 或选择不同布局 | 核对 API 契约和浏览器配置,不要推断远程检测器的决定 |
例如,navigator.webdriver 改变时,应追查自动化控制和浏览器启动配置。该属性报告的是 WebDriver 相关状态,不说明采集是否获准,也不说明站点是否接受了会话。排查 CSS 前,应先将变化后的 innerWidth 与配置的页面 viewport 对照。screen.width 改变表示页面可见的屏幕配置发生了变化,不应重新解释成物理设备身份。
模式比较也无法隔离网络端行为。如果 fixture 服务、cookie、请求时间或缓存内容变化,两次运行可能收到不同标记。将 DOM 差异归因于 Chrome 前,先比较 fixture 响应和源文件状态。若涉及远程站点,应遵循其公开访问策略,并把服务端响应与浏览器 API 读数分开;页面不提供绕开挑战或访问控制的做法。
如果目标是了解某个命名检测器如何组合观测信号,那与选择采集启动模式是不同问题。CreepJS headless 检测层介绍公开的信号类别,profile 一致性则讨论基于 profile 的浏览器值。未实际测量的服务,不能由本地比较预测其决定。
哪些情况无法得出结论
如果不知道实际可执行文件、浏览器版本不同、fixture 已改变,或者两次运行没有使用相同 viewport 和状态,比较就无法得出有效结论。如果只保留截图而没有启动元数据,也同样如此:截图展示像素,却不能说明产生这些像素的二进制、参数、profile、字体、语言或显示配置。
一次观察不能证明某行为适用于所有 headless 浏览器。浏览器会发布新版本。Puppeteer 和 Chrome 也可能使用不同的更新周期。只有结合解释该选项的库版本及其启动的可执行文件,headless: true 才有明确含义。为了可复现地比较,应固定这些依赖,并在计划内升级后重新运行自有 fixture。
不要在同一次“前后对比”里同时改模式、viewport、profile、语言和网络。多个变量一起改变时,页面结果会有多个合理原因。可以使用小型矩阵:第一组在相同可执行文件、版本和 viewport 下比较可视与统一 headless;第二组单独记录 shell 及其实际可执行文件;有需要时,再保持模式不变单独比较第二种 viewport。每一组只回答一个范围明确的问题。
BotBrowser 文档中的 profile 行为是产品配置的一个维度,不是上游 Chrome 模式。Profile 可以提供文档说明的浏览器值,服务器部署页也描述了运行限制。这不表示 renderer 架构发生变化,不表示所有主机信号都相同,也不保证站点返回相同内容。Profile 一致性与可视/headless 输出之间的边界能帮助分开这些问题。
常见问题
Headless Chrome 一定能被检测出来吗?
没有一个受支持的通用布尔值,可以对所有页面和浏览器版本回答这个问题。页面能读取 API 和布局结果,自动化配置也可能暴露 WebDriver 相关状态。某次读数只描述该次运行,不能保证所有站点都使用同样的字段或得到同样结论。有效记录应包括浏览器版本、启动模式、输入条件、fixture 和观察字段。
Headless Chrome 生成的页面是否和可视 Chrome 完全相同?
Chrome 的统一 headless 实现与常规 Chrome 共用实现,但实现相同并不意味着所有环境输入相同。可视桌面窗口、虚拟显示、字体、图形栈、viewport、设备缩放和 profile 都可能不同。把变化归因于启动模式之前,应使用同一可执行文件和受控 fixture 进行比较。不要承诺跨操作系统的像素或 DOM 输出完全一致。
headless: 'shell' 只是 headless: true 的另一种写法吗?
不是。Puppeteer 将 shell 选项说明为单独的 headless-shell 可执行文件;true 则选择统一 headless Chrome。可用二进制取决于软件包版本和安装方式。应记录每次运行的浏览器版本;如果实现发生变化,不要把 shell 结果直接并入统一 headless 的基线。
数据有差异时,采集器应该改用可视模式吗?
先确定变化的字段,并核对受控输入。如果差异来自 viewport、字体、存储或页面状态,切换模式可能只是隐藏原因,而不是修复原因。如果远程服务改变了响应,应遵守其公开访问规则,并单独调查服务端响应。更换启动模式不是规避访问控制的办法,也不能证明数据提取已获授权或足够可靠。
来源
- Chrome Headless 模式
- Chrome 的新版 Headless 模式
- Puppeteer:Headless 模式
- BotBrowser:Headless 服务器设置
- BotBrowser:自动化一致性
- MDN:Navigator.webdriver
- MDN:Window.innerWidth
- MDN:Window.devicePixelRatio
实践结论
根据部署和工作负载选择启动模式,并记录实际启动的内容。在同一份本地 fixture、同一版本、viewport、profile 策略和状态下比较可视与统一 headless;将 shell 作为独立可执行文件处理。差异文件可以解释该配置下页面可见值的变化,但不能认证身份、访问授权或网站响应。
本文属于指南: 浏览器自动化入门:首次运行地图