聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎
分布式爬虫要点
![](https://img.haomeiwen.com/i9538421/52103f677f4549cc.png)
爬虫 A、B、C 分别放在三台服务器上,还需要一个 “状态管理器” 来对 URL 进行集中管理、去重等操作,它可以单独部署在一个服务器上面,也可以部署在 A、B、C 任何一台服务器上面
![](https://img.haomeiwen.com/i9538421/af3af400d25fb104.png)
回顾一下 Scrapy 架构图
![](https://img.haomeiwen.com/i9538421/27600ab1a8952306.png)
在一台机器上运行的爬虫中,SPIDER yield 出来的任何一条 REQUEST 都需要进入 SCHEDULER,然后 ENGINE 从 SCHEDULER 取 REQUEST 发送给 DOWNLOADER;还有关于 Scrapy 的去重,是放在内存中的一个 set 里面的。
如果想将 Scrapy 改造成分布式,就会有两个问题必须要解决
①request 队列集中管理
② 去重集中管理
![](https://img.haomeiwen.com/i9538421/adbe6c1f4a3acbef.png)
如何解决:
①在 Scrapy 中,SCHEDULER 实际上是放在一个 QUEUE(队列)中的,而这个 QUEUE 实际上就是放在内存中的,如果是分布式爬虫,其他服务器是拿不到当前服务器内存中的内容的,所以 Scrapy 就没法支持分布式,要想办法将这地方的 QUEUE 管理做成一种集中式的管理
②去重也是要做集中管理的,Scrapy 当中有一个去重的扩展,去重原理就是通过内存中的 set 来实现的,所以也没法做成分布式
所以关注的重点就是,将 “去重的 set” 和 “REQUEST 队列 SCHEDULER” 这两个放到第三方的组件来做,这个第三方组件就是分布式爬虫的主角 Redis
Redis 是基于内存的数据库,事实上也可以用 关系型数据库来做分布式,但是效率会很低
redis基础知识
Redis 菜鸟教程:http://www.runoob.com/redis/redis-tutorial.html
Redis 中文文档:http://www.redis.cn/commands.html
scrapy-redis编写分布式爬虫代码
GitHub 地址:https://github.com/rmax/scrapy-redis
需要安装库
pip install redis
Scrapy-Redis 的使用
settings.py 中需要配置
# 调度器(必须)
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 去重(必须)
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 用于将 ITEM 序列化并发送到 Redis(可选)
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300
}
所有的编写的 Spider 不再继承
scrapy.Spider
,而要继承RedisSpider
from scrapy_redis.spiders import RedisSpider
class MySpider(RedisSpider):
name = 'myspider'
def parse(self, response):
# do stuff
pass
启动 Spider 命令不再是
scrapy crawl myspider
,而是改成scrapy runspider myspider.py
,注意要写 文件名 而不再是 爬虫名
scrapy runspider myspider.py
现在启动爬虫后,所有的 REQUEST 就不再是本地 SCHEDULER 来完成的,而是在 settings.py 中新增的 Scrapy-Redis 的 SCHEDULER
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
启动 Spider 以后,必须要向队列里面放入一个初始化 URL,现在所有 URL 都是放在 Redis 当中的,所以初始化的时候,必须要向 Redis 中 push 一个起始 URL,这样才能够进行爬取
redis-cli lpush myspider:start_urls http://google.com
新建 Scrapy 项目
ScrapyRedisTest
scrapy startproject ScrapyRedisTest
![](https://img.haomeiwen.com/i9538421/2b395f844fae6a4f.png)
将 Scrapy-Redis 源码 clone 下来
git clone https://github.com/rmax/scrapy-redis.git
![](https://img.haomeiwen.com/i9538421/8c86dc7a2b7f896c.png)
将 clone 下来的 Scrapy-Redis 源码 复制到 新建的
ScrapyRedisTest
项目中
![](https://img.haomeiwen.com/i9538421/0fa3d4f556acce10.png)
将 jobbole spider 改造成 基于 Scrapy-Redis 的分布式爬虫
![](https://img.haomeiwen.com/i9538421/2de77d215792bb40.png)
![](https://img.haomeiwen.com/i9538421/7b00945b45ba5c83.png)
![](https://img.haomeiwen.com/i9538421/9bd885bd815136aa.png)
![](https://img.haomeiwen.com/i9538421/af3dc75c22dc6fa1.png)
![](https://img.haomeiwen.com/i9538421/def5c97f12b2c8d8.png)
![](https://img.haomeiwen.com/i9538421/86e2e896d21fd223.png)
关于
RedisMixin
类中获取下一个 URL 的方法next_requests
Scrapy 中获取
next_requests
的时候是通过 Scrapy 的 SCHEDULER 来完成的,SCHEDULER 是维持一个 QUEUE,是放在内存当中的;现在 Scrapy-Redis 将这个 QUEUE 放到了 Redis 当中,使用 list 或 set 等来完成
可以将原来的 jobbole spider 中 parse 方法逻辑完全拷贝过来
# ScrapyRedisTest/spiders/jobbole.py
from urllib.parse import urljoin
import scrapy
from scrapy_redis.spiders import RedisSpider
class JobboleSpider(RedisSpider):
name = 'jobbole'
allowed_domains = ['jobbole.com']
redis_key = 'jobbole:start_urls'
def parse(self, response):
"""
1. 提取文章列表页中所有文章详情页链接,并交给 parse_detail 方法进行解析
2. 提取下一页链接,并交给 Scrapy 进行下载
Args:
response: 响应信息
Yields:
1. 文章详情页链接,交给 parse_detail 解析
2. 下一页链接,交给 Scrapy 下载
"""
post_nodes = response.xpath('//div[@id="archive"]')
for post_node in post_nodes:
post_url = post_node.xpath('.//div[@class="post-meta"]//a[@class="archive-title"]/@href').extract_first('')
front_img_url = post_node.xpath('.//div[@class="post-thumb"]//img/@src').extract_first('')
yield scrapy.Request(url=urljoin(response.url, post_url), callback=self.parse_detail,
meta={'front_img_url': front_img_url})
next_url = response.xpath('//a[@class="next page-numbers"]/@href').extract_first()
if next_url:
yield scrapy.Request(url=next_url, callback=self.parse)
def parse_detail(self, response):
pass
因为这里只做演示,所以删除了很多逻辑,只有一个简单的 parse 方法
接下来就是 settings.py 中的配置
# ScrapyRedisTest/settings.py
ROBOTSTXT_OBEY = False
# Scrapy-Redis 相关配置
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300
}
编写启动爬虫主文件 main.py
![](https://img.haomeiwen.com/i9538421/d3c982a636b19e3c.png)
运行爬虫
![](https://img.haomeiwen.com/i9538421/de14d2452f554a12.png)
向 Redis 里面 push 一个起始 URL
![](https://img.haomeiwen.com/i9538421/40afdb0f877e218d.png)
发现 spider 开始进行爬取
![](https://img.haomeiwen.com/i9538421/0d8290241488e77b.png)
为了方便查看 Scrapy-Redis 爬虫的流程,可以在
jobbole.py
和 Scrapy-Redis 源码scheduler.py
中分别打一个断点,进行调试检测
![](https://img.haomeiwen.com/i9538421/920bc897cef19b16.png)
![](https://img.haomeiwen.com/i9538421/22bef09b7961203d.png)
DeBug 运行 spider,第一次停到断点这里实际上没有起始的 URL,此时向 Redis 中 push 第一个 URL,然后 f9 跳过这个请求,程序又会卡在这个断点
![](https://img.haomeiwen.com/i9538421/be1d74d82a673b00.png)
![](https://img.haomeiwen.com/i9538421/04c1f1d84c6bf015.png)
此时查看 Redis,新增了一个
jobbole:requests
键,值为ZSET
类型,为什么使用可排序的ZSET
而不是SET
或者LIST
呢?因为默认情况下jobbole:requests
是有优先级的,我们在程序中是可以设定优先级的,为了满足这个功能,必须使用可排序的ZSET
实际上,存到 Redis 里面的这个 request 是一个 Python 类,将这个类放到了 Redis 中,但是 Redis 中是不可能放 Python 类的,实际上这里存入 Redis 中是经过 Pickle 序列化的,将类序列化成字符串,存入 Redis,后续拿出来使用的时候可以反序列化成类
![](https://img.haomeiwen.com/i9538421/6dd8147019984a33.png)
多按几次 f9,再次查看 Redis,多了一个
jobbole:dupefilter
键,这个键的值类型为SET
,因为是用于去重的,所以就没必要使用ZSET
了
![](https://img.haomeiwen.com/i9538421/6806d37795b64b64.png)
![](https://img.haomeiwen.com/i9538421/bb05755e0997a14b.png)
scrapy-redis源码剖析-dupefilter.py、picklecompat.py
![](https://img.haomeiwen.com/i9538421/55b74d3fcf49cfeb.png)
- dupefilter.py
dupefilter.py 是用来去重的文件,Scrapy-Redis 的 dupefilter.py 源码几乎同 Scrapy 的 dupefilter.py 源码一模一样
![](https://img.haomeiwen.com/i9538421/bb0aad16fe47d121.png)
![](https://img.haomeiwen.com/i9538421/4b83a55bb8037d3a.png)
![](https://img.haomeiwen.com/i9538421/084d305cb6409720.png)
Scrapy-Redis 在初始化的时候就生成了一个 server,这个 server 就连接到了 Redis,调用了
get_redis_from_settings
方法,这个方法是在 connection.py 中的
![](https://img.haomeiwen.com/i9538421/ad23966668a34877.png)
![](https://img.haomeiwen.com/i9538421/0908ca83b86ebd21.png)
![](https://img.haomeiwen.com/i9538421/8413bf84824ef182.png)
![](https://img.haomeiwen.com/i9538421/f08d97fc4948867c.png)
- picklecompat.py
picklecompat.py 实际上就是调用了 Python 的 pickle 库
![](https://img.haomeiwen.com/i9538421/16c2accbf5c55f61.png)
scrapy-redis源码剖析- pipelines.py、 queue.py
![](https://img.haomeiwen.com/i9538421/2dda1f03f768837b.png)
- pipelines.py
pipelines.py 文件中会将 ITEM 放到 Redis 当中
![](https://img.haomeiwen.com/i9538421/18f8ab8bc96d9022.png)
pipelines.py 文件中
RedisPipeline
类的初始化方法同样会初始化一个server
,这个server
同样会调用 connection.py 中的get_redis_from_settings
方法
![](https://img.haomeiwen.com/i9538421/815c52db4121a7ed.png)
![](https://img.haomeiwen.com/i9538421/0b89fc14235782e6.png)
![](https://img.haomeiwen.com/i9538421/66646f3a86282104.png)
![](https://img.haomeiwen.com/i9538421/5932c773dae3823b.png)
事实上,将数据存入 Redis 的过程的类
RedisPipeline
不是必须要配置到settings.py
中的,实际上也可以在爬虫爬取完成之后直接存储到本地数据库中,设置保存到 Redis 当中的好处是,可以完成数据共享,将数据放到 Redis 当中就可以多起几个进程,甚至还可以写一个脚本(不是爬虫),直接从 Redis 中读取数据并将其存储到关系型数据库当中
- queue.py
queue.py 是供给 schedluer 调度来使用的,共有 3 个 Queue
![](https://img.haomeiwen.com/i9538421/47ce74ef9aac7153.png)
FifoQueue 意思是:first in first out 的简写,先进先出,也就是有序队列
![](https://img.haomeiwen.com/i9538421/a0bdad4933796169.png)
LifoQueue 意思是:last in first out 的简写,后进先出,类似于
栈
![](https://img.haomeiwen.com/i9538421/5f48d4479bbdfb0c.png)
PriorityQueue:默认使用的就是这个 Queue
![](https://img.haomeiwen.com/i9538421/3099d1ba463ca328.png)
![](https://img.haomeiwen.com/i9538421/e22e78bcbcfeff02.png)