一个在前几个页面运行顺畅的爬虫,往往在大规模运行时崩溃。脚本在测试十个 URL 时运行得很干净,上线后,在第一万次请求之后的某个时刻,成功率悄悄下降:空的响应体、CAPTCHA 重定向、半填充的数据集,运行数小时后崩溃的 worker。代码没有任何变化。变化的是网站开始将你的流量视为一种模式,而不是一个访客。

本指南逐一介绍只在大规模下才会出现的失败模式,IP 声誉与封禁、CAPTCHA 墙、指纹和 TLS 检测、会话过期、选择器漂移、JavaScript 渲染内容、资源泄漏以及缺失的重试逻辑,并为每一种配对了具体的修复方案。读完之后,你将明白为什么相同的代码在十次请求和一万次请求时表现不同,以及这些问题中哪些值得自己解决,哪些值得交给托管层来处理。

为什么爬虫在大规模下会崩溃

在低请求量时,网站几乎没有理由关注你。你的少量请求混入普通的背景流量中,所以即使是一个粗糙的爬虫,裸请求头、单一 IP、没有节奏控制,也能顺利通过并给你带来虚假的自信。麻烦在于,反爬虫系统不会孤立地为单个请求打分。它们会分析一个会话内以及一个 IP 随时间的行为,而随着你的请求数量增长,这个行为档案只会越来越清晰。

超过几千次请求之后,几件事同时发生变化。你的流量模式在统计上与人类浏览行为显著不同,每 IP 阈值被触发,随着该地址记录更多自动化活动,IP 声誉下降,单次请求从未暴露的小不一致之处积累成一个自信的"这是机器人"判定。防御不是在第 10,000 次请求时才开启的;你只是越过了它们有足够信号采取行动的请求量门槛。以下修复方案都指向同一个方向:让每次请求看起来像真实浏览器,并让管道能够从现在已经确定会发生的失败中恢复。

相同的代码,大规模下不同的曲线。一个朴素的爬虫在请求量越过检测阈值之前保持较高的成功率,随后在封锁和静默失败中断崖式下跌。一个经过强化的爬虫,配备了轮换、节流和渲染,在相同的请求量下保持稳定的曲线。

失败模式及每种的修复方案

1. IP 速率限制与封禁

第一道墙是来自单一地址的请求量。网站统计每个 IP 的请求数,当某个来源看起来过于繁忙时就会采取行动:速率限制在某个时间窗口内上限请求数并开始返回 429,一旦某个地址越入"滥用"领域就会被直接列入黑名单。声誉会加剧这个问题。反爬虫系统追踪 IP 背后的 ASN、它是来自数据中心、住宅还是移动池,以及该地址段的历史行为,所以一个被标记的池会拖累其中的每一个地址。

解决方案。将请求分散到多个地址,使得没有单一 IP 呈现出可被封禁的特征。混合住宅和数据中心 IP 的轮换代理池可以分散负载、规避每 IP 速率限制,并通过不同地区路由来访问地理锁定的内容。然而,仅靠轮换并不是万全之策:如果你在保持相同机械化时序和请求头的同时加快轮换,只会更快地烧完地址。将轮换与下一节中的节奏控制结合起来。参见如何使用轮换代理了解设置方法。

2. CAPTCHA 墙

当网站怀疑存在自动化行为时,它会停止封锁,转而开始出验证:reCAPTCHA、hCaptcha、FunCaptcha 或点击拖拽拼图。在大规模下,这些验证不仅出现在登录时,还会出现在普通内容页面的中间爬取过程中,而遇到这种情况的爬虫要么直接停滞,要么更糟,跟随重定向后开始把验证页面当成数据来收集。

解决方案。持久的修复方案是从一开始就避免触发验证:表现得像真实浏览器,即真实的请求头、持久的 cookie、节奏化的请求和可信的 IP。事后解决 CAPTCHA 是一场必败的竞赛;预防才是胜利。当验证确实出现时,明确地检测它,将验证页面视为失败而非成功,并绕过它而非尝试解析。网络抓取中如何绕过 CAPTCHA涵盖了具体机制。

