爬虫scrapy-redis分布式
一、scrapy和scrapy-redis区别
1、Scrapy 是一个通用的爬虫框架,但是不支持分布式,
Scrapy-redis是为了更方便地实现Scrapy分布式爬取,而提供了一些以redis为基础的组件(仅有组件)。
安装:pip3 install scrapy-redis
2、Scrapy-redis提供了下面四种组件(components):(四种组件意味着这四个模块都要做相应的修改)
Scheduler
Duplication Filter
Item Pipeline
Base Spider
3、scrapy-redis框架:
1.框架图

2.如上图所示,scrapy-redis在scrapy的架构上增加了redis,基于redis的特性拓展了如下组件
a)Scheduler:
Scrapy改造了python本来的collection.deque(双向队列)形成了自己的Scrapy queue(https://github.com/scrapy/queuelib/blob/master/queuelib/queue.py)),
但是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已经无法使用,所以使用Scrapy-redis的scheduler组件。
b)Duplication Filter
Scrapy中用集合实现这个request去重功能,Scrapy中把已经发送的request指纹放入到一个集合中,把下一个request的指纹拿到集合中比对,
如果该指纹存在于集合中,说明这个request发送过了,如果没有则继续操作。这个核心的判重功能是这样实现的:
def request_seen(self, request):
# self.request_figerprints就是一个指纹集合
fp = self.request_fingerprint(request)
# 这就是判重的核心操作
if fp in self.fingerprints:
return True
self.fingerprints.add(fp)
if self.file:
self.file.write(fp + os.linesep)
在scrapy-redis中去重是由Duplication Filter组件来实现的,它通过redis的set不重复的特性,巧妙的实现了Duplication Filter去重。
scrapy-redis调度器从引擎接受request,将request的指纹存⼊redis的set检查是否重复,并将不重复的request push写⼊redis的 request queue。
引擎请求request(Spider发出的)时,调度器从redis的request queue队列里根据优先级pop出request返回给引擎,引擎将此request发给spider处理。
c)Item Pipeline:
引擎将(Spider返回的)爬取到的Item给Item Pipeline,scrapy-redis的Item Pipeline将爬取到的 Item 存在redis的items queue。
修改过Item Pipeline可以很方便的根据key从items queue 提取item,进而实现 items processes集群。
d)Base Spider
不在使用scrapy原有的Spider类,重写的RedisSpider继承了Spider和RedisMixin这两个类,RedisMixin是用来从redis读取url的类。
当我们生成一个Spider继承RedisSpider时,调用setup_redis函数,这个函数会去连接redis数据库,然后会设置signals(信号):
一个是当spider空闲时候的signal,会调用spider_idle函数,这个函数调用schedule_next_request函数,保证spider是一直活着的状态,并且抛出DontCloseSpider异常。
一个是当抓到一个item时的signal,会调用item_scraped函数,这个函数会调用schedule_next_request函数,获取下一个request。
二、官方文档
官方站点:https://github.com/rolando/scrapy-redis。scrapy-redis工程的主体还是是redis和scrapy两个库,工程本身实现的东西不是很多,
这个工程就像胶水一样,把这两个插件粘结了起来。下面我们来看看,scrapy-redis的每一个源代码文件都实现了什么功能,
最后如何实现分布式的爬虫系统:
1、connection.py
负责根据setting中配置实例化redis连接。被dupefilter和scheduler调用,总之涉及到redis存取的都要使用到这个模块 .
#代码见资料
2、dupefilter.py
负责执行requst的去重,实现的很有技巧性,使用redis的set数据结构。但是注意scheduler并不使用其中用于在这个模块中实现的dupefilter键做request的调度,
而是使用queue.py模块中实现的queue。当request不重复时,将其存入到queue中,调度时将其弹出。
#代码见资料
3、picklecompat.py
#代码见资料
这里实现了loads和dumps两个函数,其实就是实现了一个序列化器。因为redis数据库不能存储复杂对象(key部分只能是字符串,value部分只能是字符串,字符串列表,字符串集合和hash),
所以我们存啥都要先串行化成文本才行。
4、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存起来,主要是为了方便我们延后处理数据。
5、queue.py
该文件实现了几个容器类,可以看这些容器和redis交互频繁,同时使用了我们上边picklecompat中定义的序列化器。这个文件实现的几个容器大体相同,只不过一个是队列,一个是栈,一个是优先级队列,
这三个容器到时候会被scheduler对象实例化,来实现request的调度。比如我们使用SpiderQueue最为调度队列的类型,到时候request的调度方法就是先进先出,而实用SpiderStack就是先进后出了。
#代码见资料
6、scheduler.py
此扩展是对scrapy中自带的scheduler的替代(在settings的SCHEDULER变量中指出),正是利用此扩展实现crawler的分布式调度。其利用的数据结构来自于queue中实现的数据结构。
scrapy-redis所实现的两种分布式:爬虫分布式以及item处理分布式就是由模块scheduler和模块pipelines实现。上述其它模块作为为二者辅助的功能模块
#代码见资料
7、spider.py
设计的这个spider从redis中读取要爬的url,然后执行爬取,若爬取过程中返回更多的url,那么继续进行直至所有的request完成。之后继续从redis中读取url,循环这个过程。
分析:在这个spider中通过connect signals.spider_idle信号实现对crawler状态的监视。当idle时,返回新的make_requests_from_url(url)给引擎,进而交给调度器调度。
#代码见资料
总结:
最后总结一下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读取strart url池,拿去新的一批url入口,然后再次重复上边的工作。
三、scrapy-redis分布式
1、分布式策略
假如有5台电脑,其中一台作为Master端,其余作为Slaver端
Master端(核心服务器):搭建一个Redis数据库,不负责爬取,只负责url指纹判重、Request的分配,以及数据的存储
Slaver端(爬虫程序执行端):负责执行爬虫程序,运行过程中提交新的Request给Master
2、流程图
1.见图1
2.过程
首先Slaver端从Master端拿任务(Request、url)进行数据抓取,Slaver抓取数据的同时,产生新任务的Request便提交给 Master 处理;
Master端只有一个Redis数据库,负责将未处理的Request去重和任务分配,将处理后的Request加入待爬队列,并且存储爬取的数据。
Scrapy-Redis默认使用的就是这种策略,我们实现起来很简单,因为任务调度等工作Scrapy-Redis都已经帮我们做好了,我们只需要继承RedisSpider、指定redis_key就行了。
缺点是,Scrapy-Redis调度的任务是Request对象,里面信息量比较大(不仅包含url,还有callback函数、headers等信息),
可能导致的结果就是会降低爬虫速度、而且会占用Redis大量的存储空间,所以如果要保证效率,那么就需要一定硬件水平。
3、测试Slave端远程连接Master端
测试中,Master端的IP地址为:192.168.199.108
1.Master端按指定配置文件启动 redis-server
非Windows系统:sudo redis-server /etc/redis/redis/conf
Windows系统:命令提示符(管理员)模式下执行 redis-server C:\Intel\Redis\conf\redis.conf读取默认配置即可。
2.Master端启动本地redis-cli
3.slave端启动redis-cli -h 192.168.199.108,-h 参数表示连接到指定主机的redis数据库
#注意
Slave端无需启动redis-server,Master端启动即可。只要 Slave 端读取到了 Master 端的 Redis 数据库,则表示能够连接成功,可以实施分布式
4、Redis数据库桌面管理工具
推荐 Redis Desktop Manager。
下载地址:https://redisdesktop.com/download
#见图2和图3



