从单机到集群:一个程序员眼中的分布式爬虫进化论
作为一名程序员,我们都写过爬虫。从用 requests + BeautifulSoup 抓取单个页面的“小打小闹”,到用 Scrapy 框架处理整个网站的“正规军”,我们享受着数据从无序变有序的快感。但很快,新的瓶颈就出现了:
- 速度太慢:单机 IP 很快被封,CPU 和带宽成为瓶颈。
- 单点故障:脚本一挂,整个采集任务中断。
- 难以扩展:想抓取更多网站,只能换更强的机器,成本高昂。 这时,“分布式”这个词就像一道光,照亮了前进的方向。《Python 分布式爬虫完全教程:从基础原理到 Scrapy-Redis 实战》这样的课程,正是我们从一个“脚本小子”向“系统架构师”思维转变的关键一步。它教我们的,不是一个新的库,而是一种架构思想。 今天,我们就用代码来解构这个进化过程,看看如何从一个标准的 Scrapy 爬虫,平滑地演进到一个强大的分布式集群。
第一阶段:单机 Scrapy 爬虫的“天花板”
一个标准的 Scrapy 爬虫,其核心是 Scheduler(调度器)和 Downloader(下载器)。它们都在同一个进程中,共享一个待抓取的请求队列。这就像一个只有一个柜台的小商店,顾客(Request)多了,自然就排起了长队。
这是一个简化的 Scrapy 爬虫结构:
# items.py
import scrapy
class QuoteItem(scrapy.Item):
text = scrapy.Field()
author = scrapy.Field()
tags = scrapy.Field()
# spiders/quote_spider.py
import scrapy
from tutorial.items import QuoteItem
class QuoteSpider(scrapy.Spider):
name = 'quotes'
start_urls = ['http://quotes.toscrape.com/js/']
def parse(self, response):
for quote in response.css('div.quote'):
item = QuoteItem()
item['text'] = quote.css('span.text::text').get()
item['author'] = quote.css('small.author::text').get()
item['tags'] = quote.css('div.tags a.tag::text').getall()
yield item
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
yield response.follow(next_page, self.parse)
这个爬虫运行良好,但它的 start_urls 和后续发现的 Request 都被存储在本地内存中。这就是它的“天花板”——无法被其他机器共享。
第二阶段:Scrapy-Redis,分布式架构的“心脏”
如何打破这个天花板?答案是共享。如果所有爬虫节点都能访问同一个待抓取队列,问题就迎刃而解了。Redis,这个高性能的内存数据库,天生就是做这件事的完美选择。
Scrapy-Redis 的核心思想就是:用 Redis 替换掉 Scrapy 原生的、基于内存的调度器和队列。
架构演进:
- 共享队列:所有待抓取的
Request对象被序列化后存入 Redis 的list或zset数据结构中。 - 共享去重:利用 Redis 的
set来存储已抓取请求的指纹,实现全局去重。 - Master/Worker 模式:
- Master 节点:只负责向 Redis 的
start_urls队列中放入初始种子 URL。 - Worker 节点:可以有多个。它们从 Redis 中获取请求,抓取页面,解析数据,并将新发现的请求再次放回 Redis 队列。
- Master 节点:只负责向 Redis 的
第三阶段:代码实战,改造我们的爬虫
将单机 Scrapy 爬虫改造为 Scrapy-Redis 分布式爬虫,出人意料地简单。 1. 安装依赖
pip install scrapy scrapy-redis
2. 修改 settings.py
这是最关键的一步。我们只需要告诉 Scrapy 使用 Scrapy-Redis 提供的组件即可。
# settings.py
# 启用 Scrapy-Redis 的调度器,取代原生调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 启用 Scrapy-Redis 的去重机制,取代原生去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 在 Redis 中保持请求队列,允许暂停/恢复爬虫
SCHEDULER_PERSIST = True
# 指定 Redis 连接信息
REDIS_HOST = 'localhost' # 或你的 Redis 服务器 IP
REDIS_PORT = 6379
# 可选:指定 Redis 数据库编号
# REDIS_DB = 0
# 可选:如果 Redis 有密码
# REDIS_PARAMS = {
# 'password': 'your_password',
# }
3. 修改爬虫文件 spiders/quote_spider.py
改动非常小,只需将爬虫的父类从 scrapy.Spider 改为 scrapy_redis.spiders.RedisSpider,并删除 start_urls。
# spiders/quote_spider.py
import scrapy
from scrapy_redis.spiders import RedisSpider # 1. 导入 RedisSpider
from tutorial.items import QuoteItem
class QuoteSpider(RedisSpider): # 2. 继承 RedisSpider
name = 'quotes'
# 3. 删除 start_urls,改为 redis_key
# 爬虫将从 Redis 的这个 key 对应的 list 中获取起始 URL
redis_key = 'quotes:start_urls'
def parse(self, response):
# parse 方法完全不需要改动!
for quote in response.css('div.quote'):
item = QuoteItem()
item['text'] = quote.css('span.text::text').get()
item['author'] = quote.css('small.author::text').get()
item['tags'] = quote.css('div.tags a.tag::text').getall()
yield item
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
yield response.follow(next_page, self.parse)
代码解读:
RedisSpider:这个父类已经为我们处理好了一切。它会自动从 Redis 中获取请求,并将新请求放回 Redis。redis_key:这是爬虫的“启动信标”。我们不再需要硬编码start_urls。取而代之的是,我们只需要在 Redis 中,向quotes:start_urls这个listlpush一个或多个初始 URL 即可。
第四阶段:启动你的分布式爬虫集群
现在,你的爬虫已经是一个“分布式”爬虫了。启动它,就像启动一个舰队: 1. 启动 Redis 服务器 确保你的 Redis 服务正在运行。 2. 向 Redis 中推送种子 URL 打开 Redis 客户端:
redis-cli
执行命令:
LPUSH quotes:start_urls "http://quotes.toscrape.com/js/"
3. 启动爬虫节点 你可以在同一台机器上开多个终端,也可以在多台不同的机器上(只要它们都能访问到你的 Redis 服务器)执行以下命令:
# 在终端 1 (Worker 1)
scrapy crawl quotes
# 在终端 2 (Worker 2)
scrapy crawl quotes
# 在另一台机器上 (Worker 3)
scrapy crawl quotes
你会看到,多个爬虫实例会同时从 Redis 中争抢任务,协同工作。一个节点挂了,其他节点不受影响,Redis 中的任务队列依然存在,重启即可继续工作。
结论:架构思维的胜利
从单机到分布式,我们修改的代码寥寥无几,但背后的架构思想却发生了质的飞跃。Scrapy-Redis 的精妙之处在于,它通过一个中间件(Redis)巧妙地解耦了“任务生产”和“任务消费”,实现了爬虫的水平扩展。
这就是程序员的价值所在:我们不只是代码的实现者,更是架构的设计者。理解了 Scrapy-Redis 的原理,你就能举一反三,去设计更复杂的分布式任务系统,比如用 RabbitMQ 或 Kafka 替代 Redis,实现更强大的任务调度和消息传递。这,才是从一个“教程跟随者”到“技术掌控者”的真正蜕变。