3. 指纹和 TLS 检测

现代检测远不止统计请求数量。反爬虫系统会分析请求本身:你的请求头的顺序和完整性、你的客户端产生的 TLS 握手(其 JA3 签名)、客户端提示,以及这些是否与你声称的 user agent 一致。一个通过 Python HTTP 客户端的 TLS 指纹发送 Chrome user agent 的爬虫,在自我矛盾,而这种矛盾是微不足道就能标记的。行为信号还会叠加,因为一个从不移动鼠标、从不加载二级资源、以固定节奏发送请求的会话,看起来是合成的。

解决方案。来自干净 IP 是不够的;请求必须从头到尾读起来像真实浏览器。发送完整、一致的请求头集合,在整个会话中持久化 cookie,永远不要组合出没有任何真实浏览器会产生的请求头和 TLS 组合。在所有属性上保持指纹一致是真正困难的,这正是检测器所利用的差距,所以这是将这部分卸载给一个能为你维护真实浏览器指纹的层次的最强理由之一。浏览器指纹识别解释了你面对的挑战。

长时间运行会引入短时测试从未触及的失败:会话变得过期。认证 cookie 过期、CSRF token 轮换,以及绑定到单一 IP 的会话状态在你中途轮换到新地址时就会断开。一个在百万页面任务开始时认证、并假设会话会持续的爬虫,在第二个小时就会开始收集到登录页面的重定向。

解决方案。有意识地管理会话。登录一次,持久化 cookie,并在每次请求中重用该会话,而不是每次请求都重新认证,但同时也要检测过期、监视登录重定向或丢失的 token,并在下一批开始之前刷新凭据。当某个流程将会话绑定到一个 IP 时,将该会话固定在单一的粘性地址上,而不是在其中轮换,使得网站在整个会话生命周期内看到一个一致的访客。

5. 由标记变化导致的选择器漂移

即使是无懈可击的爬虫,也会在目标网站重新设计的那一刻崩溃。网站会重命名 class、重构 DOM 并重排端点以改进自己的产品,而每一次这样的变化都可能悄悄地破坏你的解析器所依赖的选择器。在大规模下,这不是"如果"而是"何时",而且跨多个网站时这种情况持续不断地发生:昨天还在运行的脚本,今天开始返回空字段,且没有任何错误提示。

解决方案。防御性地解析。优先选择稳定的语义选择器和持久属性,而非任何重新设计都会破坏的脆弱、深层 CSS 路径。对每一次提取都进行验证,断言必填字段存在且类型正确,使得缺失字段触发告警,而不是将 null 写入你的数据集。保持解析器模块化,使得某个网站的变化只影响那一个解析器,而不是整个管道。

6. JavaScript 渲染内容

许多网站发送一个近乎空洞的 HTML 外壳,并在加载后用 JavaScript 绘制真实内容,通常来自后续的 API 调用。普通的 HTTP fetch 抓取到的是外壳,你的解析器什么也找不到,因为数据从未在你下载的源码中存在。这在大规模下产生了最令人困惑的失败:对一个功能上是空白的页面返回干净的 200 OK,所以你的爬虫报告成功,而你的数据集却在被空白填满。

解决方案。两条路径都有效。首先,打开浏览器网络标签,找到页面调用的内部 JSON API;直接访问该端点比渲染更快、稳定得多,而且许多"JavaScript 网站"都是覆盖在你可以直接查询的 API 之上的薄前端。当数据只有在渲染之后才可访问时,驱动无头浏览器或使用一个能为你渲染并返回完成的 HTML 的 API。无论哪种方式,在解析之前都要验证响应体,因为一个 200 响应带着 700 字节的内容和"Just a moment"的标题,是一个静默封锁,而非真实结果。参见如何爬取 JavaScript 网站

Crawlbase Crawling API

