把单机Scrapy爬虫升级成分布式系统,核心在于让多个爬虫进程共享同一份待抓队列和已抓记录。Scrapy-Redis正是在这一诉求下诞生的组件,它重写了Scrapy原有的调度器与去重逻辑,将请求对象序列化后存入Redis,使任意节点都能生产或消费任务。这种结构不仅消除了单点瓶颈,还让扩容变得只是增加机器这么简单。

Scrapy-Redis的底层调度原理
在原生Scrapy中,调度器维护着一个内存中的请求队列,爬虫关闭后队列随之消失,且无法被其他进程访问。Scrapy-Redis通过替换SCHEDULER为scrapy_redis.scheduler.Scheduler,将队列操作指向Redis服务。每一个Request对象经过request_to_dict序列化后,以字节形式推入Redis的列表或有序集合,不同爬虫节点调用next_request时从同一处弹出,自然形成了分布式队列。
去重机制同样发生了改变。默认情况下,Scrapy使用内存中的集合保存指纹,而Scrapy-Redis改用Redis的set或zset存储请求指纹。每当有新请求进入,调度器先计算其指纹(通常是请求方法、URL和体的哈希),再尝试写入去重集合,若已存在则直接丢弃。由于Redis本身支持多客户端并发读写,即便几十个节点同时提交也不会产生重复抓取。
另一个关键点是阻塞与唤醒。当Redis队列为空时,Scrapy-Redis的调度器会让爬虫进程短暂休眠而非立即退出,这依靠redis.blpop的阻塞特性实现。只要其他节点后续写入了新任务,空闲节点便会被自动唤醒继续工作。这种机制保证了集群在任务不均匀时依然能高效利用资源,而不需要额外写监控脚本。
环境准备与项目基础改造
要使用该方案,首先得准备好可联网的Redis服务,以及一台或多台装有Python与Scrapy的机器。在每台爬虫机上执行pip install scrapy-redis即可获得核心依赖。Redis端建议设置密码并绑定内网地址,避免队列被外部恶意写入。如果是云上部署,记得在安全组放通相应端口。
接着修改Scrapy项目的配置文件。我们需要在settings.py里声明使用Redis调度器,并关掉原生去重。具体写法如下方代码所示,其中REDIS_URL指向统一的Redis实例,所有节点必须配置相同地址,否则就谈不上共享队列了。
# settings.py 核心配置 REDIS_URL = 'redis://:密码@192.168.0.1:6379/0' SCHEDULER = 'scrapy_redis.scheduler.Scheduler' DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter' # 允许爬虫结束后不清理Redis队列,方便断点续爬 SCHEDULER_PERSIST = True # 使用优先级队列(默认) SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
改完配置后,原本继承scrapy.Spider的爬虫需要改为继承scrapy_redis.spiders.RedisSpider。这种父类不再从start_urls读取起始链接,而是监听Redis中某个键,一旦有外部推入初始URL就启动解析。下面的示例展示了一个最简结构。
import scrapy
from scrapy_redis.spiders import RedisSpider
class NewsSpider(RedisSpider):
name = 'news'
redis_key = 'news:start_urls'
def parse(self, response):
# 假设页面中有文章链接
for href in response.css('a::attr(href)').getall():
yield response.follow(href, self.parse_article)
def parse_article(self, response):
yield {
'url': response.url,
'title': response.css('h1::text').get()
}
启动爬虫时,各节点直接运行scrapy crawl news,此时它们会处于等待状态。随后你在任一机器执行redis-cli lpush news:start_urls https://ipipp.com/list,所有等待中的爬虫就会争抢这个种子并展开抓取。这种解耦方式让种子注入与爬虫运行完全分离。
队列类型与调度策略选择
Scrapy-Redis并非只有一种队列,它内置了三种实现:PriorityQueue、FifoQueue和LifoQueue。优先级队列基于Redis有序集合,请求携带的priority数值越小越先处理,适合需要优先抓热点页的场景;先进先出队列用普通列表实现,严格按推送顺序消费,逻辑直观;后进先出则类似栈,常用于深度优先遍历。切换方式仅是修改SCHEDULER_QUEUE_CLASS配置。
在真实业务中,队列选择直接影响抓取行为。例如电商比价系统希望先抓首页再逐层深入,用FifoQueue更合理;而舆情监控想最快拿到最新发布的帖子,可给新请求赋更低优先级数值,配合PriorityQueue实现插队。需要注意的是,优先级队列因使用zset,在任务量极大时内存占用高于列表,需要结合Redis容量规划。
除了队列类型,还可借助Redis本身的能力做动态调度。比如写一个独立生产者脚本,根据数据库里未抓取的站点定时lpush种子,爬虫集群只管消费。若某节点宕机,由于SCHEDULER_PERSIST为True,Redis里的请求不会丢失,其他节点或重启后的节点仍能接着处理。相比自己用RabbitMQ搭一套,这种组合降低了运维复杂度。
常见坑点与性能优化建议
初学者常犯的错误是忘了关掉Scrapy自带的去重,导致RFPDupeFilter并未生效,重复请求依旧进入队列。务必确认DUPEFILTER_CLASS已替换,且各节点指向同一个Redis库。另外,若Redis部署在公网且未设密码,队列可能被刷入垃圾URL,因此内网隔离与鉴权是必做项。
性能方面,当爬虫节点超过一定数量,Redis容易成为瓶颈。此时可启用Redis主从加哨兵,或将不同业务队列拆分到不同db。在代码层,适当调大CONCURRENT_REQUESTS并配合本地DNS缓存,能减少单节点等待。下例展示了在中间件里做简单限速,防止对目标站造成过大压力而被封。
# middlewares.py 简单限速中间件
import time
class SlowDownMiddleware:
def __init__(self):
self.last = 0
def process_request(self, request, spider):
now = time.time()
if now - self.last < 0.5:
time.sleep(0.5 - (now - self.last))
self.last = time.time()
return None
最后提醒,分布式只是解决了并发与容量问题,并没有绕过反爬。多节点同时请求同一域名时,更要注意代理池和请求头随机化。可以把代理配置写进meta中,让Scrapy-Redis调度的每个请求自动携带不同出口IP,从而把集群的带宽优势真正发挥出来。
PythonScrapy-Redis分布式爬虫修改时间:2026-08-13 09:36:22