返回知识中心
入门

Headless 浏览器采集:实际会发生什么变化

比较可视 Chrome、统一 headless 和 headless shell。记录 renderer、viewport 与 webdriver 信号,但不据此推断网站判定。

BotBrowser Team

文档中心

想直接进入 入门 文档吗?

这篇文章属于知识中心的指南库。若你要步骤化配置、参考说明和持续更新,请直接进入对应 docs 分区。

BotBrowser 支持使用带 profile 的 Chrome 启动可视或 headless 采集,但 profile 不会替换 Chrome renderer,也不能保证远程网站的响应。即使可视运行和 headless 运行访问同一个 URL,数据提取脚本也可能拿到不同页面内容。首先要确认实际启动了哪种浏览器实现和运行模式;其次要确认 viewport、设备缩放、字体、存储或 fixture 状态是否同时改变。这些是不同原因,不应把它们混成“浏览器可见或不可见”的同义标签。

要控制采集模式的比较,需要区分三种情况:可视 Chrome 窗口、Chrome 的统一 headless 模式,以及 Puppeteer 可选的 headless shell。Chrome 文档说明统一 headless 使用常规 Chrome 实现;shell 则是单独的可执行文件。这些说明都不保证页面输出一致,也不能预测远程服务是否会接受请求。自有本地 fixture 可以帮助记录:在明确的配置下,哪些值发生了变化。

有界面 Chrome、统一 headless 和 headless 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: falseChrome 可执行文件及版本页面输出等同于真实用户设备
统一 headlessheadless: true常规 Chrome 实现及版本页面无法区分运行环境
Headless shellheadless: '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、字体、存储或页面状态,切换模式可能只是隐藏原因,而不是修复原因。如果远程服务改变了响应,应遵守其公开访问规则,并单独调查服务端响应。更换启动模式不是规避访问控制的办法,也不能证明数据提取已获授权或足够可靠。

来源

实践结论

根据部署和工作负载选择启动模式,并记录实际启动的内容。在同一份本地 fixture、同一版本、viewport、profile 策略和状态下比较可视与统一 headless;将 shell 作为独立可执行文件处理。差异文件可以解释该配置下页面可见值的变化,但不能认证身份、访问授权或网站响应。

#Headless Browser#网页抓取#Chrome

本文属于指南: 浏览器自动化入门:首次运行地图

在 BotBrowser 里跑一遍同样的检查

加载 profile,让刚读到的这些信号在浏览器内部保持一致,并在正式上线前先在自己的机器上确认结果。