轮换、真实指纹和 JavaScript 渲染,正是在大规模下维护起来昂贵的层次,也是 Crawling API 吸收的内容。你发送一个 URL;它轮换 IP、呈现一致的浏览器指纹、可选地渲染页面、清除它能处理的验证、重试其余的,并返回干净的 HTML。一次调用取代了你否则需要构建和照管的代理池、CAPTCHA 处理和无头浏览器队列,使得大规模下的曲线保持平稳,而不是断崖式下跌。

7. 内存和连接泄漏

有些爬虫根本没有被封锁,而是被自身的重量压垮。一个每次请求都打开新连接而不使用连接池或关闭连接的循环,会耗尽文件描述符和套接字。在写入之前将每个响应都积累在内存中,会让进程不断膨胀直到被杀死。并发设置过高会在压垮目标之前先压垮你自己的机器。这些在十个 URL 的测试中都不会出现,因为泄漏需要数小时和数千次迭代才会变得致命。

解决方案。将资源视为有限的。重用池化的 HTTP 会话,而不是每次都打开新连接,并确保响应被消费和关闭,使套接字归还到池中。在数据收集过程中将结果流式写入存储,而不是将完整数据集保留在内存中。将每个主机和整体的并发数上限设置在你的机器和目标都能承受的水平。这些是普通的工程习惯,但在大规模下,它们是一个能运行数天的进程与一个在夜间死亡的进程之间的区别。

8. 没有重试或退避逻辑

在大规模下,瞬时失败不是边缘情况,而是常态。超时、断开的连接、偶尔的 429 或 503。没有重试逻辑的爬虫会丢弃这些行。立即且激进地重试的爬虫更糟,因为紧密的重试循环会在网站已经在对抗的那个时刻放大流量,从而加速封锁。这种"重试风暴"是爬虫自毁的最常见方式之一。

解决方案。重试,但要指数退避并加入抖动,使你的重试不会同步到来。上限尝试次数,遵守任何 Retry-After 响应头,并停止重试那些永远不会成功的状态码。一个小型包装器就足够了:

python
import random, time, requests

def fetch(url, attempts=5, base=1.0, cap=30.0):
    for n in range(attempts):
        r = requests.get(url, timeout=30)
        if r.status_code < 400:
            return r
        if r.status_code in (400, 404):
            break  # never going to succeed; do not retry
        delay = min(cap, base * 2 ** n) + random.uniform(0, base)
        time.sleep(delay)  # exponential backoff with jitter
    return None

同样的思路也适用于节流:在正常请求之间加入小幅抖动延迟,使即便是你成功的流量也不会以完全均匀的节拍到来,从而让检测器无法锁定。

卸载轮换和渲染

回顾这八个修复方案,会发现一个规律:其中最难的大多数与你的数据根本没有关系。轮换、指纹一致性、CAPTCHA 规避和渲染,是你在每个供应商的每个目标上维护的无差异基础设施,与真正为你创造价值的提取逻辑完全分开。自己构建这一切是可能的,但这是一项随着你添加的每个网站而增长的工程时间固定税。

这是卸载的自然时机。托管爬取层在单次请求背后承载着轮换、真实指纹、可选的 JavaScript 渲染、验证处理和智能重试,并返回干净的 HTML。你保留解析和业务逻辑,这些是真正属于你的,并让这个层次吸收那些只为了让请求通过而存在的部分。关于更广泛的问题目录和权衡,我们关于网络抓取挑战与解决方案的指南覆盖了更广的范围。

监控与告警

在大规模下伤害最深的失败模式,是没有人看到的那种。爬虫退化为返回空响应体和半填充数据集的 200 响应,这个缺口直到下游报告看起来有问题时才浮出水面,此时已经过了数天。修复方案是让沉默变得响亮。将爬虫作为一个活系统来监控:按域名跟踪成功率和失败率、封锁率和 CAPTCHA 率、响应体大小和吞吐量,使得 403 错误的持续上升或平均响应大小的突然下降在数分钟内触发告警,而不是在一次失败的运行结束后才发现。边运行边验证,并在一批次中某个必填字段缺失时告警,因为结构变化应该把你叫醒,而不是悄悄毒化数据。抓取的真实成本很少是第一次构建,而是随时间保持其诚实运行。

