抓取一百个页面的是脚本。抓取几百万个页面的是系统,而这两者的失败方式完全不同。在小规模任务中运行顺畅的代码,一旦面对真实的数据量,就会开始超时、被封锁或静默地丢失数据行。解决方案很少是更快的机器,而是围绕并发、轮换、重试和可观测性建立的一套习惯,让吞吐量保持高位而不触发封禁。
这是一份扩展网络抓取项目最佳实践的实战指南:如何控制请求频率、合理轮换代理、在反机器人防御中存活、重试而不放大故障,以及真正看清你的流水线在做什么。当托管层值得引入时,本指南会指向 Crawlbase,它将代理轮换、渲染和重试整合进单次调用,让你无需自己构建和照看那套基础设施。
为何扩展会让简单爬虫崩溃
在小规模上,抓取就是一个循环:请求一个页面,解析它,继续下一个。那个模型没有任何冗余余地。每个请求都阻塞在网络延迟上,一个瞬时错误就会让整次运行停摆,而目标站点会从单个 IP 看到一种稳定、机械的节奏。在一百个页面时这些都无所谓。在十万个页面时这些全都要紧。
扩展不是“把同样的事做得更多”。它是把事做得聪明,这意味着要围绕那些只有在负载之下才会显现的失败模式来设计:
- 速率限制和封锁。单个 IP 快速发射就会被限流、质询或封禁。
- 并发争用。工作线程太少,你要爬好几天;太多,你又会让目标或你自己的机器过载。
- 瞬时故障。超时、5xx 响应和断开的连接在大规模下持续不断,因此一个没有重试逻辑的运行永远跑不完。
- 内存和存储压力。在一次写入之前把所有东西都攥在内存里,扛不住数百万行数据。
- 可观测性盲区。当你看不到每个域名的成功率时,缓慢的退化看起来和“仍在运行”一模一样。
下面的实践逐一应对这些问题。它们大致按从请求层向外的顺序排列,但它们会叠加:没有速率控制的轮换照样会让你被封锁,而没有可观测性的重试只会掩盖腐烂。
控制并发和请求速率
第一个杠杆是你同时运行多少个请求,以及你发射得有多快。这是两个不同的旋钮,而人们常把它们混为一谈。并发是同时在途的请求数量;速率是你每秒启动多少个。你想要高并发以掩盖网络延迟,但又想要一个速率上限,这样你才不会把单个主机猛敲到封锁你。
使用异步模型,而不是每请求一线程的循环。异步 I/O 让一个进程在等待网络时保持数百个请求在途,而那正是同步爬虫几乎浪费掉全部时间的地方。用信号量给在途数量设上限,并给新请求设定节奏,让单个域名永远不会遭遇洪流。
import asyncio, aiohttp, random MAX_CONCURRENCY = 20 # in-flight requests at once PER_REQUEST_DELAY = 0.25 # seconds of jitter to spread load async def fetch(session, url, sem): async with sem: await asyncio.sleep(random.random() * PER_REQUEST_DELAY) async with session.get(url) as resp: return url, resp.status, await resp.text() async def crawl(urls): sem = asyncio.Semaphore(MAX_CONCURRENCY) async with aiohttp.ClientSession() as session: tasks = [fetch(session, u, sem) for u in urls] return await asyncio.gather(*tasks, return_exceptions=True)
按目标而非全局来调优 MAX_CONCURRENCY。一个健壮的公开 API 能承受数百个并发请求;一个脆弱的小站点在十个并发时就会倒下。每个请求上的抖动比人们预期的更重要:完全均匀的节奏本身就是一种机器人指纹,因此一点随机性能让你的流量看起来更像人类,并把负载从任何单一的某一秒里分散开。
用住宅与数据中心的混合来轮换代理
并发给你速度;代理轮换让你越过那些速度本会触发的速率限制。从一个 IP 发送每一个请求是大规模下最快被封锁的唯一方式。把请求分散到一个地址池上,意味着没有任何单个 IP 会呈现出可疑的模式。
两种代理类型在成本与可信度之间各有取舍。数据中心代理便宜又快,但容易被检测,因为它们的 IP 段已知属于托管服务商。住宅代理通过真实的消费者连接进行路由,因此目标会把它们读作普通访客,但它们成本更高。务实的做法是混用:对软目标依靠数据中心 IP,把住宅代理留给那些会反击的站点。
无论你用哪种,轮换都有它自己的规则。轮换要足够频繁,让没有任何 IP 积累起可被封锁的历史,但当一个站点把某条流程绑定到某个地址时(一条登录后的路径或一个多步表单),要把会话固定在一个 IP 上。监控代理健康状况,并剔除开始返回错误的地址。关于具体机制,如何使用轮换代理和轮换住宅代理深入讲解了配置。
构建和维护一个健康的池子是实打实的工作,因此这里是一个自然适合外包的地方。Crawlbase Smart AI Proxy 暴露一个单一端点,它在幕后轮换一个庞大的住宅和数据中心池,重试失败的 IP,并处理地理定向,于是你只需把现有的 HTTP 客户端指向一个代理 URL,而不必自己管理地址。
轮换 IP 能击败按 IP 计的速率限制,但它对浏览器指纹识别、TLS 签名或 JavaScript 质询毫无作用。一个对请求本身做画像的站点,即便你用一个全新的住宅 IP,照样会标记你。把轮换当作一个层,与真实的请求头、有节奏的请求以及(在需要时)真正的渲染搭配使用。
为反机器人韧性而构建
现代目标做的远不止统计每个 IP 的请求数。它们检查请求头、TLS 握手和浏览器指纹,并向看起来自动化的流量投放 CAPTCHA 或 JavaScript 质询。要扩展越过那些防御,意味着要看起来像一个真实的浏览器,而不仅仅是来自一个真实的 IP。
先打好基础:发送一套完整、一致的请求头(一个真实的 User-Agent、Accept-Language,一应俱全),在一个会话中保留 cookie,并且绝不发送任何真实浏览器不会发送的请求头组合。除此之外,那些重型质询(CAPTCHA、行为指纹识别、Cloudflare 式的过渡页)是一场你在大规模下通常不想徒手去打的军备竞赛。
这正是托管抓取层产生回报之处。Crawlbase Crawling API 为你处理整个反机器人栈:它轮换 IP,呈现真实的浏览器指纹,解决那些能解决的质询,并重试那些不能解决的,然后返回干净的 HTML。关于更全面的操作手册,如何抓取网站而不被封锁详细讲解了这些战术。
仅在必须时才使用无头渲染
无头浏览器(Puppeteer、Playwright、Selenium)能渲染普通 HTTP 抓取无法渲染的重 JavaScript 页面,但它们代价高昂:每个实例都是一个吃 CPU 和内存的完整浏览器,这限制了你能并行运行多少个,并拖慢每一个请求。在大规模下那个代价非常残酷,因此规则很简单:除非你必须,否则不要渲染。
在动用无头浏览器集群之前,先检查数据是否在不渲染的情况下就已经可得。打开网络选项卡,找一找页面所调用的内部 JSON API;直接命中那个端点比解析渲染后的 HTML 更快也稳定得多。许多“JavaScript 站点”其实只是一个 API 之上的薄前端,而那个 API 你可以直接查询。
当你确实需要渲染时,要有选择地做,而不是为整次爬取都运行浏览器。Crawling API 让你用一个 JavaScript token 按调用请求渲染,于是你只在需要的页面上付出浏览器代价,在其余所有地方都走廉价的静态路径。这就把那个昂贵的层限定在了真正需要它的少数页面上。
轮换、真实的指纹、可选的 JavaScript 渲染以及自动重试,全在一次调用中。你发送一个 URL 并拿回干净的 HTML,于是你省去了自己运行一个代理池和一支无头浏览器集群的麻烦。本页上的大多数实践都已内置。从免费套餐开始,把它指向一个真实的目标。
用指数退避和预算来重试
在大规模下,瞬时故障不是边缘情况,它们是稳态。超时、429、503 和断开的连接持续不断地发生,因此一个没有重试逻辑的爬虫永远跑不完一次大型运行。但简单粗暴的重试比没有重试更糟:在一个正挣扎的主机刚出错的瞬间就猛敲它,只会加深问题,并且看起来活脱脱就是一场攻击。
正确的模式是带抖动的指数退避,并对总尝试次数设上限。每次失败后等得更久,加入随机性让一波故障不至于步调一致地重试,并在有界的尝试次数后放弃,这样一个死掉的 URL 就不能永远阻塞流水线。只重试值得重试的:一个 503 或一个超时,可以;一个 404 或一个 403,不行,因为它们在下一次尝试时不会改变。
import time, random RETRYABLE = {429, 500, 502, 503, 504} def fetch_with_backoff(get, url, max_attempts=5, base=1.0, cap=30.0): for attempt in range(max_attempts): resp = get(url) if resp.status_code < 400: return resp if resp.status_code not in RETRYABLE: raise RuntimeError(f"non-retryable {resp.status_code}") sleep = min(cap, base * 2 ** attempt) + random.random() time.sleep(sleep) raise RuntimeError(f"gave up on {url}")
把重试与状态码素养搭配起来。一次开始返回质询或代理错误的运行,是在告诉你当前的速率或 IP 层级已经不够了;放慢并轮换,而不是盲目重试。把代理状态错误码当作信号来读,能让你去适应,而不只是猛敲。
把工作排入队列并异步处理
一个按顺序抓取、解析和写入的单一循环无法扩展,因为每个阶段都会阻塞下一个,而一个缓慢的步骤会拖住整个东西。真正能扩展的架构用一个队列把那些阶段解耦:生产者把 URL 推上去,一个工作线程池拉取并处理它们,队列吸收突发并分摊负载。
这一次性给你换来几样东西。工作线程横向扩展,因为你添加的机器全都从同一个队列拉取。失败的作业回到队列以待稍后重试,而不阻塞任何别的东西。而且队列就是你天然的速率控制点,你可以在那里节流每个域名分派作业的速度。Redis、RabbitMQ 或一个云队列都行;模式比工具更重要。
Crawlbase 把这作为一项托管服务提供。异步 Crawler 是一个基于推送的队列:你通过 Crawling API 提交 URL,每个都获得一个用于跟踪的请求 ID,系统并发地爬取它们并自动重试失败,然后把完成的结果 POST 到你服务器上的一个 webhook。你得到了队列、并发和重试机制,而无需搭建那套基础设施,而那恰恰是大多数团队耗费数周去构建的那一层。
积极缓存以避免冗余工作
最便宜的请求是你从不发送的那个。在大规模下,一次爬取中有出乎意料的一部分是冗余的:你已经抓过的页面、没有变化的内容、你在各次运行间重复的查询。缓存削减请求量,而这一下子就削减了成本、对目标的负载以及你的封锁风险。
在不止一个层级上缓存。对已经在一个新鲜度窗口内爬过的 URL 直接跳过,而不是重新抓取它们。尊重 HTTP 缓存头(ETag 和 Last-Modified),这样当没有任何变化时,一个条件请求会返回一个廉价的 304。并且对昂贵的派生工作(比如已解析或已规范化的记录)做记忆化,这样重新运行就不必再做一遍。一次每个周期都重新抓取未变化页面的爬取,正在把它大部分的预算浪费在它已经拥有的数据上。
监控一切并校验数据
在大规模下你无法用肉眼盯着一次运行,所以你必须给它装上仪表。重要的指标是每个域名的成功率和失败率、请求延迟、封锁和 CAPTCHA 率、队列深度,以及随时间变化的吞吐量。要点是及早抓住一次缓慢的退化:403 悄然上升意味着一个目标开始封锁你,而你想在几分钟内知道这件事,而不是在一次运行以一半数据行缺失收场之后。
校验是“它究竟有没有起作用”的另一半。一个返回 200 却带着空 body 或一个 CAPTCHA 页的请求是一次静默失败,而在大规模下这些会悄无声息地毒化你的数据集。所以要边走边校验:检查必需字段是否存在且类型正确,对取值范围做合理性检查,去重,并把干净的数据行直接流式写入存储,而不是把所有东西攥在内存里直到最后。在爬取时抓住坏数据,远比在下游报表中发现它便宜得多。
如果你跑在 Crawlbase 上,这其中很多都是白来的。仪表板呈现成功和失败计数,一个实时监视器显示实时活动和队列大小,一个重试监视器拆解正在被重试的内容,于是可观测性这一层是内置的,而不是你从零拼装出来的东西。对于结构化输出,Crawling API 为受支持的站点返回已解析的 JSON,这消除了一类脆弱的选择器代码以及随之而来的校验头疼。
尊重 robots.txt 和服务条款
负责任地扩展不仅是一个伦理问题,它也是一个运营问题。无视一个站点声明限制的激进抓取会让你更快被封锁,并且可能让你暴露于法律风险,因此克制是保持在线的一部分。
守住几条底线。只抓取公开数据,即任何人无需账户即可看到的内容,并且绝不抓取登录后的任何内容或任何能识别个人身份的东西。阅读目标的 robots.txt 及其声明的速率预期,并把你的量保持得足够低,以免给任何人的服务器造成压力。如果你计划将数据用于商业再利用,请获得许可或正式的数据协议,而不是假定沉默即同意。一个做好公民的爬虫,也是一个能更长久地保持不被封锁的爬虫。
核心要点
- 把并发与速率分开。用异步 I/O 让许多请求在途,但给你命中任何单个主机的速度设上限,并加入抖动,让你的流量不机械。
- 轮换一个住宅与数据中心的混合。把请求分散到一个健康的池上,对硬目标偏重住宅代理,并记住轮换本身并不能击败指纹识别。
- 仅在必须时才用无头渲染。先找一找内部 JSON API;把昂贵的浏览器路径留给那些确实需要它的页面。
- 用带上限的指数退避来重试。带抖动地退避,限定总尝试次数,并且只重试瞬时性的状态码,绝不重试 404 或 403。
- 排队、缓存并观测。用队列解耦各阶段,用缓存跳过冗余抓取,并给成功率和数据校验装上仪表,让静默失败迅速浮现。
- 让托管层承担无差别的工作。Crawlbase 把轮换、渲染、重试、排队和监控整合进单个 API,于是你扩展的是逻辑,而不是基础设施。
常见问题
扩展网络抓取项目的最佳实践有哪些?
核心实践是:用异步 I/O 并发地运行请求,同时给每个主机的速率设上限,在一个混合了住宅和数据中心 IP 的健康代理池上轮换,用带上限的指数退避重试瞬时故障,只在一个页面确实需要时才用无头浏览器渲染,用一个队列解耦各阶段,用缓存跳过冗余抓取,并给成功率加上数据校验装上仪表,让静默失败迅速浮现。一个像 Crawlbase Crawling API 这样的托管层,开箱即用地给你提供了其中几样。
扩展一个爬虫时我应该运行多少个并发请求?
没有单一的数字,因为它取决于目标。按站点调优并发:一个健壮的公开 API 能吸收数百个同时请求,而一个小型或脆弱的站点在十个并发时就会倒下。从保守开始,盯住错误率和封锁率,仅在成功率保持高位时才提高在途上限。给新请求加一点抖动来设定节奏,这样即便是高并发,在目标看来也不至于读作机械的。
大规模抓取该用住宅代理还是数据中心代理?
两者都用,与目标相匹配。数据中心代理便宜又快但容易被检测,因此它们适合那些不会强力反击的软目标。住宅代理通过真实的消费者连接进行路由并被读作普通访客,这使它们成为对付带激进反机器人防御站点的选择,代价更高。一个混合的池在让你越过硬目标的同时,把开销压下去。Crawlbase Smart AI Proxy 在一个端点背后为你管理这种混合。
在扩大规模时我如何避免被封锁?
封锁来自看起来自动化,而不只是来自量。把每个 IP 的速率保持低位并在许多地址间轮换,发送完整且一致的浏览器请求头,在一个会话内保留 cookie,并加入抖动让你的节奏不至于完全均匀。对于带 CAPTCHA 或指纹识别的站点,一个呈现真实指纹并解决质询的托管抓取 API,远比你自己造一套规避手段可靠。盯住你的状态码,在质询开始出现的瞬间就放慢。
我什么时候该用无头浏览器,什么时候该用普通 HTTP 请求?
仅当数据是在客户端渲染且无法以任何其他方式获取时,才动用无头浏览器。先在网络选项卡里查一查页面所调用的内部 JSON API;直接命中那个端点更快也稳定得多。无头浏览器吃 CPU 和内存,这限制了并行度并拖慢每一个请求,因此在大规模下你想把它们限定在真正需要渲染的少数页面上,而不是为整次爬取都运行。
Crawlbase 如何帮助我扩展一个网络抓取项目?
Crawlbase 去除了这些实践大多所需的基础设施。Crawling API 在单次调用中轮换 IP、呈现真实指纹、可选地渲染 JavaScript 并重试失败。Smart AI Proxy 在一个端点背后给你一个托管的轮换池。异步 Crawler 提供一个基于推送的队列,带并发、自动重试和 webhook 投递,外加用于成功率、实时活动和重试的仪表板。它们合在一起,让你专注于抓取逻辑,而不是自己构建和维护扩展层。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

