在网络历史的大部分时间里,爬虫只在门口被审视一次。服务器看一眼 User-Agent,或许再看一眼 IP,然后决定是否放这个请求进来。爬虫之后做了什么,无人过问,直到某个站点被压垮;即便到了那时,投诉也只是发往一个无人查看的滥用举报邮箱。
2026年10月的第三周,这一点变了,而且四天之内变了三次。10月13日,Google 在文档中说明,其 Google-Agent 抓取器会用 Web Bot Auth 为其中一部分请求签名。10月15日和16日,一个以 Roblox 邮箱地址自报身份的研究爬虫读取了 robots.txt,随即无视了它,从三个业余 3D 模型站点拉走了 145.5 GB。同样在10月16日,Cloudflare 宣布 Verified bots 从此会被持续评分,依据是它们是否遵守 robots.txt,以及是否会在站点返回以下状态码后放慢速度:429。
这三件事放在一起,就勾勒出了新规则。身份让你进门,行为决定你下个月是否还受欢迎。对任何运营爬虫的人来说,问题不再是“我能抓这个吗?”,而是“记录会说我做了什么?”
- Cloudflare 现在为每个 Verified bot 维护一份持续更新的评分,由两个信号构成:它的流量是否长期符合站点公布的
robots.txt,以及它在收到429之后是否会退避。评分将在 Radar 上公开,站点所有者将自行设定阈值。 - 网络也开始为行为定价:Pay Per Use 在 AI 产品使用某个页面时向发布者付费,Monetization Gateway 则用
402和一个价格来回应请求。无法证明自己身份的爬虫,没有资格参与。 - 一个能通过评分的爬虫需要四个习惯,没有一个是难事:在第一个请求之前先读
robots.txt,按每主机预算花费请求,把429当作一份契约,并且永远不要重新下载没有变化的页面。下面是一个把这四件事都做到的循环,以及它产生的追踪记录。
一周之内发生的三件事
先说那个爬虫。Open3DLab、SFMLab 和 SmutBase 是同一批人运营的三个小站点,托管用户制作的 3D 模型。10月26日,他们 公开了自己的日志:在10月15日和16日的 34 小时里,一个自称 trellis2-data-sourcing/1.0 (3D foundation model research; [redacted]@roblox.com) 的客户端从单个 AWS 地址发出了 26,429 个请求,读取了 18,926 个项目的页面,并从其中 712 个项目下载了 1,027 个文件。它抓取了 robots.txt,但没有遵守。其中 423 个项目仅授权非商业用途。站点在10月18日封锁它之后,它在19日又回来了,20日再次回来,并且,用站点的话说,“面对我们的封锁,它并没有减少下载尝试”。据 后来报道的一份声明,Roblox 表示是一名研究人员的智能体误抓了这些站点,该智能体已被停用。
这个爬虫没有任何隐藏。它的 User-Agent 里带着一个邮箱地址。它来自单个 IP。按 Verified bots 计划的标准,它在身份上是诚实的。但它的所作所为,恰恰是该计划现在要衡量和惩罚的行为,而且站点能用自己的日志证明这一点。这就是为什么这件事上了 Hacker News 的首页,而不是进了滥用举报邮箱。
Cloudflare 的公告发布于该爬虫主要一轮运行的次日,机制上着墨不多,意图上却很精确。“Verified bots 的持续问责” 描述了两个初始信号。第一个信号问的是,一个机器人观察到的流量是否长期与站点公布的偏好一致,并以 RFC 9309 作为解读 robots.txt 的参照。第二个信号问的是站点返回 429后会发生什么:“它是减速、暂停,还是继续以同样的节奏发送请求?”结果汇入一个“可解释的信任等级”,文章小心地把这个标签称为一段历史而非一纸判决,因为“孤立的违规”无足轻重,而“无视站点偏好的重复行为模式”才举足轻重。评分将在 Radar 上公开,站点所有者将可以选择 Verified 流量仍可通行的最低等级。
Google 的贡献更安静,从某些方面说也更具影响。它的 Web Bot Auth 页面于10月13日更新,称来自 Google-Agent的一部分请求(这是 Google 基础设施上托管的智能体背后的抓取器)已经签名,并以 https://agent.bot.goog的身份进行认证。该实现被标注为实验性。但 Google-Agent 页面 列在用户触发型抓取器的清单里,而这份清单的指引并没有变:“由于抓取是用户请求的,这些抓取器通常会忽略 robots.txt 规则。”也就是说,地球上最大的爬虫运营者,现在正在为一个按其自身文档并不读取那份文件的智能体签名,而其他所有人正在因那份文件被评分。这不是虚伪,这是即将到来的那场争论的形状。签名解决的是谁发出了请求,对请求是否受欢迎则只字不提。
信任等级究竟衡量什么
把规模说具体一些会有帮助。在10月19日至21日举行的 Cloudflare Connect 上,公司产品管理高级总监 Matthew Conroy 表示,上个季度 智能体在其网络上发出了超过 6000 亿次请求,几乎是 2025 年底数字的四倍,并且“自报身份的机器人发出的自动化请求不到十分之一”。Matthew Prince 在同一场会议上预测,智能体流量将在五年内达到人类流量的一千倍,并附带一句,他之前的预测全都过于保守。DataDome 于10月15日发布的 Q3 报告补充了一个细节,解释了为什么身份突然成了一种产品:在 Meta 的 Muse 智能体的流量样本中,99.3% 的请求来自云和 CDN 的 IP 段,其中 81.2% 来自 Cloudflare 自己的网段,而且这个智能体并不自报身份。IP 信誉,这个全行业依赖了二十年的工具,已经无法把智能体和它所运行的边缘网络区分开。
所以评分必须建立在行为之上,而行为必须在时间中被观察。这就是信任等级。把它当作工程规范而非政策文章来读,它有四个输入。
身份 是前提条件,而不是分数本身。Web Bot Auth 这套 Ed25519 签名方案,是我们在它 与 Pay Per Crawl 一同上线时介绍过的;它自 9 月起就是 IETF 工作组文档,并于10月29日修订为 draft-ietf-webbotauth-httpsig-protocol-01。OpenAI 和 AWS 已经签名;Google 在实验;Cloudflare 自己的 Web Search API 于10月2日上线,背后是 Ceramic.ai、Exa 和 Linkup,并指出三者“已承诺遵守 Cloudflare 的 verified bot 抓取标准”。没有稳定的身份,就没有可以附着历史的东西,这就是为什么未签名流量根本不被评分。它只是默认拒绝的那一类。
公布的偏好 是第一个信号,而词汇表仍在变动。RFC 9309 是基线:分组,以及 Allow 和 Disallow,最长匹配优先。在此之上是 Cloudflare 的 Content Signals,它承载的是用途而非路径:search,ai-input,ai-train,以及自8月31日起新增的第四个字段 use,取值为 reference 和 full。Cloudflare 自己的抓取端点现在会直接返回 400 拒绝请求,只要爬虫声明的 contentUse 比站点的设置更宽松。IETF 的并行努力,即 AI Preferences 词汇表,于9月14日推出草案 08,至今仍是一份互联网草案;工作组的 8 月里程碑过去了,标准没有出来。换句话说,评分在语法定稿之前就已经开始了。
对反馈的响应 是第二个信号,也是大多数爬虫在不知不觉中栽跟头的那个。429 不是一个可以重试的错误;它是一条指令,而当它带着 Retry-After 时,它就是一条带数字的指令。Cloudflare 的文章说得很直白:在 429 之后减速或暂停“会提高信任,而反复无视这个信号则会降低信任”。大多数重试库做的恰恰相反。它们稍等片刻就重试失败的请求,而把引发 429 的并发度原封不动地留在原地。
效率 不在初始评分里,但会进入。Cloudflare 7 月的新闻稿称“AI 爬虫超过 50% 的抓取流量花在了重新抓取未变化的页面上”,而它 与 OpenAI 的试点自 7 月起在其网络约 1% 的范围内运行,向爬虫提供页面变更信号,并过滤掉不存在或只是重定向的页面。文章报告了数亿个新发现的页面,还有一句值得记住的话:“关于某个页面的信号并不授予访问它的权限。”这些信号是为已经守规矩的爬虫构建的,它们不是一条进门的路。
这四者背后都是钱。Pay Per Use 自9月30日起进入测试,它在 AI 公司报告使用了某个页面时向发布者付费,而不是在抓取时付费,因为用 Cloudflare 的话说,“对每次抓取收费,等于让买家在知道自己需要什么之前先付钱”。同一天推出的 Monetization Gateway 则用 402 Payment Required 和一个价格来回应请求,并通过 x402 协议结算。两者都需要一个身份可验证、历史清白的买家。信任等级就是这个市场的信用评分。
站点在要求什么,今早的测量
这一切在实践中有多少已经落到了 robots.txt 文件里?10月30日,我们抓取了 robots.txt,来源是 Tranco 榜单前 2,000 个域名,用的是一个自报身份的研究 User-Agent,每个主机一个请求。1,130 个主机返回了可读的文件。其余的是无话可说的 API 和 CDN 主机、434 个完全没有应答的域名,以及 145 个对一个已自报家门的爬虫回以 403 的主机,这本身就是一个小发现。
| 指令 | 站点数 | 占 1,130 的比例 |
|---|---|---|
| 按 User-Agent 提及至少一个 AI 爬虫 | 381 | 33.7% |
| 完全禁止至少一个 AI 爬虫 | 292 | 25.8% |
完全禁止 GPTBot
|
208 | 18.4% |
设置了 Crawl-delay
|
143 | 12.7% |
带有 Content-Signal 行 |
71 | 6.3% |
发送 aipref 的 Content-Usage 头 |
0 | 0% |
有两个细节比标题更重要。第一,用途已经在被手工拆分:在 208 个完全封锁 GPTBot 的站点中,有 21 个提及了 OAI-SearchBot 并放行,有 18 个对 ChatGPT-User做了同样的事。运营者在用他们手头唯一的词汇说“索引我、引用我,但别拿我训练”,而这套词汇就是每个用途一个 User-Agent。第二,在 71 个带有 Content-Signal 行的站点中,有 39 个带着完全相同的 search=yes, ai-train=no, use=reference,这是 Cloudflare 的托管默认值。新词汇正在通过 CDN 到来,而不是通过站点所有者的编辑器,并且我们抓取的文件中有 35% 带着 cf-ray 头返回。这就是记分板变成规则的机制。
一个能过关的抓取循环
这四个习惯没有一个是困难的。困难的是在抓取进度落后、队列爆满的时候还能坚持,所以它们应该写进抓取器,而不是写进操作手册。下面是一个 226 行的参考循环,用标准库 Python 编写,摘录了最关键的四个部分,运行在一个有自己脾气的本地源站上:一个 robots.txt,它禁止 /private/;每个页面都带 ETag;以及返回 429 并附带 Retry-After: 2 给任何超过每秒两个请求的客户端。
在第一个请求之前先读规则
RFC 9309 规定,读取了 robots.txt 的爬虫必须遵守它,可以缓存它,但缓存副本的保留不应超过 24 小时。它也规定了文件读不到时该怎么办:4xx 表示没有规则,而 5xx 或网络故障则表示应当假定自己不被允许,稍后再试。下面的缓存按主机做的正是这件事,并暴露出 Crawl-delay 以便预算使用。
def _load(self, scheme: str, host: str) -> urllib.robotparser.RobotFileParser | None: url = f"{scheme}://{host}/robots.txt" req = urllib.request.Request(url, headers={"User-Agent": self.user_agent}) parser = urllib.robotparser.RobotFileParser(url) try: with urllib.request.urlopen(req, timeout=10) as resp: parser.parse(resp.read().decode("utf-8", "replace").splitlines()) return parser except urllib.error.HTTPError as e: if 400 <= e.code < 500: parser.parse([]) return parser return None except Exception: return None def allowed(self, url: str) -> bool: parts = urllib.parse.urlsplit(url) host = parts.netloc with self._lock: cached = self._cache.get(host) if cached is None or time.time() - cached[0] > ROBOTS_TTL: cached = (time.time(), self._load(parts.scheme, host)) self._cache[host] = cached parser = cached[1] return parser is not None and parser.can_fetch(self.user_agent, url)
我们第一次运行这个演示时,测试服务器还没有启动完成。每个 URL 都返回了 disallowed,包括那些规则本来会允许的 URL,因为循环读不到规则。这是正确的结果。一个搞不清站点想要什么的爬虫,不应该朝着对自己有利的方向去猜。
把 429 当作一份契约
每主机预算是一个令牌桶,这没什么稀奇。关键在于 429 到来时它做了什么。有 Retry-After 就遵守,没有就带抖动退避,把速率减半,并且,这是大多数实现跳过的一步,把上限降到 80%,基准是引发这次 429的速率。站点刚刚告诉了你它的极限。每隔几分钟就往回试探到这个极限,正是信任等级要抓的那种“重复模式”。一分钟内五次 429就会打开熔断器,这个主机会被放置五分钟,而这正是该有人去读日志的时候。
def on_429(self, retry_after: float | None) -> float: """Honour Retry-After when given, else back off exponentially with jitter.""" with self._lock: now = time.monotonic() self._recent_429 = [t for t in self._recent_429 if now - t < self.trip_window] + [now] # The rate that drew the 429 is the host's limit: cap recovery # below it so one 429 is a lesson, not a cycle. self._ceiling = max(self.rate * 0.8, 0.05) self.rate = max(self.rate / 2, 0.05) if len(self._recent_429) >= 5: self._open_until = now + self.cooldown return self.cooldown if retry_after is not None: pause = retry_after else: pause = min(60.0, 2 ** len(self._recent_429)) * random.uniform(0.5, 1.5) self._open_until = now + pause return pause
先问页面变了没有,再请求页面
AI 抓取流量有一半在重新抓取未变化的页面,这个数字 Cloudflare 有商业利益在里面,但它并非不可信,而且解决办法比问题早了二十五年。记住页面发来的验证器,把它们发回去,并接受 304 作为答案。下面的存储刻意写得很朴素;在生产环境中它是 URL 表里的一列。
class ConditionalStore: """Remember validators per URL so a re-crawl asks 'has this changed?' first.""" def __init__(self): self._validators: dict[str, dict[str, str]] = {} def headers_for(self, url: str) -> dict[str, str]: v = self._validators.get(url, {}) out = {} if "etag" in v: out["If-None-Match"] = v["etag"] if "last-modified" in v: out["If-Modified-Since"] = v["last-modified"] return out def remember(self, url: str, headers) -> None: v = {} if headers.get("ETag"): v["etag"] = headers["ETag"] if headers.get("Last-Modified"): v["last-modified"] = headers["Last-Modified"] if v: self._validators[url] = v
记下每一个决定
fetch 方法把这三者串在一起,并把每个结果记录为一行 JSON:请求了什么、规则怎么说、站点怎么答、循环等了多久、当时运行在什么速率。Open3DLab 公布日志时,最有杀伤力的一句是爬虫“面对我们的封锁,并没有减少下载尝试”。审计日志就是你提出相反主张并让人相信的方式。而且并非巧合,这也正是 EDPB 在今天结束征求意见的网页抓取指南中所要求的:可靠的来源、每条记录一个时间戳、使用前先验证。
def fetch(self, url: str) -> tuple[str, int, bytes | None]: """Return (outcome, status, body). Outcomes: fetched, unchanged, disallowed, throttled, error.""" if not self.robots.allowed(url): self._record(url=url, outcome="disallowed") return "disallowed", 0, None budget = self._budget(url) waited = budget.acquire() req = urllib.request.Request(url, headers={"User-Agent": self.user_agent, **self.store.headers_for(url)}) try: with urllib.request.urlopen(req, timeout=30) as resp: body = resp.read() self.store.remember(url, resp.headers) budget.on_success() self._record(url=url, outcome="fetched", status=resp.status, bytes=len(body), waited=round(waited, 2)) return "fetched", resp.status, body except urllib.error.HTTPError as e: if e.code == 304: budget.on_success() self._record(url=url, outcome="unchanged", status=304, waited=round(waited, 2)) return "unchanged", 304, None if e.code == 429: pause = budget.on_429(parse_retry_after(e.headers.get("Retry-After"))) self._record(url=url, outcome="throttled", status=429, retry_after=e.headers.get("Retry-After"), pause=round(pause, 2), rate=round(budget.rate, 3)) return "throttled", 429, None self._record(url=url, outcome="error", status=e.code) return "error", e.code, None
追踪记录长什么样
针对本地站点的两次运行。礼貌的那次以每秒一个请求抓取三个产品页面,有一个路径被规则拒绝,没有一个数据包离开机器,然后重新抓取这三个页面,得到三个 304。急躁的那次以每秒五个请求起步,而限制是每秒两个。
polite pass 1: ['fetched', 'fetched', 'fetched', 'disallowed'] polite pass 2: ['unchanged', 'unchanged', 'unchanged'] (4.0s for 7 fetches) impatient run: ..T..T..T............................... 3 x 429 in 40 tries, 33.2s, 1.11 pages/s achieved rate trajectory: [5.0, 2.76, 0.76, 0.92, 1.12, 1.22, 1.22, 1.22, 1.22, 1.22] {"url": ".../private/report", "outcome": "disallowed"} {"url": ".../products/1", "outcome": "unchanged", "status": 304, "waited": 1.0} {"url": ".../catalog/2", "outcome": "throttled", "status": 429, "retry_after": "2", "pause": 2.0, "rate": 2.5} {"url": ".../catalog/8", "outcome": "throttled", "status": 429, "retry_after": "2", "pause": 2.0, "rate": 0.76} {"url": ".../catalog/39", "outcome": "fetched", "status": 200, "bytes": 67, "waited": 0.82} naive run: ..T..T..T..T..T..T..T..T..T..T..T..T..T.
三个 429,每一个都按站点要求的两秒遵守,每一个都把速率减半并降低上限。第三个之后,连续 31 次抓取没有被限流,稳定在每秒 1.2 个请求,而限制是 2。常见的重试循环,也就是等完 Retry-After 然后以同样速率继续的那种,招来了 13 个 429,而这是在同样的 40 个请求里,平均每三个请求一个,并且每秒抓到的页面反而更少:0.94 对 1.11。一天下来,那就是某人信任历史里的数千条记录,每一条都是信任等级专为捕捉而建的那种重复模式。
- 它不给请求签名。Web Bot Auth 需要一对密钥、一个公开的 JWKS 目录和一个 HTTP Message Signatures 实现;之前那篇文章 讲了其中的机制。把它加在
fetch之前,而不是里面。 - 它不读取 Content Signals 或 aipref 词汇表,因为两者都还没有定型的解析器。等它们定型了,它们应该放进
RobotsCache,紧挨着crawl_delay。 - 它不决定你可以拿这些字节做什么。干净的信任等级只说明你抓取得很礼貌。许可证和 EDPB 的合法利益测试关乎的是使用,没有任何抓取器能替你解决。
如果你通过 Crawlbase 抓取,这改变了什么
Crawlbase 不会替你读 robots.txt,也不会假装去读。策略是你的,历史也是你的。而 Crawling API 做的,是让站点那一侧的对话可见,好让你的循环据此行动。每个响应都在 original_status 里带着目标站点自己的状态码,在 cb_status里带着 Crawlbase 的判定:回应 403 的站点会以 705返回,回应 429 的站点会以 706返回,两者都不计费,所以退避除了时间什么都不花。加上 get_headers=true 之后,站点的响应头也会一并返回,这正是 Retry-After,ETag 和 Last-Modified 所在的地方。上面的预算和验证器存储,用这些字段替换原始响应后可以原样工作。
在 Enterprise Crawler上,并发限制(默认 20)和推送速率(每个令牌每秒 30 个 URL)是每主机预算的上限,而不是它的替代品。把上面的循环作为决定什么进入队列的那一层来运行,让爬虫只负责抓取。
用一个 JSON 响应拿到任何页面,附带站点的真实状态码、Crawlbase 的判定和响应头,需要时在浏览器中渲染。封锁和限流永远不计费。从最多 5,000 次免费请求开始,无需信用卡。
未来一年的务实立场
有一种反方论点,而且带着数据。10月20日,一位在 Apify 上售卖抓取器的开发者 公布了遥测数据,涵盖他的抓取器的 169,941 次运行,发现通过 MCP 到来的调用,也就是来自 AI 智能体的调用,成功率为 86.9%,而控制台里的人类用户为 47.9%,并且其中只有 0.6% 触发了他的滥用门槛。“我把故事完全搞反了,”他写道。调用 API 的智能体是网络上行为最好的客户端,因为 API 用它们读得懂的形式告诉了它们规则。Roblox 的那个爬虫,则是在没有这种接口、只有一个它选择不解析的文本文件的站点上行为失当。
这就是整个论点的一个对比。网络并没有对爬虫关闭。它正在用一本账簿取代一扇门,而账簿只奖励一件事:可读、可查。能签名就给请求签名,不管同不同意都去读站点的偏好,让 429 来设定你的速率,发送验证器,并保留日志。做到这些,信任等级就是一项免费的、会复利增长的资产。跳过它们,你跑的每一次抓取都是对方手里的证据,而且时间戳是别人打的。
结论
2026年10月是爬虫行为开始有记录的月份。Cloudflare 开始依据 robots.txt 的一致性和 429 响应为 Verified bots 评分,Google 开始为一个跳过 robots.txt的智能体签名,而一个两样都没做的爬虫,带着自己的日志上了首页。评分还处在早期,词汇表仍是草案,最大的站点里也只有三分之一写过任何 AI 指令。但方向已经定了,它所要求的工程,不过是把二十年前的 HTTP 做得仔细一些:先读规则,按预算花费,服从 429,抓取前先验证,并把一切记下来。如果你运营爬虫,通过 Crawlbase 跑一遍上面的循环,在别人不得不替你读之前,先自己读一读它的日志。
常见问题(FAQ)
信任等级适用于不是 Verified bot 的爬虫吗?
不适用。评分附着在身份上,所以未签名、未验证的流量不会被评分;它和以前一样由站点的机器人规则处理。实际效果是一样的:没有身份的爬虫没有可以积累的历史,所以无法走出默认拒绝的那一类。
429 有没有可以立即重试的时候?
只有在响应明确这么说的时候,而它从来不会这么说。一个 429 如果带着 Retry-After,就给了你一个数字;遵守它。不带的那种,是在告诉你指数退避并降低稳态速率。用同样的并发度重试同一个请求,正是评分设计出来要抓的东西。
我必须遵守 Content Signals 和 aipref 词汇表吗?
两者都是偏好,不是访问控制,而且都还不是已发布的标准。Cloudflare 的抓取端点已经在强制执行 use 级别,而信任等级读取的是与站点“公布的偏好”的一致性,这个范围会扩展到包含它们。现在就读取它们不花任何成本,还能更早开始积累历史。
这和 Google、Bing 一直以来的做法有什么不同?
Googlebot 读 robots.txt 并在出错时退避已经二十年了,但这种行为从来没有被 Google 之外的任何人衡量过。变化在于,现在有一个第三方在数百万个站点上观察行为、公布结果,并让站点所有者在边缘规则里据此行动。
站点对我限流时,Crawlbase 收多少钱?
不收。回应 429 的目标站点会以 cb_status 706返回,回应 403 的会以 705返回,两者都不算成功请求,所以退避和稍后重试只花时间。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和验证码,让您的团队专注于构建数据管道,而不是维护爬取基础设施。免费提供最多 5,000 次请求,无需绑定信用卡。
