为什么需要分布式?单机的天花板
在构建分布式系统之前,我们必须理解单机爬虫的局限:
- 性能瓶颈:单台机器的 CPU、内存、网络带宽有限,爬取速度无法线性提升。
- IP 封禁风险:高频请求从同一 IP 发出,极易被目标网站识别并封禁。
- 单点故障:一旦爬虫程序崩溃或机器宕机,整个采集任务中断。
分布式爬虫通过将任务拆分,部署在多台机器上协同工作,有效解决了上述问题。而 Scrapy-Redis 正是实现这一目标的经典组合:Scrapy 提供强大的爬虫框架,Scrapy-Redis 则利用 Redis 作为中央调度器,实现请求的共享和去重。
核心架构:Scrapy-Redis 的工作原理
在分布式架构中,Redis 扮演着“大脑”的角色:
- 共享队列 (Queue) :所有爬虫节点从同一个 Redis 队列中获取待爬取的 URL(
start_urls和后续解析出的链接)。 - 去重集合 (Dupefilter) :每个请求的指纹(由 URL 生成的哈希值)存储在 Redis 的集合中,确保同一 URL 不会被重复爬取。
- 数据存储 (Item) :爬取到的数据(Item)可以统一写入 Redis,再由其他服务消费,或直接存入数据库。
这样,多个 Scrapy 爬虫实例(可以是同一台机器的多个进程,也可以是不同机器)就能并行工作,互不干扰,极大地提升了效率和稳定性。
从零开始:构建一个分布式豆瓣电影爬虫
让我们通过一个完整的例子,用代码实现一个分布式爬取豆瓣电影 Top250 的系统。
1. 环境准备与依赖
首先,确保安装了必要的库:
pip install scrapy scrapy-redis redis
2. 项目结构
distributed_spider/
├── scrapy.cfg
├── douban_spider/
│ ├── __init__.py
│ ├── items.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders/
│ ├── __init__.py
│ └── douban_top250.py
3. 定义数据结构 (items.py)
# items.py
import scrapy
class MovieItem(scrapy.Item):
# 电影排名
ranking = scrapy.Field()
# 电影名称
title = scrapy.Field()
# 评分
score = scrapy.Field()
# 评价人数
votes = scrapy.Field()
# 引用语(经典台词)
quote = scrapy.Field()
# 电影链接
url = scrapy.Field()
4. 配置文件 (settings.py) - 分布式的核心
# settings.py
import os
# --- Scrapy 基础配置 ---
BOT_NAME = 'douban_spider'
SPIDER_MODULES = ['douban_spider.spiders']
NEWSPIDER_MODULE = 'douban_spider.spiders'
# 避免法律风险,遵守 robots.txt
ROBOTSTXT_OBEY = True
# 下载延迟,避免对目标网站造成过大压力
DOWNLOAD_DELAY = 2
# 并发请求数量
CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 8
# --- Scrapy-Redis 分布式配置 ---
# 指定调度器,使用 Scrapy-Redis 提供的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 指定去重类
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 指定 Redis 连接信息
REDIS_HOST = '127.0.0.1' # 如果 Redis 在远程服务器,改为服务器 IP
REDIS_PORT = 6379
# REDIS_PASSWORD = 'your_password' # 如果设置了密码
# 是否在爬虫结束后,保持 Redis 中的请求队列和去重集合
# 开发调试时设为 True,方便复用;生产环境可设为 False
SCHEDULER_PERSIST = True
# 请求队列的优先级排序
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderPriorityQueue' # 优先级队列
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderQueue' # 队列(FIFO)
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderStack' # 栈(LIFO)
# --- 数据管道 (Pipeline) ---
ITEM_PIPELINES = {
# 可以将 Item 写入 Redis,供其他服务处理
'scrapy_redis.pipelines.RedisPipeline': 300,
# 或者自定义管道存入数据库
# 'douban_spider.pipelines.DoubanPipeline': 300,
}
# 日志级别
LOG_LEVEL = 'INFO'
# 用户代理,可以设置为随机 User-Agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
关键配置解析:
SCHEDULER和DUPEFILTER_CLASS:这两项是启用 Scrapy-Redis 分布式能力的核心,它们将默认的内存调度和去重替换为基于 Redis 的实现。SCHEDULER_PERSIST:设为True时,即使爬虫停止,队列中的 URL 也不会丢失,下次启动可继续爬取。SCHEDULER_QUEUE_CLASS:决定了 URL 的处理顺序。SpiderQueue是先进先出,适合广度优先;SpiderStack是后进先出,适合深度优先。
5. 编写爬虫 (douban_top250.py)
# spiders/douban_top250.py
import scrapy
from scrapy_redis.spiders import RedisSpider # 使用 RedisSpider
from douban_spider.items import MovieItem
class DoubanTop250Spider(RedisSpider):
name = 'douban_top250'
# 注意:这里不再需要 start_urls!
# 分布式爬虫的起始 URL 由 Redis 提供
# start_urls = ['https://movie.douban.com/top250']
# Redis 中存放起始 URL 的键名
redis_key = 'douban:start_urls'
def parse(self, response):
"""
解析电影列表页
"""
# 提取电影列表
movie_list = response.css('ol.grid_view li')
for movie in movie_list:
item = MovieItem()
# 排名
item['ranking'] = movie.css('div.pic em::text').get()
# 名称
item['title'] = movie.css('span.title::text').get()
# 评分
item['score'] = movie.css('span.rating_num::text').get()
# 评价人数
item['votes'] = movie.css('div.star span::text').re_first(r'(\d+)人评价')
# 引用语
item['quote'] = movie.css('span.inq::text').get()
# 电影链接
item['url'] = movie.css('div.pic a::attr(href)').get()
yield item
# 提取下一页链接并生成请求
next_page = response.css('span.next a::attr(href)').get()
if next_page:
next_page_url = response.urljoin(next_page)
yield scrapy.Request(next_page_url, callback=self.parse)
def make_requests_from_url(self, url):
"""
可选:用于处理从 Redis 读取的 URL
可以在这里添加额外的参数,如 headers、cookies
"""
return scrapy.Request(url=url, dont_filter=True)
关键点:
- 继承
RedisSpider而非scrapy.Spider。 - 使用
redis_key指定 Redis 中存放起始 URL 的键。 - 爬虫启动后,会自动从 Redis 的
douban:start_urls队列中获取 URL 并开始爬取。
6. 启动分布式爬虫
-
启动 Redis 服务:
redis-server -
向 Redis 中添加起始 URL:
# 使用 redis-cli redis-cli > LPUSH douban:start_urls https://movie.douban.com/top250 -
启动多个爬虫实例: 在不同的终端或不同的机器上,运行:
scrapy crawl douban_top250你可以同时启动多个实例,它们都会从同一个 Redis 队列中取任务,实现分布式爬取。
-
监控: 使用
redis-cli监控队列:redis-cli > LLEN douban:start_urls # 查看队列长度 > SCARD douban:requests:dupefilter # 查看去重集合大小
总结:分布式爬虫的实战心得
通过这个项目,我深刻体会到:
- Redis 是灵魂:它简单、高效、稳定,完美适合作为分布式爬虫的调度中心。
- 去重是关键:Scrapy-Redis 的
RFPDupeFilter基于 Redis 的set,能有效避免重复爬取,节省资源。 - 可扩展性:增加爬虫节点非常简单,只需部署代码并启动实例,系统会自动负载均衡。
- 容错性:单个爬虫节点崩溃不影响整体任务,其他节点会继续处理队列中的请求。
当然,真正的生产环境还需考虑更多:使用代理 IP 池应对反爬、将数据存入 MySQL/MongoDB、使用 Docker 容器化部署、结合 Celery 进行任务调度等。但 Scrapy-Redis 为我们提供了一个坚实而优雅的起点。掌握它,你就真正迈入了高效、稳定的数据采集世界。