四、scrapy-redis分布式项目
1、源码自带项目说明
使用scrapy-redis的example来修改
先从github上拿到scrapy-redis的示例,然后将里面的example-project目录移到指定的地址:
# clone github scrapy-redis源码文件
git clone https://github.com/rolando/scrapy-redis.git
# 直接拿官方的项目范例,改名为自己的项目用(针对懒癌患者)
mv scrapy-redis/example-project ~/scrapyredis-project
我们clone到的 scrapy-redis 源码中有自带一个example-project项目,
这个项目包含3个spider,分别是dmoz, myspider_redis,mycrawler_redis。
2、dmoz (class DmozSpider(CrawlSpider))
这个爬虫继承的是CrawlSpider,它是用来说明Redis的持续性,当我们第一次运行dmoz爬虫,然后Ctrl + C停掉之后,
再运行dmoz爬虫,之前的爬取记录是保留在Redis里的。分析起来,其实这就是一个 scrapy-redis 版 CrawlSpider 类,
需要设置Rule规则,以及callback不能写parse()方法。
执行方式:scrapy crawl dmoz
3、myspider_redis (class MySpider(RedisSpider))
这个爬虫继承了RedisSpider, 它能够支持分布式的抓取,采用的是basic spider,需要写parse函数。
其次就是不再有start_urls了,取而代之的是redis_key,scrapy-redis将key从Redis里pop出来,成为请求的url地址。
#加代码
#注意:
RedisSpider类 不需要写allowd_domains和start_urls:
1.scrapy-redis将从在构造方法__init__()里动态定义爬虫爬取域范围,也可以选择直接写allowd_domains。
2.必须指定redis_key,即启动爬虫的命令,参考格式:redis_key = 'myspider:start_urls'
3.根据指定的格式,start_urls将在 Master端的 redis-cli 里 lpush 到 Redis数据库里,RedisSpider 将在数据库里获取start_urls。
4、mycrawler_redis (class MyCrawler(RedisCrawlSpider))
这个RedisCrawlSpider类爬虫继承了RedisCrawlSpider,能够支持分布式的抓取。因为采用的是crawlSpider,所以需要遵守Rule规则,以及callback不能写parse()方法。
同样也不再有start_urls了,取而代之的是redis_key,scrapy-redis将key从Redis里pop出来,成为请求的url地址。
#执行方式:
1.通过runspider方法执行爬虫的py文件(也可以分次执行多条),爬虫(们)将处于等待准备状态:
scrapy runspider myspider_redis.py
2.在Master端的redis-cli输入push指令,参考格式:
$redis > lpush myspider:start_urls http://www.dmoz.org/
3.Slaver端爬虫获取到请求,开始爬取。
#总结
官方例题提供三种方法:
如果只是用到Redis的去重和保存功能,就选第一种;
如果要写分布式,则根据情况,选择第二种、第三种;
通常情况下,会选择用第三种方式编写深度聚焦爬虫。
##有缘网项目
存入redis(官方第一种)
1 #需要进行设置的选项,其他均默认 2 ############################# 3 4 BOT_NAME = 'youyuan' 5 SPIDER_MODULES = ['youyuan.spiders'] 6 NEWSPIDER_MODULE = 'youyuan.spiders' 7 #==============================================================================# 8 9 # Crawl responsibly by identifying yourself (and your website) on the user-agent 10 USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' 11 12 # Obey robots.txt rules 13 ROBOTSTXT_OBEY = False 14 #============================================================================================# 15 16 DEFAULT_REQUEST_HEADERS = { 17 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 18 # 'Accept-Language': 'en', 19 } 20 #==============================================================================# 21 22 ITEM_PIPELINES = { 23 'youyuan.pipelines.YouyuanPipeline': 300, 24 'scrapy_redis.pipelines.RedisPipeline' : 400, #对redis的设置。优先级要小于YouyuanPipeline 25 } 26 27 #==============================================================================# 28 29 #====================================redis存储开始============================# 30 31 # 使用了scrapy-redis里的去重组件,不使用scrapy默认的去重 32 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" 33 # 使用了scrapy-redis里的调度器组件,不实用scrapy默认的调度器 34 SCHEDULER = "scrapy_redis.scheduler.Scheduler" 35 # 允许暂停,redis请求记录不丢失 36 SCHEDULER_PERSIST = True 37 #延迟爬取时间 38 DOWNLOAD_DELAY = 1 39 40 #默认的scrapy请求(按优先级顺序)队列形式 41 42 #按sotred排序顺序出队列 43 #SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue" 44 #按栈形式,请求先进后出 45 SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderStack" 46 # 使用队列形式,请求先进先出 47 SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderQueue" 48 49 #下面的设置,如果存在本机数据库,可以不进行设置。 50 #REDIS_HOST = '10.12.159.130' 51 #REDIS_PORT = 6379 52 53 #====================================redis存储结束=================================#
1 from scrapy import Field, Item 2 3 class YouyuanItem(Item): 4 # 用户名 5 username = Field() 6 # 年龄 7 age = Field() 8 # 头像图片的链接 9 header_url = Field() 10 # 相册图片的链接 11 images_url = Field() 12 # 内心独白 13 content = Field() 14 # 籍贯 15 place_from = Field() 16 # 学历 17 education = Field() 18 # 兴趣爱好 19 hobby = Field() 20 # 个人主页 21 source_url = Field() 22 # 数据来源网站 23 sourec = Field()
1 ##存入当地,保存成json文件 2 import json 3 4 class YouyuanPipeline(object): 5 def open_spider(self,spider): 6 self.fw = open("youyuan.json", "w",encoding="utf-8") 7 8 def process_item(self, item, spider): 9 content = json.dumps(dict(item), ensure_ascii=False) + "\n" 10 self.fw.write(content) 11 return item 12 13 def close_spider(self, spider): 14 self.fw.close()
1 # -*- coding: utf-8 -*- 2 import scrapy 3 from scrapy.linkextractors import LinkExtractor 4 from scrapy.spiders import CrawlSpider, Rule 5 from youyuan.items import YouyuanItem 6 7 import re 8 9 class YySpider(CrawlSpider): 10 name = 'yy' 11 allowed_domains = ['youyuan.com'] 12 start_urls = ['http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/'] 13 14 # 第一级匹配规则:北京市18~25岁女性的每一页链接匹配规则 15 page_links = LinkExtractor(allow = (r"youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p\d+/")) 16 # 第二级匹配规则:每个女性个人主页的匹配规则 17 profile_links = LinkExtractor(allow = (r"youyuan.com/\d+-profile/")) 18 19 rules = ( 20 Rule(page_links,follow=True), 21 Rule(profile_links, callback = "parse_item",follow=False), 22 ) 23 24 def parse_item(self, response): 25 item = YouyuanItem() 26 27 item['username'] = self.get_username(response) 28 # 年龄 29 item['age'] = self.get_age(response) 30 # 头像图片的链接 31 item['header_url'] = self.get_header_url(response) 32 # 相册图片的链接 33 item['images_url'] = self.get_images_url(response) 34 # 内心独白 35 item['content'] = self.get_content(response) 36 # 籍贯 37 item['place_from'] = self.get_place_from(response) 38 # 学历 39 item['education'] = self.get_education(response) 40 # 兴趣爱好 41 item['hobby'] = self.get_hobby(response) 42 # 个人主页 43 item['source_url'] = response.url #每个人的主页不需要再xpath匹配规则,直接response.url获取即可 44 # 数据来源网站 45 item['sourec'] = "youyuan" 46 47 yield item 48 49 def get_username(self, response): 50 username = response.xpath("//dl[@class='personal_cen']//div[@class='main']/strong/text()").extract_first() 51 return username.strip() 52 53 54 def get_age(self, response): 55 age = response.xpath("//dl[@class='personal_cen']//dd/p/text()").extract() 56 if len(age): 57 age = re.findall(u"\d+岁", age[0])[0] 58 else: 59 age = "NULL" 60 return age.strip() 61 62 def get_header_url(self, response): 63 header_url = response.xpath("//dl[@class='personal_cen']/dt/img/@src").extract_first() 64 return header_url.strip() 65 66 67 def get_images_url(self, response): 68 images_url = response.xpath("//div[@class='ph_show']/ul/li/a/img/@src").extract() 69 if len(images_url): 70 images_url = images_url 71 else: 72 images_url = "NULL" 73 return images_url 74 75 def get_content(self, response): 76 content = response.xpath("//div[@class='pre_data']/ul/li/p/text()").extract_first() 77 return content.strip() 78 79 def get_place_from(self, response): 80 place_from = response.xpath("//div[@class='pre_data']/ul/li[2]//ol[1]/li[1]/span/text()").extract_first() 81 return place_from.strip() 82 83 def get_education(self, response): 84 education = response.xpath("//div[@class='pre_data']/ul/li[3]//ol[2]/li[2]/span/text()").extract_first() 85 return education.strip() 86 87 88 def get_hobby(self, response): 89 hobby = response.xpath("//dl[@class='personal_cen']//ol/li/text()").extract() 90 if len(hobby): 91 hobby = ",".join(hobby).replace(" ","") #可以将所有的爱好(在不同的标签下),拼接成一个完整字符串, 92 #replace的作用是,将 " " (空格) ---> ""(空字符串) 93 else: 94 hobby = "NULL" 95 return hobby.strip() #strip去除左右两端的空格
有缘网项目分布式,需要更改的文件(第二种、第三种)
1 # -*- coding: utf-8 -*- 2 import scrapy 3 from scrapy.linkextractors import LinkExtractor 4 #from scrapy.spiders import CrawlSpider, Rule 5 from scrapy.spiders import Rule 6 from scrapy_redis.spiders import RedisCrawlSpider 7 from youyuan.items import YouyuanItem 8 9 import re 10 11 #class YySpider(CrawlSpider): 12 class YySpider(RedisCrawlSpider): 13 name = 'yy' 14 #allowed_domains = ['youyuan.com'] 15 #start_urls = ['http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/'] 16 redis_key = "yyspider:start_urls" 17 18 # 动态域范围获取 19 def __init__(self, *args, **kwargs): 20 # Dynamically define the allowed domains list. 21 domain = kwargs.pop('domain', '') 22 self.allowed_domains = filter(None, domain.split(',')) 23 super(YySpider, self).__init__(*args, **kwargs) 24 25 # 第一级匹配规则:北京市18~25岁女性的每一页链接匹配规则 26 page_links = LinkExtractor(allow = (r"youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p\d+/")) 27 # 第二级匹配规则:每个女性个人主页的匹配规则 28 profile_links = LinkExtractor(allow = (r"youyuan.com/\d+-profile/")) 29 30 rules = ( 31 Rule(page_links), 32 Rule(profile_links, callback = "parse_item"), 33 ) 34 35 def parse_item(self, response): 36 item = YouyuanItem() 37 38 item['username'] = self.get_username(response) 39 # 年龄 40 item['age'] = self.get_age(response) 41 # 头像图片的链接 42 item['header_url'] = self.get_header_url(response) 43 # 相册图片的链接 44 item['images_url'] = self.get_images_url(response) 45 # 内心独白 46 item['content'] = self.get_content(response) 47 # 籍贯 48 item['place_from'] = self.get_place_from(response) 49 # 学历 50 item['education'] = self.get_education(response) 51 # 兴趣爱好 52 item['hobby'] = self.get_hobby(response) 53 # 个人主页 54 item['source_url'] = response.url #每个人的主页不需要再xpath匹配规则,直接response.url获取即可 55 # 数据来源网站 56 item['sourec'] = "youyuan" 57 58 yield item 59 60 def get_username(self, response): 61 username = response.xpath("//dl[@class='personal_cen']//div[@class='main']/strong/text()").extract_first() 62 return username.strip() 63 64 65 def get_age(self, response): 66 age = response.xpath("//dl[@class='personal_cen']//dd/p/text()").extract() 67 if len(age): 68 age = re.findall(u"\d+岁", age[0])[0] 69 else: 70 age = "NULL" 71 return age.strip() 72 73 def get_header_url(self, response): 74 header_url = response.xpath("//dl[@class='personal_cen']/dt/img/@src").extract_first() 75 return header_url.strip() 76 77 78 def get_images_url(self, response): 79 images_url = response.xpath("//div[@class='ph_show']/ul/li/a/img/@src").extract() 80 if len(images_url): 81 images_url = images_url 82 else: 83 images_url = "NULL" 84 return images_url 85 86 def get_content(self, response): 87 content = response.xpath("//div[@class='pre_data']/ul/li/p/text()").extract_first() 88 return content.strip() 89 90 def get_place_from(self, response): 91 place_from = response.xpath("//div[@class='pre_data']/ul/li[2]//ol[1]/li[1]/span/text()").extract_first() 92 return place_from.strip() 93 94 def get_education(self, response): 95 education = response.xpath("//div[@class='pre_data']/ul/li[3]//ol[2]/li[2]/span/text()").extract_first() 96 return education.strip() 97 98 99 def get_hobby(self, response): 100 hobby = response.xpath("//dl[@class='personal_cen']//ol/li/text()").extract() 101 if len(hobby): 102 hobby = ",".join(hobby).replace(" ","") #可以将所有的爱好(在不同的标签下),拼接成一个完整字符串, 103 #replace的作用是,将 " " (空格) ---> ""(空字符串) 104 else: 105 hobby = "NULL" 106 return hobby.strip() #strip去除左右两端的空格
更改地方除了主文件的前半部分,还有setting.py
#下面的设置,如果存在本机数据库,可以不进行设置。 #host表示作为主机的地址。其他机器需要和这个主机进行连接 #REDIS_HOST = '10.12.159.130' #REDIS_PORT = 6379
五、将项目存入MySQL
后续更新。。。。。。。。。。。。。。。。。

浙公网安备 33010602011771号