前言
当我们了解了Scrapy爬虫框架的用法过后,这些框架都是在同一台主机上运行的,爬取效率比较有限。如果能够用多台主机协同爬取,那么爬取效率必然会成倍增长,这就是分布式爬虫的优势。这里我们就来了解一下分布式爬虫的基本原理,以及 Scrapy 实现分布式爬虫的流程。
分布式爬虫理念
1.分布式爬虫框架
Scrapy 单机爬虫中有一个本地爬取队列 Queue,这个队列是利用 deque 模块实现的。新的 Request 生成就会被放到队列里,随后被调度器 Scheduler 调度,交给Downloader 执行爬取;
如果两个 Scheduler 同时从队列里面取 Request,每个 Scheduler 都有其对应的 Downloader,那么在带宽足够、正常爬取且不考虑队列存取压力的情况下,爬取效率会有什么变化?没错,爬取效率会翻倍。
这样,Scheduler 可以扩展多个,Downloader 也可以扩展多个。而爬取队列 Queue 必须始终为-个,也就是所谓的共享爬取队列。这样才能保证 Scheduler 从队列里调度某个 Request后,其他 Scheduler不会重复调度此 Request,就可以做到多个 Scheduler 同步爬取了。这就是分布式爬虫的基本雏形,
我们需要做的就是在多台主机上同时运行爬虫任务协同爬取,而协同爬取的前提就是共享爬取队列。这样各台主机就不需要各自维护爬取队列,从共享爬取队列存取 Request 就行了。但是各台主机还是有各自的 Scheduler 和 Downloader,所以调度和下载功能分别完成。如果不考虑队列存取性能消耗,爬取效率还是会成倍提高。
2.维护爬取队列
爬取队列怎样维护比较好呢?我们首先需要考虑的就是性能问题,什么数据库存取效率高?我们自然能想到基于内存存储的 Redis,而且Redis 支持多种数据结构,例如列表(List)、集合(set)、有序集合(Sorted Set)等,存取的操作也非常简单,所以在这里我们采用 Redis 来维护爬取队列。
3.去重
Scrapy有自动去重功能,它的去重使用了Python中的集合。这个集合记录了Scrapy中每个Reques的指纹,这个指纹实际上就是 Request的散列值。
那么要实现去重,这个指纹集合也需要是共享的。Redis 正好有集合的存储数据结构,我们可以利用 Redis 的集合作为指纹集合,那么这样去重集合也是利用Redis 共享的。每台主机新生成 Request后,把该Request的指纹与集合比对,如果指纹已经存在,说明该Request是重复的,否则将 Reques的指纹加人这个集合。利用同样的原理,我们在不同的存储结构中实现了分布式 Regeust 的去重。
4.防止中断
在 Scrapy 中,爬虫运行时的 Request队列放在内存中。爬虫运行中断后,这个队列的空间就被释
放,此队列就销毁了。所以一旦爬虫运行中断,爬虫再次运行就相当于全新的爬取过程。要做到中断后继续爬取,我们可以将队列中的 Request 保存起来,下次爬取直接读取保存数据即可获取上次爬取的队列。我们在Scrapy中指定一个爬取队列的存储路径即可,这个路径使用J0B_DIR变量来标识,可以用如下命令来实现:
scrapy crawl spider -s JOBDIR=crawls/spider
更加详细的使用方法可以参见官方文档:https://doc.scrapy.org/en/latesttopics/jobs.html.
在 Scrapy 中,我们实际是把爬取队列保存到本地,第二次爬取直接读取并恢复队列。那么在分布式架构中,我们还用担心这个问题吗?不需要。因为爬取队列本身就是用数据库保存的,如果爬虫中断了,数据库中的 Request 依然存在,下次启动就会接着上次中断的地方继续爬取。
所以,当 Redis 的队列为空时,爬虫会重新爬取;当 Redis的队列不为空时,爬虫便会接着上次中断之处继续爬取。
5.架构实现
我们接下来就需要在程序中实现这个架构了。首先实现一个共享的爬取队列,还要实现去重的功能。另外,重写一个 Scheduer 的实现,使之可以从共享的爬取队列中存取 Request。
幸运的是,已经有人实现了这些逻辑和架构,并发布成叫Scrapy-Redis的Python 包。
我们可以使用pip下载它
pip install Scrapy-Redis
这样,我们把之前说的3个分布式的问题解决了,总结如下。
- 爬取队列的实现:这里提供了3种队列,使用Redis的列表或有序集合来维护
- 去重的实现:这里使用 Redis 的集合来保存Request 指纹,以提供重复过滤。
- 中断后重新爬取的实现:中断后Redis的队列没有清空,再次启动时调度器的nextrequest 会从队列中取到下一个 Request,继续爬取。
基于Scrapy-Redis的分布式爬虫实现
1.准备工作
在开始前我们需要下载我们爬虫所需要用到的包以及我们需要用到的Redis数据库和实现分布式的至少两台机器,例如这里我有三台机器,这三台主机处于同一个局域网下,其代号和 IP 地址为分别为 A 主机(192.168.0.2 )、B 主机(192.168.0.3)、C 主机(192.168.0.4),这三台主机均安装好了 Python 环境并能正常运行如上 Scrapy爬虫项目。另外,在A主机上需要正常运行 Redis 数据库;并且 Redis数据库能够被B主机和C主机正常连接,接下来我就以这三台主机为示例来配置分布式爬虫。
所需包:
Scrapy
Scrapy-Redis
redis
2.验证Redis连接
首先在 A 主机上运行 Redis 数据库,该 Redis数据库没有设置密码,运行在 6379 端口上,因此连接地址就是 192.168.0.2:6379,在 B主机和 C 主机上可以使用如下代码验证 Redis 能否正常连接:
from redis import StrictRedis
conn = StrictRedis(host='192.168.0.2',port=6379)print(conn.ping())
如果 Redis 能正常连接,那么输出结果如下:
True
如果无法正常连接,则会直接报错,错误信息类似如下:
redis.exceptions.ConnectionError: Erro 61 connecting to 192.168.0.2:6379. Connection refused.
该步骤需要确保B主机和C主机能正常连接A主机运行的 Redis数据库。如不能连接成功,请检查 A 主机上的 Redis 服务是否正常运行或者检査防火墙、安全组相关设置。
3.配置Scrapy-Redis
到此为止,A 主机提供了 Redis 数据库服务,接下来我们只需要在代码里面修改一下调用方式就可以完成分布式爬虫的配置了,整个配置流程非常简单,只需要修改一下 settings.py 配置文件即可。
注意,这些配置需要分别在 A、B、C 主机上配置,内容完全一致。
核心配置
首先最主要的是将调度器的类和去重的类替换为 Scrapy-Redis 提供的类,在 settings.py 里面添加如下配置即可:
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
Redis连接配置
接下来配置 Redis 的连接信息,这里有两种配置方式。
第一种方式是通过连接字符串配置。我们可以用 Redis 的地址、端口、密码来构造一个 Redis 连接字符串,支持的连接形式如下所示:
redis://[:passwordl@host:port/dbrediss://[:password]@host:port/db
unix://:password]@/path/to/socket.sock?db=db
password是密码,需要以冒号开头,中括号代表此选项可有可无,host是Redis的地址,port是运行端口,db 是数据库代号,其值默认是 0。
这里我们配置为 A 主机的 Redis 连接信息,该 Redis 是没有配置密码的,构造这个 Redis 的连接字符串如下所示:
redis://192.168.0.2:6379
直接在 setings.py 里面配置为REDIS_URL变量即可:
REDIS_URL = 'redis://192.168.0.2:6379'
第二种配置方式是分项单独配置。这个配置就更加直观了,如根据我的 Redis 连接信息,可以在settings.py 中配置如下代码:
REDIS_HOST = '192.168.0.2'
REDIS_PORT = 6379
REDIS_PASSWORD = None
这段代码分开配置了 Redis 的地址、端口和密码,密码为空。
注意,如果配置了 REDIS_URL,那么 Scrapy-Redis将优先使用REDIS_URL连接,会覆盖上面的3项配置。如果想要分项单独配置,请不要配置 REDIS_URL。
项,在本项目中,我们选择的是配置 REDIS_URL。
配置持久化
此配置是可选的,默认是False。Scrapy-Redis 默认会在爬取全部完成后清空爬取队列和去重指纹
集合。
如果不想自动清空爬取队列和去重指纹集合,可以增加如下配置:
SCHEDULER_PERSIST = True
将 SCHEDULER PERSIST 设置为 True 之后,爬取队列和去重指纹集合不会在爬取完成后自动清空,如果不配置,默认是 False,即自动清空。
值得注意的是,如果强制中断爬虫的运行,爬取队列和去重指纹集合是不会自动清空的。
在本项目中不进行任何配置,我们使用默认配置。(我们在调试阶段可以设置为False,用来查看Redis中是否出现了我们的调度队列以及指纹集合。)
配置重爬
此配置是可选的,默认是 False。如果配置了持久化或者强制中断了爬虫,那么爬取队列和指纹集合不会被清空,爬虫重新启动之后就会接着上次爬取。如果想重新爬取,我们可以配置重爬的选项,
SCHEDULER_FLUSH_ON_START = True
这样将 SCHEDULER_FLUSH_ON_START设置为True之后,爬虫每次启动时,爬取队列和指纹集合都会清空。所以要做分布式爬取,我们必须保证只能清空一次,否则每个爬虫任务在启动时都清空一次,就会把之前的爬取队列清空,势必会影响分布式爬取。
注意,此配置在单机爬取的时候比较方便,分布式爬取不常用此配置。
在本项目中不进行任何配置,我们使用默认配置。
Pipeline配置
此配置是可选的,默认不启动 Pipeline。Scrapy-Redis 实现了一个存储到 Redis 的 ltem pipeline。
如果启用了这个 Pipeline,爬虫会把生成的 ítem 存储到 Redis 数据库中。在数据量比较大的情况下.我们一般不会这么做。因为 Redis 是基于内存的,我们利用的是它处理速度快的特性,用它来做存储未免太浪费了,配置如下:
ITEM_PIPELINES ={'scrapy_redis.pipelines.RedisPipeline': 300)
本项目不进行任何配置,即不启动 Pipeline。
若我们需要将爬虫数据内容写到Kafka内我们可以在此文件中写一个管道用来想Kafka中传输数据。
spider配置
让爬虫继承scrapy-redis中的爬虫继承类(原先是继承scrapy.Spider)
from scrapy_redis.spiders import RedisSpider
到此为止,Scrapy-Redis 的配置就完成了。有的选项我们没有配置,但是这些配置在其他 Scrapy 项目中可能会用到,要根据具体情况而定。
4.运行
这里不再举例爬虫spider文件的爬虫内容,读者只要可以运行自己的Scrapy有爬虫的内容即可,需要有自己的spider和item配置。以上修改需要同时在 A、B、C三台主机上执行,三台主机上的代码是完全一样的。修改完毕后我们便完成了分布式爬虫的配置了,这样三台主机就共享了同一个 Redis 爬取队列。
接下来我们就可以运行一下实现分布式爬取了,我们配置好分布式爬虫后我们需要在三台机器分别运行我们的爬虫
scrapy crawl spider
主机的运行顺序不分先后,每台主机启动了此命令后,就会从配置的A主机的 Redis 数据库中调度 Request 并利用 Request 的指纹集合进行去重过滤。同时每台主机占用各自的带宽和处理器,不会互相影响,爬取效率成倍提高。
随着时间的推移,指纹集合会不断增长,爬取队列会动态变化。我们可以在Redis数据库中检查是否有数据产生。
另外值得注意的是,在爬取的过程中,去重指纹集合是不断增长的,如果中途想要中断所有的Spider 重新进行爬取,需要先停止所有 Spider,然后手动从 Redis 中删除指纹集合和爬取队列,再重新运行。
至此,Scrapy分布式的配置已全部完成,通过简单的配置,我们就完成了多主机多Spider 的协同爬取。
注:以上内容源来于我在Python3网络爬虫中内容以及加上我自己实现后的编改。