Scrapy分布式爬虫实战:从单机到千万级URL架构

发布时间:2026/10/6 4:31:43
Scrapy分布式爬虫实战:从单机到千万级URL架构 在做爬虫项目的时候很多人一开始是“写个脚本跑一跑”但等到数据量上来、目标站点一多、单机怎么也跑不完的时候就会意识到一个问题——单机爬虫的瓶颈不是代码写得好不好而是你只有一张网卡、一个 CPU、一块磁盘。我也经历过那个阶段一台 16 核的服务器上挂着几十个 Scrapy 爬虫内存吃满Redis 里塞了几亿条 URL跑两天就各种超时。后来痛定思痛把项目整体迁到了分布式架构上才真正体会到“多节点并行 统一调度”带来的效率提升。这篇就专门聊聊怎么用 Scrapy 框架搭建一套能扛住千万级 URL 的分布式爬虫从架构选型、核心组件拆解到动态 iframe 抓取、常见坑位排查全程是实战视角适合已经会用 Scrapy 写单机爬虫、但想往分布式方向进阶的同学。1. 为什么选择 Scrapy 做分布式爬虫1.1 分布式爬虫的本质需求从单机到集群先想清楚一个问题分布式爬虫到底要解决什么不是“用多台机器跑多个爬虫进程”这么简单。真正的需求是让 N 台机器上的爬虫进程像一个整体一样工作——共享任务队列、统一去重、协同抓取、集中存储结果。否则你只是在“多台机器上各跑各的”那是多实例不是分布式。单机 Scrapy 的调度器是把待抓取 URL 放在内存里的 deque 中去重用RFPDupeFilter基于 URL 指纹的集合这两个核心状态都只存在于本地进程。一旦进程退出队列和去重集合全部丢失。而分布式架构要做的就是把这两个“本地状态”外置到公共组件里队列放到 Redis去重指纹放到 Redis 的 Set这样每个节点从同一个队列取任务判重时查同一个集合互相之间自然就协调起来了。我在实际项目里最深刻的体会是分布式爬虫的难点不在“怎么写代码”而在“状态如何共享”。Scrapy 本身的设计是高度模块化的调度器、去重器、下载器中间件、扩展都能自由替换所以它天生适合被改造成分布式框架。你只需要把调度器换掉、去重器换掉再加上一个共享队列其余的数据管道、下载逻辑、中间件体系完全不用动这就是为什么社区最终选择了 Scrapy 作为分布式爬虫的基底。1.2 Scrapy 架构里天生适合分布式的部分Scrapy 给人的印象是“一个爬虫框架”但其实它的 Engine 机制非常像一个小型消息系统。核心流程是Spider 产生 Request → Engine 交给 Scheduler 排队 → Scheduler 出队后交给 Downloader 下载 → Response 回到 Spider 解析 → 解析结果要么生成新的 Request 回到队列要么交给 Item Pipeline 存储。这个流程里最关键的“胶水”就是 Scheduler 和 DupeFilter。在单机版里Scheduler 就是scrapy.core.scheduler.Scheduler它内部维护一个优先级队列并用DupeFilter判断 Request 是否重复。在 Scrapy 的默认实现中这个去重是基于请求指纹用请求的 URL、方法、请求体等信息哈希得到指纹。指纹计算本身是平台无关的所以任何节点计算同一个 URL 都会得到同样的指纹天然适合做分布式去重。另一个重要的点是 Scrapy 的Extensions机制。很多新手第一次看到scrapy.extensions目录会觉得很神秘其实扩展就是框架生命周期钩子信号的集合。比如scrapy.extensions.corestats.CoreStats会统计item_scraped_count、response_received_count等指标scrapy.extensions.logstats.LogStats会定期打印抓取速率scrapy.extensions.throttle.AutoThrottle可以根据响应延迟自动调节请求速度。在分布式场景里扩展的重要性被放大了——因为你需要跨节点观察整体运行状态而标准输出被多进程打乱的时候只有基于扩展收集的统计数据才是可靠的。我当时看了源码之后才理解想要把 Scrapy 变成分布式你不需要重写整个框架只需要实现一个自定义的 Scheduler 和一个自定义的 DupeFilter然后在 settings 里通过SCHEDULER和DUPEFILTER_CLASS两个配置项替换掉默认类即可。这就是整篇博客的核心思路接下来我会具体展开。2. 分布式方案选型与核心组件拆解2.1 常见方案对比Scrapy-Redis 与自研调度目前做 Scrapy 分布式最成熟的方案就是scrapy-redis这个库。它的做法非常直白把 Scheduler 改成从 Redis 的 List 或 ZSet 里取 URL把 DupeFilter 改成向 Redis 的 Set 里写入 URL 指纹。这个方案最大的优点是“现成、稳定、社区案例多”你只要安装后改一行 settings 就能跑起来。但它也有明显的短板所有节点共享一个 Redis 实例当 URL 量达到几千万、同时有几十个节点高频读写时Redis 的网络 IO 和内存占用会成为瓶颈。除了 Redis 方案也有人会选择用消息队列比如 RabbitMQ、Kafka做任务分发再用独立的服务做去重和状态管理。这种方案更灵活吞吐量可以做得更高但开发成本也大。我之前为了做定时全量抓取就自己在一个项目里用 Kafka 做任务队列把每个 URL 的关键信息作为消息体consumer 端再根据任务类型构造 Request。这个方案的优点是天然支持任务回溯、分区分组缺点是 Scrapy 的延迟重试、调度优先级这些机制需要自己重新实现工作量不小。到底选哪一种取决于你的规模和团队能力。下表是我做过的一个简单对比对比维度scrapy-redis自研消息队列 去重服务上手成本极低改两三个配置即可高需要设计队列协议和消费逻辑吞吐量中等受限于单 Redis 实例高可横向扩展 MQ 集群去重方案直接复用 Redis Set简单可靠需要自建布隆过滤器或 Redis Cluster优先级调度支持但体验一般可完全自定义延迟重试机制需额外封装可利用 MQ 延迟队列社区支持非常成熟靠自己踩坑对于绝大多数中小型项目我建议第一版直接用 scrapy-redis跑通架构后再按需优化。没有必要一开始就上 Kafka否则光调参和排错就能耗掉你一周。2.2 核心细节任务队列、指纹去重、状态同步分布式爬虫有三个核心细节决定了系统能不能长期稳定运行分别是任务队列、指纹去重和状态同步。任务队列的第一个问题是“从队列里取 URL 之后如果节点挂了怎么办”。scrapy-redis 默认从 List 左边pop一旦一个节点取走了 URL还没来得及请求就宕机这个 URL 无论如何都会丢失。要解决这个问题可以使用 Redis 的BRPOPLPUSH操作从主队列取任务的同时把任务备份到一个临时队列只有任务成功完成后才从临时队列删除如果节点崩溃备份队列里的任务还可以被重新放回主队列。scrapy-redis 最新版本已经提供类似机制但很多教程没提这里大家要记住。第二个核心细节是去重。用 Redis Set 直接存指纹数据量大了之后会非常占用内存。举个例子一个 URL 指纹是 40 位十六进制字符串也就是 40 字节存 1 亿条指纹大约要 4GB 内存而且 Redis 还要额外维护哈希表的开销实际占用可能翻倍。我后来做的优化是改用 Redis Bloom Filter布隆过滤器在允许极低误判率的情况下把内存降到原来的十分之一以下。具体做法就是维护一个 bit 数组和几个 hash 函数判断“某个指纹是否见过”时不再需要完整存储所有指纹。要注意布隆过滤器有误判概率会导致极少数 URL 被跳过但对爬虫来说漏抓一个两个可以通过后续的重试机制找补影响不大。再说状态同步。分布式环境下每个节点不仅消费任务还可能在运行中产生新的 URL比如解析列表页翻页、拼接详情页链接。新 URL 写入 Redis 队列之前必须先通过去重检查否则一个详情页可能被多个节点同时抓到。Scrapy 的调度器在enqueue_request时会先调用filter_request也就是去重过滤因此只要你的去重器是全局共享的新产生的 URL 就不会被重复分发。这里要特别注意如果你只改了调度器而忘了改去重器那么每个节点还在用自己的本地去重集合那等于白做分布式。2.3 Scrapy 扩展Extensions机制到底是什么在分布式爬虫项目里要监控各个节点的状态离不开 Scrapy 的扩展机制。我第一次接触 Extensions 时也一头雾水总觉得它是给框架“加功能”的神秘入口后来读了源码才明白Scrapy 是一个基于事件信号驱动的框架Extensions不过是在特定信号触发时执行特定逻辑的类而已。Scrapy 内置了大量扩展比如CoreStats监听engine_started、item_scraped、spider_closed等信号维护一个 stats 字典LogStats每隔一段时间打印一次抓取进度CloseSpider可以在 item 数量达到阈值时自动停止爬虫。你也可以根据自己的需求写一个扩展在from_crawler类方法中通过crawler.signals.connect连接到某个信号然后在回调里处理自定义逻辑。我在分布式项目里最常用的一个扩展是“心跳上报器”每个节点每隔 10 秒把自己当前的爬虫名称、进程 ID、待处理请求数、抓取速度上报到 Redis 或监控系统。这样运维人员打开一个面板就能看到整个集群里每个节点是否存活、负载是否均衡。另外我还用扩展做了“爬虫健康检查”如果某个节点连续 5 分钟没有抓到任何数据就自动发送告警。这个逻辑如果写在 spider 内部会很臃肿写成扩展就是最合理的方式。所以想要进阶 Scrapy建议把官方文档里 “Extensions” 那一节多看两遍尤其是信号列表。理解了信号和扩展之后你会发现自己能做的事突然多出一大截。3. 实操从零搭建一个 Scrapy 分布式爬虫3.1 环境准备与基础项目结构先准备环境。依赖包括 Python 3.8 以上、Scrapy 2.x、scrapy-redis 2.x以及一个可用的 Redis 实例本地或远程都行。强烈建议用虚拟环境避免污染系统 Python。安装命令如下pip install scrapy scrapy-redis # 如果后续要集成本文提到的动态页面抓取还需要额外安装 pip install playwright scrapy-playwright playwright install chromium然后创建项目。我习惯把项目结构设计成下面这样distributed_crawler/ ├── scrapy.cfg ├── requirements.txt └── distributed_crawler/ ├── __init__.py ├── settings.py # 分布式配置都在这里 ├── spiders/ │ ├── __init__.py │ └── news_spider.py # 示例爬虫 ├── middlewares.py # 下载中间件 ├── pipelines.py # 数据管道 ├── extensions.py # 自定义扩展心跳上报等 └── items.pySpider 的写法跟单机基本一样唯一要注意的是spider 中的起始请求不再是直接start_urls列表而是要从 Redis 队列里读取。在 scrapy-redis 中你只需要让 Spider 继承RedisSpider然后指定redis_key这样爬虫启动时会去 Redis 的这个 key 对应的 List 里获取起始 URL。下面是一个极简示例import scrapy from scrapy_redis.spiders import RedisSpider class NewsSpider(RedisSpider): name news redis_key news:start_urls def parse(self, response): # 解析逻辑... yield { url: response.url, title: response.css(h1::text).get(), }启动前你需要先向 Redis 的news:start_urls里推入一个种子 URLredis-cli lpush news:start_urls https://example.com/news/1然后直接用scrapy crawl news启动。只要 Redis 里有任务爬虫就会持续运行队列空了爬虫也不会立即退出而是进入等待状态。3.2 改造为分布式配置 Redis 连接与共享队列有了基础项目下面就是关键的改造步骤。打开settings.py添加以下配置# 使用 scrapy-redis 的调度器 SCHEDULER scrapy_redis.scheduler.Scheduler # 使用 scrapy-redis 的去重过滤器 DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter # 请求队列使用 Redis 的 List 类型先进先出 SCHEDULER_QUEUE_CLASS scrapy_redis.queue.FifoQueue # 是否持久化调度队列和去重集合 SCHEDULER_PERSIST True # Redis 连接配置 REDIS_URL redis://:password10.0.0.5:6379/0这里重点解释两个容易被忽略的配置项SCHEDULER_PERSIST设置为True表示在爬虫结束后不清空 Redis 中的队列和去重集合。这对于分布式场景非常重要。比如你启动了两个节点其中一个节点因为网络问题提前退出此时队列里的任务不应该被清掉否则另一个节点会漏抓。我见过很多新手因为这个配置没设导致每跑一次任务就全部丢失所以务必记住。FifoQueue是先进先出队列适合绝大多数场景。如果你有优先级需求可以换成PriorityQueue它会根据 Request 对象里设置的 priority 字段在 Redis 的 ZSet 里进行排序这样你能让详情页请求优先于列表页请求提高抓取效率。完成以上配置后你就可以在两台机器上同时scrapy crawl news它们会从同一个 Redis 队列里取任务抓到的数据各自通过 Pipeline 写入同一个存储。为了验证分布式是否真的生效我通常会在两个节点分别打印spider_id通过kw参数传进去然后在日志里确认两个节点抓到了不同的 URL。如果你要抓取的数据量很大还需要考虑一个问题Redis 的单线程模型对大量小请求的吞吐能力很强但延迟受带宽影响。我曾在一个项目中四台机器同时跑 32 个爬虫进程QPS 超过 3000结果 Redis 的INFO commandstats显示lpop和sadd占比最高。这时候可以把 Redis 放在内网避免公网带来的延迟如果还顶不住再用 Sentinel 或 Cluster 方案。3.3 应对动态页面Scrapy 中集成 Playwright 处理 iframe往年做爬虫最头疼的除了分布式还有动态渲染页面。很多网站的数据不是写死在 HTML 里而是通过 JavaScript 请求接口后渲染甚至放在多层 iframe 中。Scrapy 默认的scrapy.http.HtmlResponse是拿不到动态内容的因为 HTML 文档里根本没有这些数据。这个场景下就要引入浏览器自动化工具。我的推荐方案是scrapy-playwright它把 Playwright 集成到 Scrapy 的下载器里让每个 Request 可以选择走普通 HTTP 下载还是走无头浏览器渲染。这样你不需要把整个爬虫推倒重写只需要在 Requests 上打个标记。先启用 Playwright 中间件DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactor PLAYWRIGHT_BROWSER_TYPE chromium PLAYWRIGHT_LAUNCH_OPTIONS { headless: True, args: [--no-sandbox], }然后在 spider 中对需要动态渲染的 URL 添加meta信息def start_requests(self): for url in [https://example.com/price_info]: yield scrapy.Request( url, meta{ playwright: True, playwright_include_page: True, }, callbackself.parse_dynamic ) async def parse_dynamic(self, response): page response.meta[playwright_page] # 如果数据在 iframe 中需要先等待 iframe 加载完成 try: frame page.frames[-1] # 简单示例取最后一个 frame await frame.wait_for_selector(table#price-table, timeout5000) content await frame.content() # 也可以直接用 frame 里的选择器取文本 price await frame.locator(#price-value).text_content() finally: await page.close() yield {price: price}注意Scrapy 的playwright_include_page设置为True时response.meta里会有一个 Playwright 的 Page 对象。你必须在使用完毕后关闭它否则会造成浏览器标签页泄漏最终耗尽内存。我在生产环境里遇到过一晚上爬虫所有节点全部 OOM查到最后就是忘了关闭 Page 造成的。对于 iframe 的处理核心逻辑是先识别 iframe 的加载顺序。一般页面会有多个 frame包括主 frame 以及嵌套的iframe。你可以在 Playwright 控制台里先输出page.frames看看目标数据在哪个 frame 里找到后直接对这个 frame 进行操作。如果遇到跨域 iframePlaywright 也能访问因为它底层是真实浏览器不受同源策略限制——这是它比单纯用 requests 模拟请求再解析 HTML 强得多的原因。4. 常见问题与排查技巧实录4.1 去重失效与重复抓取分布式爬虫最常见的怪现象就是“明明配置了去重但还是抓了一堆重复页面”。排查这类问题的顺序通常是先看DUPEFILTER_CLASS是否生效再看 Redis 里dupefilter对应的 key 是否存在最后看指纹计算规则是否一致。如果你用的是 scrapy-redis默认去重 key 的格式是spider_name:dupefilter比如news:dupefilter。你可以在 Redis 里执行redis-cli scard news:dupefilter如果这个集合的大小一直增长说明去重过滤在正常写指纹。如果大小始终为 0那说明请求根本没经过这个过滤器。出现这种情况时多半是你在某些 request 上设置了dont_filterTrue强制跳过了去重。在分布式爬虫里除非有明确理由否则不要随意设置dont_filterTrue这会直接破坏全局去重。还有一种隐蔽情况你会遇到同一个 URL 因为 URL 参数顺序不同而被当成不同指纹。例如https://example.com/?b2a1和https://example.com/?a1b2在 human 看来是同一个页面但在 Scrapy 指纹算法里是不同的 URL。解决办法是自定义指纹计算类在请求进入调度器之前用urlparse解析并规范参数顺序。官方默认的指纹类是scrapy.utils.request.request_fingerprint你可以重写一个类并在DUPEFILTER_CLASS中引用。4.2 爬虫进程崩溃与任务丢失分布式环境下单节点崩溃几乎是必然事件所以设计时就要接受“节点会挂”这一事实。如果你发现一个节点停机后它取走的那部分任务永远消失了说明你的队列缺失了“备份”机制。要确保任务不丢可以换用scheduler_queue_class为scrapy_redis.queue.LifoQueue或FifoQueue都无法解决取走即删的问题你需要用 Redis 的brpoplpush模式。scrapy-redis 官方其实提供了一个queue_base但自定义程度不够。我的做法是在自己的 Scheduler 里重写enqueue_request和next_request逻辑将任务 pop 出来之后立刻放入一个processing_queue并设置一个较短的过期时间比如 10 分钟。任务正常完成后从processing_queue里移除如果节点崩溃processing_queue里的任务会在过期后重新回到主队列。注意这个逻辑需要依赖 Redis 的 Lua 脚本才能保证原子操作不建议用“先 pop 再写入”的 Python 语句分步执行否则并发下会出现任务重复分发。此外为了防止任务丢失我还习惯让每个节点在关闭前通过spider_closed信号把自己的当前状态上报到 Redis。这样即使队列真的丢了几个 URL我们也能从日志里比对每个节点“取了多少任务、完成多少任务、失败多少任务”找到具体的损失范围。4.3 反爬封锁与请求频率控制分布式解决了并发问题但同时也把“反爬封锁”从单机变成了集群层面的问题。如果你的 10 个节点都用同一个 IP 去抓同一个网站那基本等于自杀式攻击封号 ID 或 IP 的速度远比你抓取速度快。应对策略有几个层次。最基础的是控制单节点 QPS在settings.py中设置DOWNLOAD_DELAY 1.0 CONCURRENT_REQUESTS_PER_DOMAIN 2 CONCURRENT_REQUESTS_PER_IP 2 AUTOTHROTTLE_ENABLED True同时开启 AutoThrottle 扩展它会根据服务器响应时间和抓取成功率动态调整请求速度。这个方法能有效降低被封风险但代价是抓取速度不可控不适合对时效性要求高的场景。再进阶一点就是代理池。分布式爬虫一定要设计多 IP 出口否则无论你怎么调频一个 IP 的请求频率一高就会被封。我常用的做法是让所有请求都走一个代理中间件中间件从 Redis 的代理池里随机取一个代理 IP打上meta[proxy]。同时要处理代理失效的场景当请求因为代理问题返回 407 或连接超时要在下载中间件的process_exception里将失败的代理 IP 从 Redis 集合中移除并重试请求。注意重试次数不要太多一般 2 到 3 次就足够否则死锁问题会放大。对于验证码问题如果页面频繁出现验证码我建议“先绕后解”先做指纹隔离和请求伪装减少触发次数。真正需要解验证码时再接打码平台不要一开始就配置打码否则成本高且效率低。4.4 监控与运维统计 QPS、抓取成功率一个分布式爬虫跑起来很简单但要让它在生产环境稳定运行 24 小时监控是必不可少的。Scrapy 自带的CoreStats扩展里已经统计了response_received_count、item_scraped_count、downloader/exception_count等数据但如果只靠日志多个节点看不过来。我的做法是写一个自定义扩展定时把统计数据上报到 Redis 或者 Graphite。这里给一个简单的心跳扩展示例import time from scrapy import signals from scrapy.exceptions import NotConfigured class HeartbeatExtension: def __init__(self, stats, interval10): self.stats stats self.interval interval self.node_id node_ str(time.time()) classmethod def from_crawler(cls, crawler): if not crawler.settings.getbool(HEARTBEAT_ENABLED): raise NotConfigured ext cls(crawler.stats, crawler.settings.getint(HEARTBEAT_INTERVAL)) crawler.signals.connect(ext.spider_opened, signalsignals.spider_opened) crawler.signals.connect(ext.spider_closed, signalsignals.spider_closed) return ext def spider_opened(self, spider): self.task spider.crawler.reactor.callLater(self.interval, self.ping, spider) def ping(self, spider): spider.crawler.engine.pause() # 这里实际写 Redis self.stats.set_value(node_id, self.node_id) self.stats.set_value(pending_in_queue, spider.crawler.engine.scheduler.get_queue_len()) # 恢复 engine spider.crawler.engine.unpause() self.task spider.crawler.reactor.callLater(self.interval, self.ping, spider)注意上面代码里我使用了engine.pause()和engine.unpause()。为什么这么写因为获取scheduler.get_queue_len()时如果 engine 正在处理请求队列长度可能变化为了读数稳定稍微暂停一下是一个简单粗暴的兜底办法。实际上你完全可以通过 Redis 的llen命令直接获取队列长度不依赖 scheduler 内部状态这样更省事。除了这套扩展我还建议在每个节点上部署一个简单的进程守护用 systemd 或 supervisor确保进程退出后能自动拉起。我曾用 supervisor 配置了autorestarttrue并设置了startsecs5每次节点异常退出后 5 秒内就会重新启动基本可以做到无人值守。5. 项目落地后的经验与优化方向到这里核心的分布式爬虫已经能跑起来了。但跑起来只是第一步下面我分享几个在生产环境中反复踩过之后的优化心得你可以当作 checklist 来用。第一数据管道一定要做幂等。分布式环境下即使你去重做得再好也无法 100% 避免重复写入比如网络异常导致任务重发。因此在 Pipeline 写入数据库时尽量用INSERT ON CONFLICT DO NOTHINGPostgreSQL或replace intoMySQL等幂等操作避免重复数据污染。第二避免使用过大的 Item 和过重的回调。每个 Response 在多个节点之间不会有交互但如果你的 Parse 回调里做了耗时的图片下载、OCR 识别等操作会拖慢整个节点的抓取速度。建议把这类操作放到后续的独立消费服务中从 Item Pipeline 写进消息队列再由下游 worker 异步处理。第三定期清理 Redis 中的去重集合。Bloom Filter 也好Redis Set 也好长时间运行后会非常大影响读写性能。可以写一个定时任务每天凌晨将去重集合导出备份后清空并重新从数据库中已抓取 URL 重建指纹。这样能有效控制内存。第四分布式爬虫的前期设计比后期维护重要得多。我见过太多项目一开始就用 Redis 硬扛等到每天几千万 URL 级别才想到要扩展结果所有爬虫都要改。如果你预判未来数据量会快速增长建议一开始就使用支持横向扩展的消息队列比如 Kafka 或 Pulsar同时把去重服务单独拆出来这样后续扩容会从容很多。第五关于动态页面和 iframe 的处理我建议“能不用浏览器就不要用”。Playwright 渲染虽然功能强大但开销极大一个无头浏览器实例要吃 200MB~500MB 内存如果分布式有 10 个节点、每个节点开 4 个浏览器那内存压力是灾难级的。优先分析网站的接口看看数据是不是从某个 JSON API 里拉取的如果是直接请求接口。只有接口加密严重或者必须在浏览器环境里才能拿到数据时再上 Playwright。我在以往项目里遇到过一个典型场景某个网站的数据放在一个嵌有多层 iframe 的页面里最内层 iframe 的地址隐藏在 JavaScript 变量里。当时我通过 Capture 页面发起的网络请求直接从 DevTools 的 Network 面板里找出了内层 iframe 的真实 URL然后绕过浏览器直接请求那个 URL成功拿到了数据。这个思路比“死磕 iframe 渲染”高效太多。最后再分享一个小技巧在分布式爬虫里给每个 Spider 里的 Request 设置合理的dont_filter和下载优先级对整个集群的抓取效率影响巨大。列表页的翻页请求优先级可以设高一些因为列表页是生产 URL 的关键详情页的请求优先级设低一些避免海量详情请求把列表请求堵在后面。这个优化虽然简单但在实测中能把整体抓全率提升好几个百分点。一台机器能抓的数据总量是有天花板的但分布式的潜力几乎是无限的。把 Scrapy 改造成分布式爬虫的过程中你不仅是在堆机器更是在重新理解“任务分发、去重、调度、监控”这一整套工程体系。希望这篇文章能帮你少走一些弯路如果你在实操中遇到本文没写到的问题欢迎带着具体现象来交流。