负责任地抓取

保持不被封锁的部分原因是克制。坚持抓取公开数据,即任何人无需账号即可看到的内容,并远离任何登录背后或识别个人的内容。阅读目标的 robots.txt 及其声明的速率预期,并将请求量保持在足够低的水平,使你不会对其服务器造成压力,因为抓取得过快确实可能降低甚至崩溃一个网站。GDPR 和 CCPA 等隐私法律约束了你对个人信息的收集权限,网站的服务条款可能明确禁止抓取,所以在大规模运行之前请检查两者。一个表现像好公民的爬虫,也是一个保持不被封锁时间更长的爬虫。

回顾

核心要点

  • 大规模是触发器,而非 bug。你的代码不是在第 10,000 次请求时崩溃的;网站终于有了足够的信号来分析你的流量,所以每个修复方案都是为了让请求看起来更像真实浏览器,并在现在已确定会发生的失败中存活下来。
  • 轮换与节奏控制要配合使用。混合住宅和数据中心 IP 的轮换池可以规避速率限制,但只有在配合抖动节流的情况下才有效,因为在机械化时序下更快地轮换只会更快地烧完地址。
  • 在检测上,一致性胜于聪明。请求头、cookie 和 TLS 必须与你声称使用的浏览器一致,而一个过期的会话或矛盾的指纹,正是让长时间运行被标记的原因。
  • 在信任 200 状态码之前先验证。静默失败,空响应体、验证页面和漂移的选择器,要通过防御性解析、字段验证和按域名监控来捕获,而不是靠侥幸。
  • 将无差异的层次卸载出去。轮换、指纹、渲染和重试是你可以租用的基础设施,使大规模下的曲线保持平稳,让你的团队专注于真正重要的提取和逻辑。

常见问题

为什么我的爬虫在测试时能运行,但在大规模下会失败?

早期测试不会产生足够的流量来触发网站的阈值,所以即使是粗糙的爬虫也能通过。一旦你持续运行大量请求,你的流量就变得容易被分析,而请求头、时序、指纹和会话行为上的小不一致之处积累成一个自信的机器人判定。代码没有变化;你只是越过了防御有足够信号采取行动的那个节点。

为什么我得到了 200 OK 响应,但数据却缺失了?

这通常是静默封锁或未渲染的内容。服务器返回有效状态码,但响应体是占位符、验证页面或空洞的 JavaScript 外壳,而不是真实内容。在解析之前先验证响应:检查响应体大小,并寻找"Just a moment"这样的标志性标题,使静默失败变成响亮的失败,而不是在你的数据集中写入 null。

轮换代理能单独解决速率限制问题吗?

不能单独解决。轮换分散了请求,使没有单一 IP 触发每 IP 限制,但如果你在整个池中保持相同的机械化时序和请求头集合,这个模式仍然可以被检测到,你只是更快地烧完了地址。将轮换与抖动节奏和真实、一致的请求配合使用,使每个地址看起来像普通访客。

如何处理重试才不会让封锁变得更糟?

使用指数退避和抖动进行重试,上限尝试次数,并遵守任何 Retry-After 响应头。立即、激进的重试会造成一场风暴,在网站已经在对抗的那个时刻放大流量,从而加速封锁。同时跳过对 404 等永远不会成功的状态码的重试。

何时应该渲染 JavaScript 而不是抓取原始 HTML?

当你需要的数据是在加载后由 JavaScript 绘制时,或者当网站依赖脚本来设置会话 cookie 或解锁真实 HTML 时,就应该渲染。在使用无头浏览器之前,先检查页面是否从你可以直接调用的内部 JSON API 加载数据,因为这样更快且稳定得多。当内容已经存在于源码中时,原始 fetch 就足够了。

什么时候值得卸载到托管爬取 API?

当维护轮换、指纹、渲染和验证处理的成本开始超过数据的价值时,或者当你在多个网站间扩展且无法持续为每个网站打补丁时。托管层在单次请求背后承载着这些基础设施,使你的团队专注于提取和业务逻辑,而不是让请求通过的无差异工作。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量