返回知识中心
部署

无头与有头浏览器信号对比方法

在有头、无头和带 profile 的会话里运行同一份自有夹具,再对三份记录做差异比对,而不是猜测页面看到的是哪种模式。

BotBrowser Team

无头与有头浏览器信号对比方法
文档中心

想直接进入 部署 文档吗?

这篇文章属于博客内容库。若你要步骤化配置、参考说明和持续更新,请直接进入对应 docs 分区。

团队常问页面是否「看得出」无头浏览器,实话是页面看到的是一组数值,其中一部分在不同模式之间有差异,另一部分没有。这篇文章给出一种方法来分清哪些有差异,它紧挨着数据采集场景下的浏览器指纹保护那一篇:那一篇讲授权采集会话应当保持哪些值一致,这一篇讲在相信任何一种模式之前如何测量两种模式的差别。

BotBrowser 可以在有头和无头两种模式下运行声明的 profile,并让浏览器可见值与该 profile 保持一致,因此可以在自有夹具上做三方对比;它不能让无头浏览器在每个面上都与有头完全相同,不能预测第三方站点的决定,也不替代对工作流实际访问页面的测试。

无头与有头信号对比

TL;DR

  • 比较三个会话而不是两个:有头、无头,以及按你部署的模式运行的带 profile 会话。
  • 使用同一版本的夹具,并在每个会话里记录同样的字段。
  • 逐字段比对记录,把每个差异分为「预期」或「无法解释」。
  • 无法解释的差异是需要弄清楚的发现,而不是需要掩盖的缺陷。

目录

从自有夹具开始

只有在你自己控制、版本固定、并在每种模式下报告同样字段的页面上,这种对比才有意义。第三方页面会改动自己的脚本,产生与无头模式无关的差异,而你从外部分不清自己看到的是哪一种原因。

让夹具报告数值而不是结论。一个打印「检测到无头」的页面,需要先审计它的逻辑才能信任。一个报告 navigator.userAgent、navigator.platform、screen.width、screen.height、devicePixelRatio、navigator.hardwareConcurrency、Intl.DateTimeFormat().resolvedOptions().timeZone 以及若干构造类 API 是否存在的页面,给你的是可以做差异比对、又不必相信其中任何一项的数值。

在记录里写上夹具版本。两次运行之间夹具变了,会把真实差异变成夹具差异,而比对结果会怪到浏览器头上。报告字段变化时就升版本,并同时重跑基线。

至少包含一个你预期会不同的字段。所有字段都一致,可能是两种模式确实一致,也可能是夹具不再报告某些值。预期会出现的差异相当于对照:它消失了,说明夹具变了。

三次记录同样的字段

第一个会话是有头、不带 profile。它是读者最熟悉的基准:普通显示器上的普通浏览器窗口。第二个是无头、同样不带 profile,所以唯一改变的输入是模式。第三个是部署配置:你实际发布的 profile,用你实际运行的模式。

每个会话使用同样的浏览器包版本、同样的夹具版本和同样的请求序列。会话之间改变不止一个输入,会让比对结果无法阅读,这正是这种对比要避免的错误。

把字段记成扁平对象,这样比对工具不必了解结构也能逐项比较。字段内部可以有嵌套对象,但顶层应当是一份名称与取值的列表,用简单的逐行比对就能显示。二十个扁平字段的记录,比四个嵌套字段的记录更容易比较。

每个会话的输出分开保存,并按模式命名。headed.json、headless.json 和 profile.json 让三方比对一目了然,也让你可以只重跑其中一个会话而不用重跑三个。

给每个差异分类

有头与无头之间的差异并不自动意味着问题。有些差异是预期的,因为模式改变了浏览器的行为;有些是预期的,因为你的部署 profile 声明了两个默认会话都没有的值。

用户代理(navigator.userAgent)是最明确的预期差异。无头浏览器常常报告带无头标记的值,而 profile 可能声明另一个。这是声明而不是泄漏,记录里应当写明是哪个会话声明的。

屏幕尺寸(screen.width 和 screen.height)可能不同,因为无头会话的虚拟显示器不是有头会话的物理显示器。声明了屏幕尺寸的 profile 会再次改变它。值得问的不是它们是否不同,而是部署所声明的尺寸是否与依赖它的其他值一致,比如窗口大小和视口。

