scrapy与redis实战
Posted qichueng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了scrapy与redis实战相关的知识,希望对你有一定的参考价值。
从零搭建Redis-Scrapy分布式爬虫
Scrapy-Redis分布式策略:
假设有四台电脑:Windows 10、Mac OS X、Ubuntu 16.04、CentOS 7.2,任意一台电脑都可以作为 Master端 或 Slaver端,比如:
-
Master端
(核心服务器) :使用 Windows 10,搭建一个Redis数据库,不负责爬取,只负责url指纹判重、Request的分配,以及数据的存储 -
Slaver端
(爬虫程序执行端) :使用 Mac OS X 、Ubuntu 16.04、CentOS 7.2,负责执行爬虫程序,运行过程中提交新的Request给Master
-
首先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大量的存储空间,所以如果要保证效率,那么就需要一定硬件水平。
一、安装Redis
安装Redis:http://redis.io/download
安装完成后,拷贝一份Redis安装目录下的redis.conf到任意目录,建议保存到:/etc/redis/redis.conf
(Windows系统可以无需变动)
二、修改配置文件 redis.conf
打开你的redis.conf配置文件,示例:
-
非Windows系统:
sudo vi /etc/redis/redis.conf
-
Windows系统:
C:\\Intel\\Redis\\conf\\redis.conf
-
Master端redis.conf里注释
bind 127.0.0.1
,Slave端才能远程连接到Master端的Redis数据库。
2.daemonize yno
表示Redis默认不作为守护进程运行,即在运行redis-server /etc/redis/redis.conf
时,将显示Redis启动提示画面;
-
daemonize yes
则默认后台运行,不必重新启动新的终端窗口执行其他命令,看个人喜好和实际需要。
三、测试Slave端远程连接Master端
测试中,Master端Windows 10 的IP地址为:192.168.199.108
-
Master端按指定配置文件启动
redis-server
,示例:-
非Windows系统:
sudo redis-server /etc/redis/redis.conf
-
Windows系统:
命令提示符(管理员)
模式下执行redis-server C:\\Intel\\Redis\\conf\\redis.conf
读取默认配置即可。
-
-
Master端启动本地
redis-cli
:
3.slave端启动redis-cli -h 192.168.199.108
,-h 参数表示连接到指定主机的redis数据库
注意:Slave端无需启动redis-server
,Master端启动即可。只要 Slave 端读取到了 Master 端的 Redis 数据库,则表示能够连接成功,可以实施分布式。
四、Redis数据库桌面管理工具
这里推荐 Redis Desktop Manager,支持 Windows、Mac OS X、Linux 等平台:
源码自带项目说明:
使用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。
一、dmoz (class DmozSpider(CrawlSpider))
这个爬虫继承的是CrawlSpider,它是用来说明Redis的持续性,当我们第一次运行dmoz爬虫,然后Ctrl + C停掉之后,再运行dmoz爬虫,之前的爬取记录是保留在Redis里的。
分析起来,其实这就是一个 scrapy-redis 版 CrawlSpider
类,需要设置Rule规则,以及callback不能写parse()方法。
执行方式:scrapy crawl dmoz
from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class DmozSpider(CrawlSpider): """Follow categories and extract links.""" name = \'dmoz\' allowed_domains = [\'dmoz.org\'] start_urls = [\'http://www.dmoz.org/\'] rules = [ Rule(LinkExtractor( restrict_css=(\'.top-cat\', \'.sub-cat\', \'.cat-item\') ), callback=\'parse_directory\', follow=True), ] def parse_directory(self, response): for div in response.css(\'.title-and-desc\'): yield { \'name\': div.css(\'.site-title::text\').extract_first(), \'description\': div.css(\'.site-descr::text\').extract_first().strip(), \'link\': div.css(\'a::attr(href)\').extract_first(), }
二、myspider_redis (class MySpider(RedisSpider))
这个爬虫继承了RedisSpider, 它能够支持分布式的抓取,采用的是basic spider,需要写parse函数。
其次就是不再有start_urls了,取而代之的是redis_key,scrapy-redis将key从Redis里pop出来,成为请求的url地址。
from scrapy_redis.spiders import RedisSpider class MySpider(RedisSpider): """Spider that reads urls from redis queue (myspider:start_urls).""" name = \'myspider_redis\' # 注意redis-key的格式: redis_key = \'myspider:start_urls\' # 可选:等效于allowd_domains(),__init__方法按规定格式写,使用时只需要修改super()里的类名参数即可 def __init__(self, *args, **kwargs): # Dynamically define the allowed domains list. domain = kwargs.pop(\'domain\', \'\') self.allowed_domains = filter(None, domain.split(\',\')) # 修改这里的类名为当前类名 super(MySpider, self).__init__(*args, **kwargs) def parse(self, response): return { \'name\': response.css(\'title::text\').extract_first(), \'url\': response.url, }
注意:
RedisSpider类 不需要写allowd_domains
和start_urls
:
-
scrapy-redis将从在构造方法
__init__()
里动态定义爬虫爬取域范围,也可以选择直接写allowd_domains
。 -
必须指定redis_key,即启动爬虫的命令,参考格式:
redis_key = \'myspider:start_urls\'
-
根据指定的格式,
start_urls
将在 Master端的 redis-cli 里 lpush 到 Redis数据库里,RedisSpider 将在数据库里获取start_urls。
执行方式:
-
通过runspider方法执行爬虫的py文件(也可以分次执行多条),爬虫(们)将处于等待准备状态:
scrapy runspider myspider_redis.py
-
在Master端的redis-cli输入push指令,参考格式:
$redis > lpush myspider:start_urls http://www.dmoz.org/
-
Slaver端爬虫获取到请求,开始爬取。
三、mycrawler_redis (class MyCrawler(RedisCrawlSpider))
这个RedisCrawlSpider类爬虫继承了RedisCrawlSpider,能够支持分布式的抓取。因为采用的是crawlSpider,所以需要遵守Rule规则,以及callback不能写parse()方法。
同样也不再有start_urls了,取而代之的是redis_key,scrapy-redis将key从Redis里pop出来,成为请求的url地址。
from scrapy.spiders import Rule from scrapy.linkextractors import LinkExtractor from scrapy_redis.spiders import RedisCrawlSpider class MyCrawler(RedisCrawlSpider): """Spider that reads urls from redis queue (myspider:start_urls).""" name = \'mycrawler_redis\' redis_key = \'mycrawler:start_urls\' rules = ( # follow all links Rule(LinkExtractor(), callback=\'parse_page\', follow=True), ) # __init__方法必须按规定写,使用时只需要修改super()里的类名参数即可 def __init__(self, *args, **kwargs): # Dynamically define the allowed domains list. domain = kwargs.pop(\'domain\', \'\') self.allowed_domains = filter(None, domain.split(\',\')) # 修改这里的类名为当前类名 super(MyCrawler, self).__init__(*args, **kwargs) def parse_page(self, response): return { \'name\': response.css(\'title::text\').extract_first(), \'url\': response.url, }
注意:
同样的,RedisCrawlSpider类不需要写allowd_domains
和start_urls
:
-
scrapy-redis将从在构造方法
__init__()
里动态定义爬虫爬取域范围,也可以选择直接写allowd_domains
。 -
必须指定redis_key,即启动爬虫的命令,参考格式:
redis_key = \'myspider:start_urls\'
-
根据指定的格式,
start_urls
将在 Master端的 redis-cli 里 lpush 到 Redis数据库里,RedisSpider 将在数据库里获取start_urls。
执行方式:
-
通过runspider方法执行爬虫的py文件(也可以分次执行多条),爬虫(们)将处于等待准备状态:
scrapy runspider mycrawler_redis.py
-
在Master端的redis-cli输入push指令,参考格式:
$redis > lpush mycrawler:start_urls http://www.dmoz.org/
-
爬虫获取url,开始执行。
总结:
-
如果只是用到Redis的去重和保存功能,就选第一种;
-
如果要写分布式,则根据情况,选择第二种、第三种;
-
通常情况下,会选择用第三种方式编写深度聚焦爬虫。
有缘网分布式爬虫案例:
# clone github scrapy-redis源码文件 git clone https://github.com/rolando/scrapy-redis.git # 直接拿官方的项目范例,改名为自己的项目用(针对懒癌患者) mv scrapy-redis/example-project ~/scrapy-youyuan
修改settings.py
下面列举了修改后的配置文件中与scrapy-redis有关的部分,middleware、proxy等内容在此就省略了。
# -*- coding: utf-8 -*- # 指定使用scrapy-redis的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 指定使用scrapy-redis的去重 DUPEFILTER_CLASS = \'scrapy_redis.dupefilters.RFPDupeFilter\' # 指定排序爬取地址时使用的队列, # 默认的 按优先级排序(Scrapy默认),由sorted set实现的一种非FIFO、LIFO方式。 SCHEDULER_QUEUE_CLASS = \'scrapy_redis.queue.SpiderPriorityQueue\' # 可选的 按先进先出排序(FIFO) # SCHEDULER_QUEUE_CLASS = \'scrapy_redis.queue.SpiderQueue\' # 可选的 按后进先出排序(LIFO) # SCHEDULER_QUEUE_CLASS = \'scrapy_redis.queue.SpiderStack\' # 在redis中保持scrapy-redis用到的各个队列,从而允许暂停和暂停后恢复,也就是不清理redis queues SCHEDULER_PERSIST = True # 只在使用SpiderQueue或者SpiderStack是有效的参数,指定爬虫关闭的最大间隔时间 # SCHEDULER_IDLE_BEFORE_CLOSE = 10 # 通过配置RedisPipeline将item写入key为 spider.name : items 的redis的list中,供后面的分布式处理item # 这个已经由 scrapy-redis 实现,不需要我们写代码 ITEM_PIPELINES = { \'example.pipelines.ExamplePipeline\': 300, \'scrapy_redis.pipelines.RedisPipeline\': 400 } # 指定redis数据库的连接参数 # REDIS_PASS是我自己加上的redis连接密码(默认不做) REDIS_HOST = \'127.0.0.1\' REDIS_PORT = 6379 #REDIS_PASS = \'redisP@ssw0rd\' # LOG等级 LOG_LEVEL = \'DEBUG\' #默认情况下,RFPDupeFilter只记录第一个重复请求。将DUPEFILTER_DEBUG设置为True会记录所有重复的请求。 DUPEFILTER_DEBUG =True # 覆盖默认请求头,可以自己编写Downloader Middlewares设置代理和UserAgent DEFAULT_REQUEST_HEADERS = { \'Accept\': \'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\', \'Accept-Language\': \'zh-CN,zh;q=0.8\', \'Connection\': \'keep-alive\', \'Accept-Encoding\': \'gzip, deflate, sdch\' }
查看pipeline.py
# -*- coding: utf-8 -*- from datetime import datetime class ExamplePipeline(object): def process_item(self, item, spider): #utcnow() 是获取UTC时间 item["crawled"] = datetime.utcnow() # 爬虫名 item["spider"] = spider.name return item
修改items.py
增加我们最后要保存的youyuanItem项,这里只写出来一个非常简单的版本
# -*- coding: utf-8 -*- from scrapy.item import Item, Field class youyuanItem(Item): # 个人头像链接 header_url = Field() # 用户名 username = Field() # 内心独白 monologue = Field() # 相册图片链接 pic_urls = Field() # 年龄 age = Field() # 网站来源 youyuan source = Field() # 个人主页源url source_url = Field() # 获取UTC时间 crawled = Field() # 爬虫名 spider = Field()
编写 spiders/youyuan.py
在spiders目录下增加youyuan.py文件编写我们的爬虫,之后就可以运行爬虫了。 这里的提供一个简单的版本:
# -*- coding:utf-8 -*- from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule # 使用redis去重 from scrapy.dupefilters import RFPDupeFilter from example.items import youyuanItem import re # class YouyuanSpider(CrawlSpider): name = \'youyuan\' allowed_domains = [\'youyuan.com\'] # 有缘网的列表页 start_urls = [\'http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/\'] # 搜索页面匹配规则,根据response提取链接 list_page_lx = LinkExtractor(allow=(r\'http://www.youyuan.com/find/.+\')) # 北京、18~25岁、女性 的 搜索页面匹配规则,根据response提取链接 page_lx = LinkExtractor(allow =(r\'http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p\\d+/\')) # 个人主页 匹配规则,根据response提取链接 profile_page_lx = LinkExtractor(allow=(r\'http://www.youyuan.com/\\d+-profile/\')) rules = ( # 匹配find页面,跟进链接,跳板 Rule(list_page_lx, follow=True), # 匹配列表页成功,跟进链接,跳板 Rule(page_lx, follow=True), # 匹配个人主页的链接,形成request保存到redis中等待调度,一旦有响应则调用parse_profile_page()回调函数处理,不做继续跟进 Rule(profile_page_lx, callback=\'parse_profile_page\', follow=False), ) # 处理个人主页信息,得到我们要的数据 def parse_profile_page(self, response): item = youyuanItem() item[\'header_url\'] = self.get_header_url(response) item[\'username\'] = self.get_username(response) item[\'monologue\'] = self.get_monologue(response) item[\'pic_urls\'] = self.get_pic_urls(response) item[\'age\'] = self.get_age(response) item[\'source\'] = \'youyuan\' item[\'source_url\'] = response.url #print "Processed profile %s" % response.url yield item # 提取头像地址 def get_header_url(self, response): header = response.xpath(\'//dl[@class=\\\'personal_cen\\\']/dt/img/@src\').extract() if len(header) > 0: header_url = header[0] else: header_url = "" return header_url.strip() # 提取用户名 def get_username(self, response): usernames = response.xpath("//dl[@class=\\\'personal_cen\\\']/dd/div/strong/text()").extract() if len(usernames) > 0: username = usernames[0] else: username = "NULL" return username.strip() # 提取内心独白 def get_monologue(self, response): monologues = response.xpath("//ul[@class=\\\'requre\\\']/li/p/text()").extract() if len(monologues) > 0: monologue = monologues[0] else: monologue = "NULL" return monologue.strip() # 提取相册图片地址 def get_pic_urls(self, response): pic_urls = [] data_url_full = response.xpath(\'//li[@class=\\\'smallPhoto\\\']/@data_url_full\').extract() if len(data_url_full) <= 1: pic_urls.append(""); else: for pic_url in data_url_full: pic_urls.append(pic_url) if len(pic_urls) <= 1: return "NULL" # 每个url用|分隔 return \'|\'.join(pic_urls) # 提取年龄 def get_age(self, response): age_urls = response.xpath("//dl[@class=\\\'personal_cen\\\']/dd/p[@class=\\\'local\\\']/text()").extract() if len(age_urls) > 0: age = age_urls[0] else: age = "0" age_words = re.split(\' \', age) if len(age_words) <= 2: return "0" age = age_words[2][:-1] # 从age字符串开始匹配数字,失败返回None if re.compile(r\'[0-9]\').match(age): return age return "0"
运行程序:
- Master端打开 Redis:
redis-server
- Slave端直接运行爬虫:
scrapy crawl youyuan
- 多个Slave端运行爬虫顺序没有限制。
将项目修改成 RedisCrawlSpider 类的分布式爬虫,并尝试在多个Slave端运行。
有缘网分布式爬虫案例:
修改 spiders/youyuan.py
在spiders目录下增加youyuan.py文件编写我们的爬虫,使其具有分布式:
# -*- coding:utf-8 -*- from scrapy.linkextractors import LinkExtractor #from scrapy.spiders import CrawlSpider, Rule # 1. 导入RedisCrawlSpider类,不使用CrawlSpider from scrapy_redis.spiders import RedisCrawlSpider from scrapy.spiders import Rule from scrapy.dupefilters import RFPDupeFilter from example.items import youyuanItem import re # 2. 修改父类 RedisCrawlSpider # class YouyuanSpider(CrawlSpider): class YouyuanSpider(RedisCrawlSpider): name = \'youyuan\' # 3. 取消 allowed_domains() 和 start_urls ##### allowed_domains = [\'youyuan.com\'] ##### start_urls = [\'http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/\'] # 4. 增加redis-key redis_key = \'youyuan:start_urls\' list_page_lx = LinkExtractor(allow=(r\'http://www.youyuan.com/find/.+\')) page_lx = LinkExtractor(allow =(r\'http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p\\d+/\')) profile_page_lx = LinkExtractor(allow=(r\'http://www.youyuan.com/\\d+-profile/\')) rules = ( Rule(list_page_lx, follow=True), Rule(page_lx, follow=True), Rule(profile_page_lx, callback=\'parse_profile_page\', follow=False), ) # 5. 增加__init__()方法,动态获取allowed_domains() def __init__(self, *args, **kwargs): domain = kwargs.pop(\'domain\', \'\') self.allowed_domains = filter(None, domain.split(\',\')) super(youyuanSpider, self).__init__(*args, **kwargs) # 处理个人主页信息,得到我们要的数据 def parse_profile_page(self, response): item = youyuanItem() item[\'header_url\'] = self.get_header_url(response) item[\'username\'] = self.get_username(response) item[\'monologue\'] = self.get_monologue(response) item[\'pic_urls\'] = self.get_pic_urls(response) item[\'age\'] = self.get_age(response) item[\'source\'] = \'youyuan\' item[\'source_url\'] = response.url yield item # 提取头像地址 def get_header_url(self, response): header = response.xpath(\'//dl[@class=\\\'personal_cen\\\']/dt/img/@src\').extract() if len(header) > 0: header_url = header[0] else: header_url = "" return header_url.strip() # 提取用户名 def get_username(self, response): usernames = response.xpath("//dl[@class=\\\'personal_cen\\\']/dd/div/strong/text()").extract() if len(usernames) > 0: username = usernames[0] else: username = "NULL" return username.strip() # 提取内心独白 def get_monologue(self, response): monologues = response.xpath("//ul[@class=\\\'requre\\\']/li/p/text()").extract() if len(monologues) > 0: monologue = monologues[0] else: monologue = "NULL" return monologue.strip() # 提取相册图片地址 def get_pic_urls(self, response): pic_urls = [] data_url_full = response.xpath(\'//li[@class=\\\'smallPhoto\\\']/@data_url_full\').extract() if len(data_url_full) <= 1: pic_urls.append(""); else: for pic_url in data_url_full: pic_urls.append(pic_url) if len(pic_urls) <= 1: return "NULL" return \'|\'.join(pic_urls) # 提取年龄 def get_age(self, response): age_urls = response.xpath("//dl[@class=\\\'personal_cen\\\']/dd/p[@class=\\\'local\\\']/text()").extract() if len(age_urls) > 0: age = age_urls[0] else: age = "0" age_words = re.split(\' \', age) if len(age_words) <= 2: return "0" age = age_words[2][:-1] if re.compile(r\'[0-9]\').match(age): return age return "0"
分布式爬虫执行方式:
6. 在Master端启动redis-server:
redis-server
7. 在Slave端分别启动爬虫,不分先后:
scrapy runspider youyuan.py
8. 在Master端的redis-cli里push一个start_urls
redis-cli> lpush youyuan:start_urls http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/
scrapy-redis分布式爬虫实战
[Python3网络爬虫开发实战] 1.8.4-Scrapy-Redis的安装
实战scrapy-redis + webdriver 爬取航空网站
Python网络爬虫Scrapy+MongoDB +Redis实战爬取腾讯视频动态评论教学视频