ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Scrapy-Redis分布式爬虫架构与实战指南

Scrapy-Redis分布式爬虫架构与实战指南 1. Scrapy框架与分布式爬虫基础解析Scrapy作为Python生态中最强大的爬虫框架之一其设计哲学遵循Dont Repeat Yourself原则。单机模式下Scrapy通过内置的调度器Scheduler管理请求队列使用基于内存的集合实现去重。这种架构在中小规模爬取任务中表现优异但当面临以下场景时就会显现局限性千万级URL的抓取任务需要突破单机带宽限制应对目标网站的反爬策略实现24/7不间断爬取分布式爬虫通过将爬取任务分解到多台机器协同工作来解决这些问题。其核心在于共享的请求队列所有爬虫节点从统一队列获取任务集中式去重避免不同节点重复爬取相同页面结果汇总统一存储爬取结果关键点Scrapy原生架构中调度器和去重器都是基于单机内存实现这是其不支持分布式的根本原因。改造方向是将这些组件替换为基于网络存储的实现。2. Scrapy-Redis架构深度剖析Scrapy-Redis通过四个核心组件改造了原生Scrapy架构2.1 分布式调度器实现class RedisScheduler: def enqueue_request(self, request): if not request.dont_filter and self.df.request_seen(request): return False self.queue.push(request) # 将请求存入Redis队列 return True调度器改造要点将原生deque队列替换为Redis的List结构支持优先级队列通过Redis的Sorted Set实现持久化配置使爬虫中断后可恢复实测对比指标原生调度器Redis调度器队列容量内存限制取决于Redis配置重启恢复不支持支持跨机器共享不支持支持2.2 基于Redis的去重机制去重指纹生成算法def request_fingerprint(request): fp hashlib.sha1() fp.update(request.method.encode()) # HTTP方法 fp.update(normalize_url(request.url)) # 标准化URL fp.update(request.body or b) # 请求体 return fp.hexdigest() # 40位SHA1值去重性能优化策略使用Redis的SET数据结构存储指纹批量操作减少网络往返本地缓存近期指纹降低Redis访问2.3 分布式管道设计RedisPipeline的工作流程序列化Item对象为JSON字符串使用RPUSH命令存入Redis列表列表键名格式spider_name:items数据消费建议方案import redis import json def consume_items(): r redis.Redis(hostredis-host) while True: _, item_data r.blpop(spider:items) item json.loads(item_data) # 处理item逻辑2.4 RedisSpider的核心改造相比原生SpiderRedisSpider主要变化移除start_urls改为从Redis读取起始URL实现心跳机制保持爬虫活性增加分布式任务分配逻辑3. 完整分布式爬虫搭建实战3.1 环境准备与配置硬件建议配置Redis服务器4核CPU/8GB内存/SSD存储爬虫节点按需扩展建议初始2-4台软件安装清单# 所有节点执行 pip install scrapy scrapy-redis redis-py sudo apt-get install redis-server # Redis节点安装关键配置settings.py# 必须配置 SCHEDULER scrapy_redis.scheduler.Scheduler DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter REDIS_URL redis://:passwordmaster-node:6379/0 # 推荐配置 SCHEDULER_PERSIST True # 保持任务队列 SCHEDULER_QUEUE_CLASS scrapy_redis.queue.PriorityQueue CONCURRENT_REQUESTS 32 # 并发数优化3.2 爬虫节点改造示例原始爬虫class MySpider(scrapy.Spider): name myspider start_urls [http://example.com] def parse(self, response): # 解析逻辑改造后from scrapy_redis.spiders import RedisSpider class MyDistributedSpider(RedisSpider): name myspider redis_key myspider:start_urls # 从Redis读取起始URL def parse(self, response): # 保持原有解析逻辑3.3 集群启动与管理启动所有节点# 在每个爬虫节点执行 scrapy crawl myspider管理Redis任务队列# 添加起始URL redis-cli -h redis-host lpush myspider:start_urls http://target-site.com # 监控队列状态 redis-cli -h redis-host info | grep keyspace3.4 性能优化技巧管道批处理class BatchRedisPipeline: def __init__(self): self.items [] def process_item(self, item, spider): self.items.append(item) if len(self.items) 100: self._send_items() return item def _send_items(self): with redis.pipeline() as pipe: for item in self.items: pipe.rpush(f{self.spider.name}:items, json.dumps(item)) pipe.execute() self.items []去重优化使用Bloom Filter替代SETredisbloom模块本地内存缓存定期同步策略自适应限速class SmartThrottle: classmethod def from_crawler(cls, crawler): return cls(crawler.stats) def __init__(self, stats): self.stats stats def adjust_delay(self, old_delay): # 根据错误率动态调整 error_rate self.stats.get_value(downloader/exception_ratio, 0) if error_rate 0.1: return min(old_delay * 1.5, 10) return max(old_delay * 0.9, 0.5)4. 生产环境问题排查指南4.1 常见故障模式队列阻塞现象表现所有爬虫闲置但队列不为空检查redis-cli llen queue_key解决方案重启调度器或检查爬虫回调逻辑内存溢出监控指标redis-cli info memory优化方案设置maxmemory-policy定期清理已完成任务的队列重复爬取检查去重集合redis-cli scard dupefilter_key确认指纹算法一致性4.2 监控方案设计推荐监控指标# prometheus_client示例 from prometheus_client import Gauge redis_queue_size Gauge(spider_queue_size, Current task queue size) redis_dupefilter_size Gauge(spider_dupefilter_size, Unique URLs count) class MonitorExtension: def __init__(self): self.redis redis.Redis() classmethod def from_crawler(cls, crawler): ext cls() crawler.signals.connect(ext.spider_opened, signalsignals.spider_opened) return ext def spider_opened(self, spider): self.spider spider self.timer LoopingCall(self._update_metrics) self.timer.start(60) def _update_metrics(self): queue_size self.redis.llen(f{self.spider.name}:requests) redis_queue_size.set(queue_size) dupe_size self.redis.scard(f{self.spider.name}:dupefilter) redis_dupefilter_size.set(dupe_size)4.3 扩展架构建议对于超大规模爬取需求可以考虑多级Redis集群主从复制保证可用性分片存储解决单实例容量限制动态节点管理Kubernetes自动扩缩容基于队列长度的自动调节混合存储方案Redis作为消息队列MongoDB存储最终结果5. 高级应用场景5.1 增量爬取实现基于时间戳的方案class IncrementalSpider(RedisSpider): def parse(self, response): last_crawl self.redis.get(f{self.name}:last_crawl) item extract_item(response) if item[date] last_crawl: yield item self.redis.set(f{self.name}:last_crawl, current_time())5.2 分布式渲染集成结合Selenium Gridfrom selenium.webdriver import Remote class JSSpider(RedisSpider): def parse(self, response): driver Remote( command_executorhttp://grid-hub:4444/wd/hub, desired_capabilities{browserName: chrome} ) driver.get(response.url) rendered_html driver.page_source # 解析渲染后内容5.3 智能调度算法基于优先级的动态调整class PriorityCalculator: def __init__(self, redis_conn): self.redis redis_conn def calculate(self, url): domain urlparse(url).netloc # 基于域名的历史成功率 success_rate self.redis.hget(domain_stats, f{domain}:success_rate) or 0.8 # 最近响应时间 response_time self.redis.hget(domain_stats, f{domain}:response_time) or 1.0 return success_rate / response_time在实际项目中我们曾用这套架构实现了日均抓取千万级页面的电商比价系统。关键经验包括采用分片Redis集群后去重集合容量从单机2GB降低到每个分片500MB通过管道批处理网络IO减少70%动态限速使请求错误率从15%降至3%以下建议开发者在实施时特别注意为不同爬虫使用独立的Redis数据库编号定期备份去重集合实现完善的监控告警系统对重要URL采用备份队列机制
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进