设备像素比(devicePixelRatio)常常因同样的原因不同。使用默认虚拟显示器的无头会话可以报告一个比值,有头会话报告物理显示器的比值,profile 还可以声明第三个。把比值和由它得到的视口一起记录,因为只有比值而没有视口,无法说明页面测量到了什么。

时序和渲染相关的面最可能因真实原因不同。无头会话可能没有 GPU、合成方式不同、canvas 输出也不同。这些差异值得深入研究,因为即便所有声明值都一致,页面也可能借它们区分两种模式。

只出现在 profile 会话里的差异,通常是声明生效了。把它与 profile 文件对照而不是与有头会话对照,并记录声明值与观测值是否一致。这一项检查正是运行第三个会话的意义所在。

只在某一种模式的一次运行里出现的差异,既不是预期差异,也不是模式差异。它是运行之间的波动,拿它去对比另外两个会话会产生误导。在分类之前把同一会话运行两次,只出现一次的差异在重复出现之前都当作噪声。

让对比可以重复运行

放在一次性脚本里的对比,没有人会重跑。把三个会话放到一条命令后面,例如 npm run compare:modes,把三份记录写到固定目录,让差异比对成为一条命令而不是一次判断。浏览器更新改变了什么,三方比对会每个字段一行地显示出来。

把夹具和记录放进版本控制,并在文件名里写上夹具版本。后来的读者因此能看出在什么时间点比对了哪些字段,一个反复变化的字段的历史不必问任何人就能看到。

浏览器更新、profile 格式变化和驱动升级之后重跑对比。这三件事最容易在不改你代码的情况下改变某个字段,而这份对比便宜到可以每次都跑。

不要把对比做成「两种模式必须一致」的通过或失败闸门。模式允许不同;闸门应当抓的是某个字段悄悄不再被记录,或某个声明值不再出现。只要有头与无头不一致就失败的闸门,一个月内就会被忽略。

保持对比的诚实

先想清楚它能得出什么结论

三方对比回答的是一个很窄的问题:对这些字段、在这份夹具上、在这个浏览器包里,模式之间有什么不同,应用 profile 之后又有什么不同。它不回答站点是否会接受无头会话,不回答无头会话在一般情况下是否可被识别,也不回答有头会话是否更安全。这些问题涉及站点自己的逻辑、账号策略和风控规则,夹具都观察不到。

把结论写成这种窄的形式。「在 fixture@v4 与 155 版本包下,应用 profile 之前有头与无头的用户代理和屏幕尺寸不同,应用之后与 profile 声明值一致」是记录支持的结论。「无头没问题」不是,它会引出数据撑不起的决定。

让对比独立于任何目标站点。会话一旦也访问第三方页面,记录里就混进了你的浏览器行为和那个页面的响应,差异可能来自任何一边。如果需要检查真实页面,就单独跑一次并保存自己的记录,让夹具对比保持干净。

分享差异,而不是结论。评审变化字段的同事可以自己判断它对他们的工作流意味着什么;收到一个结论的同事只能凭信任接受。三份记录和一行差异才是值得保留的产物。

浏览器包或 profile 格式变化时刷新结论。为某个包版本写下的结论描述的是那个版本,后来的读者看到它附在另一个版本上,会用错。给结论标日期,并写明它适用的输入。

处理容易弄错的字段

有些字段看起来像浏览器信号,实际上是请求或环境值,不知道这一点就在模式之间比较它们,是浪费这次对比。会话发送的 HTTP 头由多个来源拼出,用户代理头可能与 navigator.userAgent 不同,差异取决于驱动和启动选项。

在两者理应一致的时候,同时记录页面可见值和请求头。两者不一致是真实发现,而如果夹具只报告页面值,它就看不见。请求头要和页面值在同一时刻捕获,这样两者描述的是同一次导航。

区域设置和时区也同样处理。Intl.DateTimeFormat().resolvedOptions().locale、Intl.DateTimeFormat().resolvedOptions().timeZone 和 navigator.language 是互相独立的值,可能不一致,系统区域不同的服务器上的无头会话会产生有头会话从未出现过的组合。三个都记录,而不是只记一个。

Canvas 和字体测量需要谨慎,因为它们依赖渲染,而渲染正是无头会话可以合理不同的地方。把测量值和渲染路径一起记录,路径不同时把差异当作预期,路径相同时才值得调查。canvas 渲染路径指南解释了同一台主机上两条软件路径为什么会产生不同输出。

让比对结果可读

