分布式采集:scrapy-redis 与任务队列
单机爬虫的瓶颈迟早会出现:带宽跑满、并发调高就被限流,而机器又有大量闲置时间。分布式采集的核心不是「多开几个进程」,而是把「待爬队列」和「去重集合」从进程内存搬到共享存储,让任意数量的 worker 安全消费同一份任务。
什么时候才需要分布式
| 信号 | 单机表现 | 分布式能解决吗 |
|---|---|---|
| 带宽跑满 | 下载队列长期为空 | 能,多机分摊出口带宽 |
| 被限流 | 403/429 集中出现 | 部分能,需配合代理池与全局限速 |
| 解析吃满 CPU | 下载空闲、解析阻塞 | 能,多机分摊解析 |
| 数据量只有几万条 | 几分钟跑完 | 不能,只会增加运维成本 |
scrapy-redis 的四块拼图
| 组件 | 配置项 | 作用 |
|---|---|---|
| 调度器 | SCHEDULER = "scrapy_redis.scheduler.Scheduler" | 请求队列存到 Redis,多 worker 共同消费 |
| 去重器 | DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" | 用 Redis 集合做全局指纹去重 |
| 待爬队列 | redis_key 指向的 list/set | 种子 URL 入口,运营侧 lpush 即启动 |
| 分布式 Pipeline | scrapy_redis.pipelines.RedisPipeline | item 写进 Redis 列表,供下游消费 |
默认队列类是 scrapy_redis.queue.PriorityQueue,请求放在 %(spider)s:requests 这个 zset 里,多个 worker 抢同一个 zset,所以重启不会丢掉整批任务。短板也很明确:请求被弹出后若进程崩溃,这条请求就丢了,scrapy-redis 不会自动回流,需要靠定期重投种子或额外任务表兜底。
配置与爬虫代码
# settings.py
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER_PERSIST = True # 队列与去重集合跨重启保留
REDIS_URL = "redis://127.0.0.1:6379/0"
ITEM_PIPELINES = {"scrapy_redis.pipelines.RedisPipeline": 800}
CONCURRENT_REQUESTS = 16
DOWNLOAD_DELAY = 0.5
AUTOTHROTTLE_ENABLED = True
# spiders/news.py
from scrapy_redis.spiders import RedisSpider
class NewsSpider(RedisSpider):
"""种子来自 Redis 列表,多个进程可同时消费"""
name = "news"
redis_key = "news:start_urls" # 运营侧往这里推 URL 即可启动
allowed_domains = ["example.com"]
def parse(self, response):
for href in response.css("a.title::attr(href)").getall():
yield response.follow(href, callback=self.parse_detail) # 去重交给 Redis
next_page = response.css("a.next::attr(href)").get()
if next_page:
yield response.follow(next_page)
def parse_detail(self, response):
yield {"url": response.url,
"title": response.css("h1::text").get(default="").strip()}
需要按规则批量发现链接时把基类换成 RedisCrawlSpider,它保留 Rule 与 LinkExtractor 的写法:rules = (Rule(LinkExtractor(allow=r"/news/\d+\.html"), callback="parse_detail", follow=True),),其余分布式能力一致。
多机部署步骤
# 1) 准备共享 Redis(内网、开密码、绑内网地址)
redis-server /etc/redis/redis.conf
# 2) 各 worker 拉同一份代码
pip install scrapy scrapy-redis redis
# 3) 每台机器启动任意数量进程,共用同一个 redis_key
scrapy crawl news -s LOG_LEVEL=INFO # worker 数量按带宽与限速定
redis-cli -h 10.0.0.10 lpush news:start_urls "https://example.com/news/" # 投放种子,放一次即可
redis-cli -h 10.0.0.10 zcard news:requests # 待爬请求数(PriorityQueue)
redis-cli -h 10.0.0.10 scard news:dupefilter # 已去重指纹数 ≈ 已调度 URL 数
redis-cli -h 10.0.0.10 llen news:items # 未被下游消费的 item 数
worker 不是越多越好:2 台 ×4 进程往往优于 1 台 ×8 进程,出口 IP 更分散;requests 长期不下降说明新链接产出少于消费速度,任务快结束了。
全局闸门:多机共享限速
多机并行最大的风险是「总 QPS 乘上机器数」。把限速计数器放进 Redis,所有 worker 共享一个配额:
# myproj/middlewares.py
import time, redis
class RedisRateLimitMiddleware:
"""所有 worker 共享同一 QPS 配额,避免并发叠加把目标站压垮"""
def __init__(self, client, qps):
self.client, self.qps = client, qps
@classmethod
def from_crawler(cls, crawler):
s = crawler.settings
return cls(redis.Redis.from_url(s.get("REDIS_URL")), s.getint("GLOBAL_QPS", 5))
def process_request(self, request, spider):
for _ in range(100): # 按秒开窗计数,超额则短暂等待
key = "ratelimit:%s:%d" % (spider.name, time.time())
count = self.client.incr(key)
if count == 1:
self.client.expire(key, 3)
if count <= self.qps:
return None
time.sleep(0.05)
return None # 兜底放行,防止死等
这段 sleep 会阻塞 Twisted 反应堆,生产上更稳妥的是改成返回 Deferred 的异步延迟,或直接用 Scrapy 的 AUTOTHROTTLE 加较小的 CONCURRENT_REQUESTS_PER_DOMAIN。
自建任务表方案与对比
| 维度 | scrapy-redis | 自建任务表 + 多 worker | Celery + 自研 fetcher |
|---|---|---|---|
| 改造量 | 小,改配置与基类 | 中,要写抢任务与超时回收 | 大,几乎全部自研 |
| 去重 | 内置全局指纹去重 | 自己用唯一键实现 | 自己实现 |
| 任务编排 | 弱,只有 URL 队列 | 强,可带优先级与业务字段 | 强,可编排 DAG |
| 适合场景 | 站点级整站采集 | 多源、多类型、要审计 | 已有 Celery 技术栈 |
CREATE TABLE crawl_task (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
url VARCHAR(1024) NOT NULL,
url_md5 CHAR(32) NOT NULL COMMENT 'URL 指纹,唯一键去重',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待爬 1进行中 2成功 3失败',
retry_times TINYINT NOT NULL DEFAULT 0,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_md5 (url_md5),
KEY idx_status (status, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
worker 用 UPDATE crawl_task SET status=1 WHERE status=0 ORDER BY id LIMIT 50 抢一批,配合 update_time 超时回收,就是可控可审计的分布式采集。
常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 多台机器重复采同一批 URL | 某台起的是普通 Spider | 确认基类是 RedisSpider 且 DUPEFILTER_CLASS 生效 |
| 重启后全部重爬 | SCHEDULER_PERSIST 为 False | 设为 True,并让 Redis 开启 RDB/AOF |
| 机器翻倍但速度没涨 | 瓶颈在目标站或代理带宽 | 看响应延迟与限速,先扩容率而不是机器 |
合规提示:分布式只是把压力分散,不代表可以放开抓取。只采集公开数据,遵守目标站 robots.txt 与服务条款,把总 QPS 控制在站点可承受范围;不采集手机号、身份证号等个人信息,不绕过登录、验证码与付费墙,优先使用官方 API 与开放数据集。数据使用需符合《个人信息保护法》《数据安全法》《著作权法》。
小结:分布式的本质是把队列与去重搬到 Redis,让 worker 无状态可横向扩展;用 scrapy-redis 四个配置项起步,多机部署重点做「全局限速 + 代理池 + 进度观测」,任务来源复杂、需要审计时再换成自建任务表方案。