每个设置 User-Agent 标头的爬虫都会选定一个浏览器版本,而几乎没有一个会去更改它。这个字符串在项目启动那天就写进了配置文件,此后这个项目会持续多年地自称是那个浏览器。与此同时,真实的浏览器在不断更新:Chrome 在 2025 年 9 月底到 2026 年 9 月底之间发布了 13 个主要版本。
这种版本漂移是否重要?反机器人系统清楚真实访客使用的浏览器版本,因此一个已经多年没人使用的版本会显得格外突出。我们通过请求 397 个顶级网站的首页,分别声称使用五个不同的 Chrome 版本(从当前版本到六年半前的版本),测量了这种差异的具体程度。
关键结论
- 声称的版本越旧,被拒绝的概率就越高,且每次都是如此。17.1% 的首页对当前 Chrome 版本发起了质询或拦截;19.9% 拒绝了一年前的版本,22.7% 拒绝了六年半前的版本。
- 这种影响只朝一个方向发生。有 22 个网站对最旧版本予以拒绝,但对当前版本予以放行;没有一个网站拒绝当前版本却放行旧版本。
- 对当前版本的两次相同请求在全部 397 个网站上得到了完全一致的结果,因此这些差异不是噪声。
- 使用 Akamai 的网站反应最快:在拒绝了仅一年前版本的 11 个网站中,有 9 个受 Akamai 保护。受 Cloudflare 保护的网站大多只对最旧的版本做出反应。
- 应保持版本是最新的,使其与请求的其余部分保持一致,并像对待任何依赖项一样定期检查它。
我们如何测量
我们从早先的反机器人技术栈查询中随机抽取了 400 个网站的首页,这些首页来自 Tranco 排名前 1000 的域名。在 2026 年 10 月 5 日,我们对每个首页请求了六次,每次间隔一秒,并按每个网站随机的顺序进行,使用标准的 Windows Chrome User-Agent 字符串,声称以下版本:
| 声称版本 | 稳定版发布日期 | 测试时的年龄 |
|---|---|---|
| Chrome 154,请求两次 | 2026 年 9 月 22 日 | 当前版本 |
| Chrome 141 | 2025 年 9 月 30 日 | 约 1 年 |
| Chrome 129 | 2024 年 9 月 17 日 | 约 2 年 |
| Chrome 110 | 2023 年 2 月 7 日 | 约 3.5 年 |
| Chrome 80 | 2020 年 2 月 4 日 | 约 6.5 年 |
对当前版本的重复请求作为对照组:如果一个网站本身存在波动,那么这两次请求之间就会出现差异。所有请求均来自同一个 Python HTTP 客户端和同一个连接;只有 User-Agent 中的版本号发生了变化。有三个网站至少对一次请求未作出响应,因而被排除,最终剩余 397 个网站。
我们发现了什么
| 声称版本 | 已放行 | 已质询 | 已拦截 | 质询或拦截占比 |
|---|---|---|---|---|
| Chrome 154,首次请求 | 329 | 42 | 26 | 17.1% |
| Chrome 154,重复请求 | 329 | 42 | 26 | 17.1% |
| Chrome 141 | 318 | 43 | 36 | 19.9% |
| Chrome 129 | 315 | 43 | 39 | 20.7% |
| Chrome 110 | 314 | 43 | 40 | 20.9% |
| Chrome 80 | 307 | 52 | 38 | 22.7% |
对当前版本的两次请求在每一个网站上都完全一致,这使得表格其余部分很容易解读:每一处差异都来自版本号。
仅统计那些两次都放行了当前版本的网站,拒绝旧版本的网站数量随版本年龄的增加而稳步上升:
| 声称版本 | 放行当前版本但拒绝此版本的网站数 |
|---|---|
| Chrome 141,1 年前 | 11 |
| Chrome 129,2 年前 | 14 |
| Chrome 110,3.5 年前 | 15 |
| Chrome 80,6.5 年前 | 22 |
没有一个网站出现相反的情况。陈旧的版本从未带来过帮助。
谁在反应,以及如何反应
将这些网站与早先查询中检测到的防护措施进行交叉比对,可以看出不同供应商划定界限的位置各不相同:
- 一年的时间就足以让 Akamai 做出反应。 在拒绝 Chrome 141 的 11 个网站中,有 9 个受 Akamai 保护;全部 11 个都以 HTTP 403 响应,其中 10 个是直接拦截而非质询。
- Cloudflare 对非常旧的版本做出了反应。 在拒绝 Chrome 80 的 22 个网站中,有 10 个受 Cloudflare 保护并返回了质询;另有 9 个受 Akamai 保护。
- 页面很少向访客说明原因。 只有两个网站针对 Chrome 80 显示了”请更新浏览器”之类的提示,而对当前版本没有显示。大多数网站只是直接拒绝。
这与机器人管理的工作方式是一致的。使用一年前浏览器的真实访客并不常见,而使用六年前浏览器的更是罕见,因此旧版本本身是一个较弱的信号,但与请求中任何其他异常之处结合起来,就会成为一个更强的信号。
代码
下面的函数可对任意 URL 运行相同的比较:它会以随机顺序分别声称每个版本请求页面,并为每次请求返回状态、结果和大小,以及页面是否告知访客其浏览器已过时。将某个版本列出两次即可得到噪声对照组。
import random
import re
import time
import requests
CHROME = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{v}.0.0.0 Safari/537.36"
OUTDATED = re.compile(r"(browser (is|you are using is) (not supported|out of date|outdated|no longer supported)"
r"|update your browser|upgrade your browser|unsupported browser|outdated browser)", re.I)
def classify(response):
"""served, challenged or blocked, from the status and well-known challenge markers."""
body = response.text[:20000].lower()
if (response.headers.get("cf-mitigated", "").lower() == "challenge"
or response.headers.get("x-amzn-waf-action", "").lower() == "challenge"
or "captcha-delivery.com" in body):
return "challenged"
if response.status_code in (401, 403, 405, 429) or response.status_code >= 500:
return "blocked"
return "served"
def ua_drift_check(url, versions, pause=1.0, seed=None):
"""Request the same URL claiming each Chrome version, in random order, and compare what comes back.
A version may be listed twice, as a control for how much the site varies on its own."""
order = list(enumerate(versions))
random.Random(seed).shuffle(order)
results = [None] * len(versions)
for i, v in order:
headers = {"User-Agent": CHROME.format(v=v), "Accept": "text/html,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9"}
try:
r = requests.get(url, headers=headers, timeout=20)
results[i] = {"version": v, "status": r.status_code, "outcome": classify(r), "bytes": len(r.content),
"outdated_notice": bool(OUTDATED.search(r.text))}
except requests.RequestException as e:
results[i] = {"version": v, "error": type(e).__name__}
time.sleep(pause)
return results
在更改 User-Agent 前后,针对你所依赖的网站运行此代码,并保留结果:如果某个目标开始拒绝你发送的版本,你就能判断版本是否是其中的原因。
本次测量的局限性
- 仅限首页、单一网络、单一时刻。 更深层的页面、其他国家和其他时间段的表现可能有所不同。
- 并非真实浏览器。 我们的客户端通过 Python HTTP 库发送了 Chrome 的 User-Agent,因此每个请求本身就带有与 Chrome 不匹配的网络指纹,正如TLS 与 HTTP/2 指纹识别中所解释的那样。这项比较隔离出了版本号本身的影响;它并不能说明每个版本的真实浏览器实际表现会如何。
- 基线并非为零。 即便是当前版本,也有 17% 的首页拒绝了来自该客户端的请求,这提醒我们:正确的 User-Agent 是必要条件,但并非充分条件。
我们也应当坦诚一个关于我们自身的发现。本周早些时候我们的反机器人查询使用的是 Chrome 129 的 User-Agent:这是一个两年前的字符串,是从一个旧脚本中复制过来的。按照本次测量的结果,这使我们损失了几个百分点的首页放行率。
保持 User-Agent 的时效性
- 把它当作一项依赖来对待。 把浏览器版本集中放在一处,并按计划更新;Chrome 大约每四周发布一个新的主要版本。
- 保持在当前版本的几个版本之内。 在我们的数据中,一年前的版本就已经比当前版本更常被拒绝。
- 保持一切协调一致。 一个当前的 Chrome 版本号,如果配上过时的标头顺序、缺失的客户端提示或非浏览器的网络指纹,仍然是不一致的;设置正确的标头和 User-Agent 涵盖了完整的做法。
- 每个会话使用一个身份。 在同一会话中切换版本比使用任何单一版本都更可疑。
- 在 CI 中进行测试。 对关键目标按计划用当前和先前的配置进行检查,可以在版本漂移影响到你的数据之前就发现它;在 CI/CD 中运行爬虫展示了如何搭建这样的流程。
- 关注拒绝率。 403 和质询数量的缓慢上升是版本漂移的典型症状;这应当纳入目标健康评分。
结论
浏览器版本不是一个只需设置一次的参数。在我们对 397 个顶级网站的测量中,所声称的 Chrome 版本每增加一年的漂移,就会带来更多的拒绝,而这种影响从未反过来发生,有些防护服务甚至只需版本过时一年就会做出反应。
保持版本的时效性,使请求的其余部分与之保持一致,并在对你重要的目标上测量其影响。这是一个爬虫项目能做出的最廉价的可靠性改进之一。