大多数爬虫项目都是以艰难的方式发现网站防护机制的:原型周一还能正常运行,周二就出现了验证页面,到了周五团队就在围绕无头浏览器重建一切。这些情况中的大部分其实在第一天就能知晓。机器人管理产品会在网站的响应头和 cookie 中留下可见的痕迹,而一次普通的请求就足以读取它们。
我们构建了一个简短的查询工具,正是用来做这件事,并对排名前 1,000 的域名的主页运行了它,记录了普通 HTTP 客户端得到的响应。本指南分享了这些结果、代码,以及在规划项目时如何使用这个答案。
关键要点
- 一次请求就能揭示很多信息。在我们能够加载的 653 个顶级网站主页中,供应商 cookie 和头信息识别出了 37% 的网站使用了机器人防护或边缘安全供应商,其中 16% 正在运行一个处于活跃状态的机器人管理产品。
- Cloudflare 是迄今为止最常见的,出现在四分之一的主页上,其次是 Akamai。DataDome、AWS WAF、Imperva 和 HUMAN 出现的频率要低得多,而 DataDome 和 HUMAN 对我们发送的每一个请求都进行了验证或阻止。
- 声称自己是 Chrome 的普通 HTTP 客户端在 81% 的主页上获得了正常响应,在 10% 的主页上遇到了验证,在 9% 的主页上被阻止。使用该库默认的 User-Agent 时,被阻止的比例上升到了 16%。
- 伪装成浏览器可能会适得其反。一家大型零售商的十个国家店面网站,对诚实标明该库身份的 User-Agent 提供了完整的主页,而对声称是 Chrome 的请求则返回了一到两千字节的验证页面。
- 将这个查询工具当作一项规划输入:它能告诉你该预期什么,官方 API 或许可是否是更好的路线,以及该如何为项目做预算。
单次请求能揭示什么
机器人管理和边缘安全产品的工作原理是置于网站前端,检查每一个请求。为此,大多数产品会在访问者的浏览器上设置 cookie 或添加响应头,而这些名称是稳定的,通常也有文档记录:
| 供应商 | 典型证据 | 来源 |
|---|---|---|
| Cloudflare | 它提供的所有响应中都带有 cf-ray 头;开启 Bot Management 或 Bot Fight Mode 时会有 __cf_bm cookie;验证页面上会有 cf-mitigated: challenge | Cloudflare 的文档 |
| Akamai | Akamai-GRN 请求编号头;来自 Bot Manager 的 _abck、ak_bmsc、bm_sz cookie | 头信息来自 Akamai 的文档;cookie 来自各网站的 cookie 政策 |
| DataDome | datadome cookie;x-datadome 头 | DataDome 的文档 |
| HUMAN | _px3、_pxhd、_pxvid cookie | HUMAN 的文档 |
| Imperva | visid_incap_、incap_ses_、nlbi_ cookie | 各网站的 cookie 政策 |
| AWS WAF | 验证时带有 x-amzn-waf-action: challenge 及 HTTP 202;aws-waf-token cookie | AWS 文档 |
在解读结果时,有两点区别很重要。身处某个内容分发网络之后,并不等同于运行了机器人管理:cf-ray 头只说明该网站使用了 Cloudflare,而 __cf_bm cookie 则说明一个 Cloudflare 机器人产品正在对访问者进行主动评分。而且,没有找到特征签名并不能证明任何事情;许多网站运行着自己的检测系统,或者某个在首次响应中不留痕迹的产品。
代码
下面这个模块会读取一个响应,并返回其检测到的供应商及相应证据、是否有机器人管理产品处于活跃状态,以及该请求得到的结果:正常响应、验证或阻止。
import re
# Cookies that each vendor's bot or WAF product sets, and headers it adds. Cookies are the strongest evidence.
SIGNATURES = {
"Cloudflare": {"headers": ["cf-ray"], "cookies": [r"^__cf_bm$", r"^cf_clearance$"]},
"Akamai": {"headers": ["akamai-grn"], "cookies": [r"^_abck$", r"^ak_bmsc$", r"^bm_sz$", r"^bm_sv$"]},
"DataDome": {"headers": ["x-datadome"], "cookies": [r"^datadome$"]},
"HUMAN": {"headers": [], "cookies": [r"^_px(3|hd|vid|cvid|de)$"]},
"Imperva": {"headers": [], "cookies": [r"^visid_incap_\d+$", r"^incap_ses_", r"^nlbi_\d+$", r"^reese84$"]},
"AWS WAF": {"headers": ["x-amzn-waf-action"], "cookies": [r"^aws-waf-token$"]},
}
# Evidence of an active bot-management product, as opposed to only the CDN in front of the site.
BOT_PRODUCT_COOKIES = [r"^__cf_bm$", r"^cf_clearance$", r"^_abck$", r"^ak_bmsc$", r"^bm_sz$", r"^datadome$", r"^_px", r"^reese84$"]
def cookie_names(response):
"""Names of every cookie the response tried to set."""
return {c.split("=", 1)[0].strip() for c in response.raw.headers.getlist("Set-Cookie")}
def detect_stack(response):
"""Return {vendor: [evidence]} from one response's headers and cookies."""
headers = {k.lower() for k in response.headers}
cookies = cookie_names(response)
found = {}
for vendor, sig in SIGNATURES.items():
evidence = [f"header {h}" for h in sig["headers"] if h in headers]
evidence += [f"cookie {c}" for c in sorted(cookies) if any(re.match(p, c) for p in sig["cookies"])]
if evidence:
found[vendor] = evidence
server = response.headers.get("server", "").lower()
if server == "cloudflare":
found.setdefault("Cloudflare", []).append("server header")
if "akamaighost" in server:
found.setdefault("Akamai", []).append("server header")
return found
def bot_product_active(response):
"""True when a cookie shows a bot-management product, not just a CDN, handled the request."""
return any(re.match(p, c) for c in cookie_names(response) for p in BOT_PRODUCT_COOKIES)
def outcome(response):
"""Classify what a request got: served, challenged or blocked."""
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 or "incapsula incident id" in body:
return "blocked"
return "served"
它作用于 requests 库返回的响应,并从原始的 Set-Cookie 头中读取 cookie,因此能看到客户端本身不会保留的那些 cookie。请注意这里”正常响应”的含义:只是说没有发现验证或阻止的信号。它并不能证明页面包含了真实内容,这一差距我们会在下文回顾。
我们在前 1,000 个域名上的发现
2026 年 10 月 3 日,我们从罗马尼亚的单一连接向 Tranco 排名前 1,000 个域名各请求了一次主页,一次使用 Chrome 的 User-Agent 字符串,一次使用 requests 库的默认值。两次请求都来自同一个 Python HTTP 客户端,它不运行任何 JavaScript,也没有浏览器的网络指纹。653 个不同的网站返回了 HTML 主页;其余的是内容网络、API 主机以及其他没有主页的域名。
| 检测到的供应商 | 主页数量 | 带有活跃机器人管理 cookie 的数量 |
|---|---|---|
| Cloudflare | 162 (24.8%) | 79 |
| Akamai | 55 (8.4%) | 16 |
| AWS WAF | 14 (2.1%) | 0 |
| DataDome | 8 (1.2%) | 8 |
| HUMAN | 2 | 2 |
| Imperva | 2 | 0 |
| 以上任意一个 | 241 (36.9%) | 105 (16.1%) |
| 未检测到 | 412 (63.1%) | 0 |
只有两个主页同时显示出两个供应商。14 个 AWS WAF 网站中有 10 个是同一家零售商的不同国家店面网站。
请求得到的结果:
| 请求 | 正常响应 | 验证 | 阻止 |
|---|---|---|---|
| Chrome User-Agent,653 个主页 | 530 (81.2%) | 64 (9.8%) | 59 (9.0%) |
| 库默认 User-Agent,650 个主页 | 500 (76.9%) | 49 (7.5%) | 101 (15.5%) |
按照网站防护情况进行细分,统计两次请求均完成的网站:
| 检测到的防护 | 网站数 | Chrome User-Agent:验证或阻止 | 库默认:验证或阻止 |
|---|---|---|---|
| Cloudflare | 162 | 54 | 63 |
| Akamai | 54 | 26 | 22 |
| DataDome | 7 | 7 | 7 |
| 未检测到 | 411 | 19 | 53 |
结果说明了什么
顶级网站中的大多数会回应一个普通请求。 五分之四的主页在没有可见验证的情况下,正常响应了一个声称是 Chrome 的基本 HTTP 客户端。不过主页是一个网站上最容易访问的页面;搜索结果、价格和结账流程通常比首页防护得更严密。
简单过滤和成熟产品的行为方式不同。 在未检测到任何供应商的网站上,库的默认 User-Agent 遭遇验证或阻止的次数几乎是 Chrome 字符串的三倍,53 次对比 19 次。这正是简单的 User-Agent 过滤的特征。在运行 DataDome 的网站上,两种请求每次都遭遇了验证或阻止;User-Agent 没有造成任何差别。
身份不匹配可能比诚实身份更糟糕。 该零售商的十个国家店面网站,对声称是 Chrome 的请求返回了一到两千字节的验证页面或占位页面,而对诚实的库 User-Agent 则返回了完整的主页,大小在 678 KB 到 1.4 MB 之间。我们对全部十个网站重复了这一检查,结果一致。正如 TLS 与 HTTP/2 指纹识别中所解释的那样,在一个看起来不像 Chrome 的连接上出现 Chrome 的 User-Agent 字符串,这本身就是一个信号。
成功的状态码不等于成功的页面。 那些占位页面中有三个返回的是 HTTP 200,且没有验证头,因此仅检查状态码会将它们计为”正常响应”。应验证内容,而不仅仅是状态码,正如沉默失败率和如何判断网站是否提供了虚假或被阻止的内容中所述。
如何利用这个查询工具来规划项目
在你实际需要的页面上运行这个查询工具,而不仅仅是主页,并让结果来塑造你的计划:
| 查询工具显示的内容 | 通常意味着什么 | 合理的下一步 |
|---|---|---|
| 未检测到供应商,普通请求正常响应 | 该页面几乎没有机器人管理 | 使用诚实标明身份的普通 HTTP,适度的请求频率,以及正确的请求头 |
| 仅有 CDN,没有机器人 cookie | 边缘安全,但没有主动的机器人评分 | 使用普通 HTTP,但要留意随着请求量增加而出现的验证 |
| 有机器人管理 cookie,请求正常响应 | 主动评分系统放行了这次请求 | 预计在规模扩大后会出现验证;用目标健康度评分进行监控 |
| 第一次请求就遇到验证 | 这是一个有意过滤自动化客户端的决定 | 先寻找官方 API 或数据源,再考虑是否有必要使用浏览器,参见何时需要无头浏览器 |
| 第一次请求就被阻止 | 严格的规则,通常按网络或地区划分 | 检查数据是否可以通过其他方式获取,包括页面背后的 API,以及该阻止是否是区域性的 |
这个查询工具也能说明一些意图方面的问题。一个运行着主动机器人管理产品的网站,已经做出了控制自动化访问的决定,这一点值得与其服务条款和 robots.txt 一并权衡,正如robots.txt、AI 退出选项和预留信号中所讨论的那样。对许多项目而言,面对一个防护严密的目标,正确的应对方式是获取许可、建立合作关系或使用官方 API,而不是升级对抗手段。在采集行为确属适当的场合,针对受保护网站的升级阶梯按成本顺序解释了各种选项。
测量方法的局限性
- 仅限主页。 更深层的页面往往采用不同的防护方式。
- 每个网站仅一次请求,来自单一网络。 结果可能因国家、网络信誉、一天中的时间以及请求历史而异;我们在哪些国家最容易被地理封锁中讨论过地区差异。
- 没有使用浏览器。 许多验证机制是设计给运行 JavaScript 的真实浏览器通过的;我们的客户端在设计上就无法做到这一点。
- 特征签名会有遗漏。 内部自研的检测系统,以及那些在首次请求中不留痕迹的产品,都会被归为”未检测到”,因此受保护网站的真实比例要更高。
结论
一次普通的请求就能告诉你规划一个爬虫项目所需的大部分信息:谁在保护这个网站,机器人评分是否处于活跃状态,以及普通客户端会受到怎样的对待。在前 1,000 个域名中,大多数主页都做出了响应,四分之一处在 Cloudflare 背后,六分之一运行着主动的机器人管理产品,而最严格的产品对我们发送的每一个请求都进行了验证或阻止。
在编写爬虫之前,先在你所需要的页面上运行这个查询工具。让它来告诉你何时该保持简单,何时该为使用浏览器做好准备,以及何时该转而寻找获取数据的官方途径。
来源与参考资料
- Cloudflare,Cloudflare cookies 以及 检测验证页面响应。
- Akamai,Global request number。
- DataDome,Cookies and stored data。
- HUMAN,Use of cookies and web storage。
- AWS,CAPTCHA and Challenge in AWS WAF。
- Tranco,list Q2K34。
- Shifter 于 2026 年 10 月 3 日使用上述代码请求了各主页。