分布式采集: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 即启动
分布式 Pipelinescrapy_redis.pipelines.RedisPipelineitem 写进 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,它保留 RuleLinkExtractor 的写法: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自建任务表 + 多 workerCelery + 自研 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确认基类是 RedisSpiderDUPEFILTER_CLASS 生效
重启后全部重爬SCHEDULER_PERSIST 为 False设为 True,并让 Redis 开启 RDB/AOF
机器翻倍但速度没涨瓶颈在目标站或代理带宽看响应延迟与限速,先扩容率而不是机器

合规提示:分布式只是把压力分散,不代表可以放开抓取。只采集公开数据,遵守目标站 robots.txt 与服务条款,把总 QPS 控制在站点可承受范围;不采集手机号、身份证号等个人信息,不绕过登录、验证码与付费墙,优先使用官方 API 与开放数据集。数据使用需符合《个人信息保护法》《数据安全法》《著作权法》。

小结:分布式的本质是把队列与去重搬到 Redis,让 worker 无状态可横向扩展;用 scrapy-redis 四个配置项起步,多机部署重点做「全局限速 + 代理池 + 进度观测」,任务来源复杂、需要审计时再换成自建任务表方案。

笔记加载中…