Scrapy-Redis 分布式
(一)分布式爬虫简介
1、为什么要用分布式
分布式爬虫就是多台计算机上都安装爬虫程序,重点是联合采集。比如爬虫 A,B,C 分别在三台服务器上,需要一个状态管理器集中分配,去重这三个爬虫的 url,状态管理器 也是一个服务,需要部署在某一个服务器上,通过状态管理器集中分配需要抓取的 URL, 以及去重。
分布式爬虫优点:
1、充分利用多机器的带宽进行加速的爬取,一台服务器上的带宽有限。
2、充分利用多机器的 IP 加速爬取的速度,一台服务器如果爬取过快则可能 IP 会被封。 分布式爬虫:多台服务器有序的爬取任务队列中的 URL。
2、Scrapy 和 Scrapy-redis 的区别
Scrapy 是一个通用的爬虫框架,不支持分布式。分布式爬取需要使用 Scrapy-redis。
Scrapy-redis 提供了下面四种组件(components):
- Scheduler
- Duplication Filter
- Item Pipeline
- Base Spider
在使用 Scrapy-redis 分布式爬取的时候需要对上述四个组件进行修改。
安装:
pip install scrapy-redis
3、Scrapy-redis 架构
如下图所示,Scrapy-redis 在 Scrapy 的架构上增加了 redis,基于 redis 的特性拓展了如 下组件:
(1)Scheduler
Scrapy 改造了 python 本来的 collection.deque(双向队列)形成了自己的 Scrapy queue ,但是 Scrapy 多个 spider 不能共享待爬取队列 Scrapy queue,即 Scrapy 本身不支持爬虫分布式,Scrapy-redis 的解决 是把这个 Scrapy queue 换成 redis 数据库(也是指 redis 队列),在同一个 redis-server 存放要 爬取的 request,便能让多个 spider 去同一个数据库里读取。
Scrapy 中跟“待爬队列”直接相关的就是调度器 Scheduler,它负责对新的 request 进行入 列操作(加入 Scrapy queue),取出下一个要爬取的 request(从 Scrapy queue 中取出)等操 作。它把待爬队列按照优先级建立了一个字典结构,比如:
{
优先级 0 : 队列 0
优先级 1 : 队列 1
优先级 2 : 队列 2
}
然后根据 request 中的优先级,来决定该入哪个队列,出列时则按优先级较小的优先出 列。为了管理这个比较高级的队列字典,Scheduler 需要提供一系列的方法。但是原来的 Scheduler 已经无法使用,所以使用 Scrapy-redis 的 Scheduler 组件。
(2)DupeFilter
Scrapy 中用集合实现这个 request 去重功能,Scrapy 中把已经发送的 request 指纹放入到 一个集合中,把下一个 request 的指纹拿到集合中比对,如果该指纹存在于集合中,说明这 个 request 发送过了,如果没有则继续操作。这个核心的判重功能是这样实现的。
在 Scrapy-redis 中去重是由 DupeFilter 组件来实现的,它通过 redis 的 set 不重复的特性, 巧妙的实现了 DupeFilter 去重。Scrapy-redis 调度器从引擎接受 request,将 request 的指纹存 ⼊redis 的 set 检查是否重复,并将不重复的 request push 写⼊redis 的 request queue。
引擎请求 request(Spider 发出的)时,调度器从 redis 的 request queue 队列里根据优先 级 pop 出⼀个 request 返回给引擎,引擎将此 request 发给 spider 处理。
(3)Item Pipeline
引擎将(Spider 返回的)爬取到的 Item 给 Item Pipeline,Scrapy-redis 的 Item Pipeline 将爬取到的 Item 存⼊redis 的 items queue。
修改过 Item Pipeline 可以很方便的根据 key 从 items queue 提取 item,从⽽实现 items processes 集群。
(4)Base Spider
不再使用 Scrapy 原有的 Spider 类,重写的 RedisSpider 继承了 Spider 和 RedisMixin 这 两个类,RedisMixin 是用来从 redis 读取 url 的类。
当我们生成一个 Spider 继承 RedisSpider 时,调用 setup_redis 函数,这个函数会去连接 redis 数据库,然后会设置 signals(信号):
- a、一个是当 spider 空闲时候的 signal,会调用 spider_idle 函数,这个函数调用 schedule_next_request 函数,保证 spider 是一直活着的状态,并且抛出 DontCloseSpider 异常。
- b、一个是当抓到一个 item 时的 signal,会调用 item_scraped 函数,这个函数会调用 schedule_next_request 函数,获取下一个 request。
总结
- 最后总结一下 Scrapy-redis 的总体思路:这套组件通过重写 scheduler 和 spider 类, 实现了调度、spider 启动和 redis 的交互。
- 实现新的 dupefilter 和 queue 类,达到了判重和调度容器和 redis 的交互,因为每个 主机上的爬虫进程都访问同一个 redis 数据库,所以调度和判重都进行统一管理, 达到了分布式爬虫的目的。
- 当 spider 被初始化时,同时会初始化一个对应的 scheduler 对象,这个调度器对象 通过读取 settings,配置好自己的调度容器 queue 和判重工具 dupefilter。
- 每当一个 spider 产出一个 request 的时候,Scrapy 引擎会把这个 reuqest 递交给这个 spider 对应的 scheduler 对象进行调度,scheduler 对象通过访问 redis 对 request 进行 判重,如果不重复就把他添加进 redis 中的调度器队列里。当调度条件满足时, scheduler 对象就从 redis 的调度器队列中取出一个 request 发送给 spider,让它爬取。
- 当 spider 爬取的所有暂时可用 url 之后,scheduler 发现这个 spider 对应的 redis 的调 度器队列空了,于是触发信号 spider_idle,spider 收到这个信号之后,直接连接 redis 读取 start_urls 池,拿取新的一批 url 入口,然后再次重复上边的工作。
(二 )Scrapy-Redis 源码分析
- 我们这边还是新建 Scrapy 项目,然后修改 settings
- 创建项目:Scrapy startproject ScrpayRedisTest
- 将 Scrapy-redis 的源码拷贝到 Scrapy 中,将拷贝路径为 Scrapy-redis/scr/srcapy-redis 拷贝到 Scrapy 中
Scrapy-redis 的官方文档写的比较简洁,没有提及其运行原理,所以如果想全面的理解分布 式爬虫的运行原理,还是得看 Scrapy-redis 的源代码才行。
Scrapy-redis 工程的主体还是是 redis 和 Scrapy 两个库,工程本身实现的东西不是很多,这个 工程就像胶水一样,把这两个插件粘结了起来。下面我们来看看,Scrapy-redis 的每一个源 代码文件都实现了什么功能,最后如何实现分布式的爬虫系统:
connection.py
负责根据 setting 中配置实例化 redis 连接。被 dupefilter 和 scheduler 调用,总之涉及到 redis 存取的都要使用到这个模块。
dupefilter.py
负责执行 requst 的去重,实现的很有技巧性,使用 redis 的 set 数据结构。但是注意 scheduler 并不使用其中用于在这个模块中实现的 dupefilter 键做 request 的调度,而是使用 queue.py 模块中实现的 queue。
当 request 不重复时,将其存入到 queue 中,调度时将其弹出。
这个文件看起来比较复杂,重写了 Scrapy 本身已经实现的 request 判重功能。因为本身 Scrapy 单机跑的话,只需要读取内存中的 request 队列或者持久化的 request 队列(Scrapy 默 认的持久化似乎是 json 格式的文件,不是数据库)就能判断这次要发出的 request url 是否已 经请求过或者正在调度(本地读就行了)。而分布式跑的话,就需要各个主机上的 scheduler 都连接同一个数据库的同一个 request 池来判断这次的请求是否是重复的了。
在这个文件中,通过继承 BaseDupeFilter 重写他的方法,实现了基于 redis 的判重。根 据源代码来看,Scrapy-redis 使用了 Scrapy 本身的一个 fingerprint 接 request_fingerprint,这个接口很有趣,根据 Scrapy 文档所说,他通过 hash 来判断两个 url 是否相同(相同的 url 会 生成相同的 hash 结果),但是当两个 url 的地址相同,get 型参数相同但是顺序不同时,也会 生成相同的 hash 结果(这个真的比较神奇…)所以 Scrapy-redis 依旧使用 url 的 fingerprint 来判 断 request 请求是否已经出现过。
这个类通过连接 redis,使用一个 key 来向 redis 的一个 set 中插入 fingerprint(这个 key 对于同一种 spider 是相同的,redis 是一个 key-value 的数据库,如果 key 是相同的,访问到 的值就是相同的,这里使用 spider 名字+DupeFilter 的 key 就是为了在不同主机上的不同爬虫 实例,只要属于同一种 spider,就会访问到同一个 set,而这个 set 就是他们的 url 判重池), 如果返回值为 0,说明该 set 中该 fingerprint 已经存在(因为集合是没有重复值的),则返回 False,如果返回值为 1,说明添加了一个 fingerprint 到 set 中,则说明这个 request 没有重复, 于是返回 True,还顺便把新 fingerprint 加入到数据库中了。 DupeFilter 判重会在 scheduler 类中用到,每一个 request 在进入调度之前都要进行判重,如果重复就不需要参加调度,直 接舍弃就好了,不然就是白白浪费资源
picklecompat.py
这里实现了 loads 和 dumps 两个函数,其实就是实现了一个序列化器。
因为 redis 数据库不能存储复杂对象(key 部分只能是字符串,value 部分只能是字符串, 字符串列表,字符串集合和 hash),所以我们存啥都要先串行化成文本才行。
这里使用的就是 python 的 pickle 模块,一个兼容 py2 和 py3 的串行化工具。这个 serializer 主要用于一会的 scheduler 存 reuqest 对象。
pipelines.py
这是是用来实现分布式处理的作用。它将 Item 存储在 redis 中以实现分布式处理。由于 在这里需要读取配置,所以就用到了 from_crawler()函数。
pipelines 文件实现了一个 item pipieline 类,和 Scrapy 的 item pipeline 是同一个对象,通 过从 settings 中拿到我们配置的 REDIS_ITEMS_KEY 作为 key,把 item 串行化之后存入 redis 数据库对应的 value 中(这个 value 可以看出出是个 list,我们的每个 item 是这个 list 中的一 个结点),这个 pipeline 把提取出的 item 存起来,主要是为了方便我们延后处理数据。
queue.py
该文件实现了几个容器类,可以看这些容器和 redis 交互频繁,同时使用了我们上边 picklecompat 中定义的序列化器。这个文件实现的几个容器大体相同,只不过一个是队列, 一个是栈,一个是优先级队列,这三个容器到时候会被 scheduler 对象实例化,来实现 request 的调度。比如我们使用 SpiderQueue 最为调度队列的类型,到时候 request 的调度方法就是 先进先出,而实用 SpiderStack 就是先进后出了。
从 SpiderQueue 的实现看出来,他的 push 函数就和其他容器的一样,只不过 push 进去 的 request 请求先被 Scrapy 的接口 request_to_dict 变成了一个 dict 对象(因为 request 对象实在 是比较复杂,有方法有属性不好串行化),之后使用 picklecompat 中的 serializer 串行化为字 符串,然后使用一个特定的 key 存入 redis 中(该 key 在同一种 spider 中是相同的)。而调用 pop 时,其实就是从 redis 用那个特定的 key 去读其值(一个 list),从 list 中读取最早进去的那个,于是就先进先出了。 这些容器类都会作为 scheduler 调度 request 的容器,scheduler 在 每个主机上都会实例化一个,并且和 spider 一一对应,所以分布式运行时会有一个 spider 的多个实例和一个 scheduler 的多个实例存在于不同的主机上,但是,因为 scheduler 都是用 相同的容器,而这些容器都连接同一个 redis 服务器,又都使用 spider 名加 queue 来作为 key 读写数据,所以不同主机上的不同爬虫实例公用一个 request 调度池,实现了分布式爬虫之 间的统一调度。
scheduler.py
此扩展是对 Scrapy 中自带的 scheduler 的替代(在 settings 的 SCHEDULER 变量中指出), 正是利用此扩展实现 crawler 的分布式调度。其利用的数据结构来自于 queue 中实现的数据 结构。
Scrapy-redis 所实现的两种分布式:爬虫分布式以及 item 处理分布式就是由模块 scheduler 和模块 pipelines 实现。上述其它模块作为为二者辅助的功能模块
这个文件重写了 scheduler 类,用来代替 Scrapy.core.scheduler 的原有调度器。其实对原 有调度器的逻辑没有很大的改变,主要是使用了 redis 作为数据存储的媒介,以达到各个爬 虫之间的统一调度。 scheduler 负责调度各个 spider 的 request 请求,scheduler 初始化时,通 过 settings 文件读取 queue 和 dupefilters 的类型(一般就用上边默认的),配置 queue 和 dupefilters使用的key(一般就是spider name加上queue或者dupefilters,这样对于同一种spider 的不同实例,就会使用相同的数据块了)。每当一个 request 要被调度时,enqueue_request 被 调用,scheduler 使用 dupefilters 来判断这个 url 是否重复,如果不重复,就添加到 queue 的 容器中(先进先出,先进后出和优先级都可以,可以在 settings 中配置)。当调度完成时, next_request 被调用,scheduler 就通过 queue 容器的接口,取出一个 request,把他发送给相 应的 spider,让 spider 进行爬取工作。
spider.py
设计的这个 spider 从 redis 中读取要爬的 url,然后执行爬取,若爬取过程中返回更多的 url,那么继续进行直至所有的 request 完成。之后继续从 redis 中读取 url,循环这个过程。
分析:在这个 spider 中通过 connect signals.spider_idle 信号实现对 crawler 状态的监视。 当 idle 时,返回新的 make_requests_from_url(url)给引擎,进而交给调度器调度。
spider 的改动也不是很大,主要是通过 connect 接口,给 spider 绑定了 spider_idle 信号, spider 初始化时,通过 setup_redis 函数初始化好和 redis 的连接,之后通过 next_requests 函 数从 redis 中取出 strat url,使用的 key 是 settings 中 REDIS_START_URLS_AS_SET 定义的(注 意了这里的初始化 url 池和我们上边的 queue 的 url 池不是一个东西,queue 的池是用于调度 的,初始化 url 池是存放入口 url 的,他们都存在 redis 中,但是使用不同的 key 来区分,就 当成是不同的表吧),spider 使用少量的 start url,可以发展出很多新的 url,这些 url 会进入 scheduler 进行判重和调度。直到 spider 跑到调度池内没有 url 的时候,会触发 spider_idle 信 号,从而触发 spider 的 next_requests 函数,再次从 redis 的 start url 池中读取一些 url。