Scrapy-Redis:从单机到分布式集群
# Scrapy-Redis:从单机到分布式集群
当你的爬取目标数据量巨大、网站反爬策略严格(如 IP 封锁频繁)、或者单台机器的处理能力达到瓶颈时,单机的 Scrapy 爬虫便会显得力不从心。此时,将爬虫从单机扩展为分布式集群,协同作业,就成了必然选择。
scrapy-redis
正是为此而生的强大组件,它将 Scrapy 无缝地与 Redis 数据库结合,将一个普通的 Scrapy 项目改造为可以由多台机器共同执行的分布式爬虫系统。
# 一、为什么需要分布式爬虫?
原生 Scrapy 框架是一个高效的单机爬虫框架,但它的核心组件都是基于内存的,这导致了其天然的局限性:
- 调度器 (Scheduler):请求队列存在于单台机器的内存中,无法被其他机器访问。
- 去重过滤器 (DupeFilter):URL 指纹集合也存在于内存中,每台机器都有自己的去重集合,无法做到全局去重。
- 数据管道 (Pipeline):Item 数据被直接处理或存储在本地,无法集中管理。
这些限制意味着,如果你在多台机器上运行同一个 Scrapy 项目,它们将是完全独立、互不相干的,会产生大量的重复抓取,无法协同完成一个大的爬取任务。
# 二、Scrapy-Redis 核心原理
scrapy-redis
的核心思想非常巧妙:它替换掉了 Scrapy 原生的核心组件,将它们的功能从基于内存实现转为基于 Redis 实现。Redis 是一个高性能的内存数据库,可以被多台机器共享访问,从而完美解决了分布式的问题。
# 2.1 组件改造
调度器
Scheduler
-> 基于 Redislist
或zset
的调度器scrapy-redis
将待抓取的 Request 对象序列化后存入 Redis 的一个list
中(通常以爬虫的redis_key
命名)。- 所有爬虫客户端都从这个共享的
list
中获取任务(lpop
或brpop
),实现了任务的统一分配。
去重过滤器
DupeFilter
-> 基于 Redisset
的去重过滤器- 所有 Request 的指纹(fingerprint)都被存入 Redis 的一个
set
中。 - 在调度请求前,会先检查该请求的指纹是否存在于这个共享的
set
中(sismember
),从而实现了全局去重。
- 所有 Request 的指纹(fingerprint)都被存入 Redis 的一个
数据管道
RedisPipeline
-> 将 Item 存入 Redisscrapy-redis
提供了一个RedisPipeline
,它会自动将爬虫抓取到的 Item 序列化为 JSON 字符串,并存入 Redis 的一个list
中(key 通常为spider.name:items
)。- 这样,数据的产生(爬虫)和数据的消费(数据处理程序)就实现了解耦。你可以编写独立的脚本从 Redis 中读取并处理数据。
# 2.2 分布式流程图
graph TD
subgraph "Master 节点 (可选, 可兼作 Slave)"
A[推送初始 URL 到 Redis] --> B{Redis 数据库};
end
subgraph "Slave 节点 1 (爬虫客户端)"
C1[爬虫 1] -->|获取 Request| B;
B -->|URL 指纹| D1[全局去重 DupeFilter];
D1 -->|判断是否存在| B;
B -->|返回 Request| C1;
C1 -->|下载页面| E1[互联网];
E1 -->|Response| C1;
C1 -->|Item| F1[RedisPipeline];
F1 -->|存储 Item| B;
end
subgraph "Slave 节点 2 (爬虫客户端)"
C2[爬虫 2] -->|获取 Request| B;
B -->|URL 指纹| D2[全局去重 DupeFilter];
D2 -->|判断是否存在| B;
B -->|返回 Request| C2;
C2 -->|下载页面| E2[互联网];
E2 -->|Response| C2;
C2 -->|Item| F2[RedisPipeline];
F2 -->|存储 Item| B;
end
subgraph "数据消费端"
B --> G[独立的数据处理程序];
G --> H[数据库/文件/其他];
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 三、分布式项目搭建完整教程
下面,我们将以抓取“阳光问政”网站为例,从零开始搭建一个完整的分布式爬虫项目。
# 3.1 环境准备
- 安装
scrapy-redis
pip install scrapy-redis
1 - 安装并启动 Redis
- 确保你的 Redis 服务已经启动。
- 为了让其他机器能够连接,需要修改
redis.conf
文件:- 注释掉
bind 127.0.0.1
这一行,允许所有 IP 连接。 - 将
protected-mode
设置为no
。 - 如果需要,设置密码
requirepass your_password
。
- 注释掉
# 3.2 创建项目与爬虫
# 创建一个标准的 Scrapy 项目
scrapy startproject fbsPro
cd fbsPro
# 使用 crawlspider 模板创建一个爬虫
scrapy genspider -t crawl fbs wz.sun0769.com
2
3
4
5
6
# 3.3 关键配置 (settings.py
)
这是将普通 Scrapy 项目改造为 scrapy-redis
项目最核心的一步。
# settings.py
# 1. (必须) 指定使用 scrapy-redis 的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 2. (必须) 指定使用 scrapy-redis 的去重过滤器
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 3. (必须) 指定 redis 的主机和端口
REDIS_HOST = "127.0.0.1" # Redis 服务器的 IP 地址
REDIS_PORT = 6379 # Redis 服务的端口
# 如果有密码,需要配置
# REDIS_PARAMS = {'password': 'your_password'}
# 4. (必须) 配置 Item Pipeline
ITEM_PIPELINES = {
# 将 RedisPipeline 的优先级设得低一些,以便其他 Pipeline 先处理
'scrapy_redis.pipelines.RedisPipeline': 400,
# 如果你有自己的数据清洗 Pipeline,可以放在 RedisPipeline 之前
# 'fbsPro.pipelines.FbsProPipeline': 300,
}
# 5. (可选) 设置 SCHEDULER_PERSIST = True,爬虫关闭时,保留 Redis 中的请求队列和去重集合,方便暂停和恢复。
SCHEDULER_PERSIST = True
# 6. (可选) 设置请求队列的类型
# SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.PriorityQueue" # 优先级队列
# SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.FifoQueue" # 先进先出队列
# SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.LifoQueue" # 后进先出队列(默认)
# 7. (可选) 设置将起始 URL 存储在 Redis 的 set 集合中,允许多个爬虫共享起始URL
# REDIS_START_URLS_AS_SET = True
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 3.4 爬虫改造 (fbs.py
)
将爬虫的父类从 CrawlSpider
修改为 RedisCrawlSpider
,并定义 redis_key
。
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import Rule
# 1. 导入 RedisCrawlSpider
from scrapy_redis.spiders import RedisCrawlSpider
# 2. 继承自 RedisCrawlSpider
class FbsSpider(RedisCrawlSpider):
name = 'fbs'
allowed_domains = ['wz.sun0769.com']
# 3. 定义 redis_key,爬虫将从这个 key 中读取起始 URL
# 这个 key 就是一个 Redis 的 list
redis_key = 'fbs:start_urls'
# 注意:start_urls 属性在 RedisSpider 中将不再需要,因为起始 URL 是从 Redis 中读取的。
# start_urls = ['http://wz.sun0769.com/']
rules = (
# 提取列表页的链接
Rule(LinkExtractor(allow=r'type=4&page=\d+')),
# 提取详情页的链接,并交给 parse_item 处理
Rule(LinkExtractor(allow=r'/political/politics/view\?id=\d+'), callback='parse_item'),
)
def parse_item(self, response):
item = {}
# 提取标题
item['title'] = response.xpath('//div[@class="focus-details-title-container"]/h2/text()').get()
# 提取编号
item['number'] = response.xpath('//div[@class="focus-details-title-container"]/p/span[last()]/text()').re_first(r'编号:(\d+)')
# 提取内容
content = response.xpath('//div[@class="details-content"]//text()').getall()
item['content'] = "".join(content).strip()
item['url'] = response.url
yield item
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 3.5 部署与启动
现在,你的分布式爬虫已经准备就绪。
1. 启动爬虫客户端 (Slave 节点)
将配置好的 fbsPro
项目代码复制到所有你希望参与爬取的机器上(Slave 节点)。在每台机器上,进入项目目录并执行:
scrapy crawl fbs
此时,所有的爬虫客户端都会启动,但它们会阻塞住,因为 Redis 的 fbs:start_urls
队列中还没有任何任务。
2. 推送初始 URL (Master 节点)
选择任意一台安装了 Redis 客户端的机器(可以是爬虫节点,也可以是独立机器),连接到 Redis 服务器,并向 redis_key
指定的队列中推送一个或多个起始 URL。
# 使用 redis-cli
redis-cli
# 连接到 Redis (如果需要)
# auth your_password
# 推送起始 URL
lpush fbs:start_urls "https://wz.sun0769.com/political/index/politicsNewest?type=4&page=1"
2
3
4
5
6
7
8
一旦你推送了 URL,所有正在等待的爬虫客户端会立刻开始抢占任务,并开始分布式爬取。
# 3.6 监控与调试
在爬虫运行期间,你可以通过监控 Redis 中的关键 key 来了解爬虫的状态:
lrange fbs:start_urls 0 -1
: 查看待抓取的起始 URL 队列。llen fbs:start_urls
: 查看队列中剩余的任务数。scard fbs:dupefilter
: 查看已经抓取过的 URL 指纹数量,可以判断去重是否正常。llen fbs:items
: 查看已经抓取到并存入 Redis 的 Item 数量。lrange fbs:items 0 10
: 查看最近抓取的 10 条数据,检查数据格式是否正确。
# 四、消费 Redis 中的数据
当爬虫将数据存入 Redis 后,你需要编写一个独立的 Python 脚本来消费这些数据。
# consume_data.py
import redis
import json
def main():
# 连接到 Redis
r = redis.Redis(host='127.0.0.1', port=6379)
while True:
# 从 'fbs:items' 队列中阻塞式地取出一个 item
# 'brpop' 会在列表为空时阻塞,直到有新元素加入
source, data = r.brpop(["fbs:items"])
# 将取出的 JSON 字符串反序列化为 Python 字典
item = json.loads(data)
# 在这里进行你的数据处理,例如存入 MongoDB, MySQL 或写入文件
print(f"消费到数据: {item['title']}")
# 假设我们将其写入一个 JSON Lines 文件
with open('wenzheng.jsonl', 'a', encoding='utf-8') as f:
f.write(json.dumps(item, ensure_ascii=False) + '\n')
if __name__ == '__main__':
main()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
你可以独立运行这个 consume_data.py
脚本,它会持续地从 Redis 中获取并处理数据。
# 五、总结与常见问题
- 核心改造:
scrapy-redis
的核心是对 Scrapy 的调度器和去重过滤器进行了 Redis 化。 - 爬虫继承:分布式爬虫必须继承自
RedisSpider
或RedisCrawlSpider
。 - 启动方式:分布式爬虫不再依赖
start_urls
,而是通过redis-cli
向redis_key
指定的队列推送 URL 来启动。 - 数据解耦:
RedisPipeline
实现了爬虫(生产者)与数据处理(消费者)的解耦,增强了系统的健壮性和可扩展性。
FAQ:
- 爬虫启动后没反应?
- 检查 Redis 连接配置(IP, Port, Password)是否正确。
- 检查是否已向正确的
redis_key
中推送了起始 URL。 - 检查防火墙是否开放了 Redis 端口。
- 出现大量重复抓取?
- 确认
DUPEFILTER_CLASS
配置是否正确。 - 检查所有爬虫客户端是否都连接到了同一个 Redis 实例和数据库。
- 确认
- 如何暂停和恢复爬虫?
- 确保
SCHEDULER_PERSIST = True
。 - 暂停:直接用
Ctrl+C
停止所有爬虫进程。 - 恢复:重新在所有节点上执行
scrapy crawl fbs
即可,它们会自动从上次中断的地方继续。
- 确保