两份 JSON 记录的比对,在记录形状扁平、取值简短时才可读。用排好序的键、每行一个字段来格式化记录,嵌套不超过一层。一个对每个变化字段打印 <field>: <headed> -> <headless> 的工具就够了,不需要自定义报告格式。

在每份记录里包含会话模式和 profile 这两个字段,这样脱离上下文的记录仍然说明它由什么产生。没有这两个字段的记录之后无法比较,而比较依赖于知道哪个会话是哪个。

在夹具旁边保留一份简短的字段变更记录。新增字段时写下日期和原因;删除字段时也要写,因为被删除的字段与停止报告的字段看起来一模一样。变更记录正是区分有意变化与悄悄变化的东西。

在持续集成里运行比对,只在字段消失或声明值不再与 profile 一致时失败。每个模式差异都失败的比对会被关掉,而被关掉的对比比一个只报告、由人来读的对比更糟。

发布前复核

发布复核问的是部署是否仍然报告它所声明的值。三方对比直接回答这个问题:profile 会话的取值对照 profile 文件,两个默认会话对照各自的基线。把它放进发布复核,而不是等事故之后再跑,并把结果存在 profile.json 旁边。

在支持周期内保留上一次的对比。本次发布的记录与上次的差异,比任何一份单独的记录都有更多信息,也是看出一个字段在两次更新之间逐步变化的唯一办法。

当夹具所用的版本不再提供服务时,让对比退役,并保留一条说明它描述的是哪次发布的备注。看起来是最新的过期对比,是最昂贵的一类文档,因为它被人信任。

对比部署本身,而不只是模式

三方对比告诉你模式之间有什么不同,但它不告诉你部署本身是否自洽,这是另一个问题,需要另一份记录。一个 profile 会话可以与声明的用户代理一致,却同时声明了与窗口大小对不上的屏幕尺寸,差异比对和单字段检查都抓不到这一点。

增加一遍自洽检查,把 profile 会话的字段互相比较,而不是与其他会话比较。视口(window.innerWidth 与 window.innerHeight)应当落在声明的屏幕之内;时区、区域设置和语言应当描述同一个地区;平台应当与用户代理兼容。每一条都是可以用一句话表达、并在同一次夹具运行里检查的规则。

把规则放在夹具旁边,并给每条规则起名。规则失败时,记录会说明是哪条规则,而不只是哪个字段变了,读者不必重新发现其中的关系。规则也能在字段改名后继续有效,而写死的比较不能。

对每种会话类型都运行自洽检查,而不只是 profile 会话。没有 profile 的有头会话,只要主机的区域设置与时区不一致,同样会违反规则;知道基线本身不自洽,会改变你解读其他所有差异的方式。

把规则失败与模式差异分开报告。模式差异是预期的、仅供参考;规则失败是部署自己负责的配置问题。把它们混进一个列表,会让预期差异变得嘈杂,让配置问题容易被忽略。

收到报告时怎么用这份对比

「某站点检测到自动化」这样的报告,到达时是一句话而不是一份记录。对比把这句话变成一个问题:自上一次已知良好的对比以来哪个字段变了,它是否在工作流使用的那种模式下变了。

先索要同一次部署的运行标识(runId)和对比结果。没有它们,第一个小时会花在弄清当时运行的是什么,而这恰恰是对比已经保存的信息。有了它们,复核者可以直接去看变化的字段。

与同一夹具版本的上一份良好记录比较。与更旧夹具版本做比对,会把浏览器差异和夹具差异混在一起,得到的列表比实际需要的更长。当前夹具还没有记录时,先生成一份再去调查。

把缺失字段本身当作一个发现。上周还在记录、本周没有的字段,可能意味着夹具不再报告它,也可能是页面在到达之前就失败了,或者该值变得不可用。每种情况的下一步各不相同,没有一种是「站点开始检测我们」。

让对比便宜到可以按需运行。需要一下午准备的对比,会在事故之后而不是事故期间运行,等记录出来事故已经结束了。

实践结论

无头与有头浏览器的差异分三类:预期的、由 profile 声明的、以及值得调查的。在一份自有夹具上运行三个会话,记录同样的扁平字段,给每个差异分类,并让对比保持可重复运行。把它与数据采集指南和无头服务器配置指南一起使用。

来源

#无头#有头#信号对比#自动化#部署

本文属于指南: 浏览器部署与发布:运维决策地图

让 BotBrowser 从研究走向生产

先用这些指南理解模型,再进入跨平台验证、隔离上下文和面向规模化的浏览器部署。