為甚要學(xué)習(xí)scrapy_redis吨岭??
Scrapy_redis在scrapy的基礎(chǔ)上實(shí)現(xiàn)了更多峦树,更強(qiáng)大的功能辣辫,具體體現(xiàn)在:reqeust去重,爬蟲(chóng)持久化魁巩,和輕松實(shí)現(xiàn)分布式
pip3 install scrapy-redis
Scrapy-redis提供了下面四種組件(components):(四種組件意味著這四個(gè)模塊都要做相應(yīng)的修改)
- Scheduler
- Duplication Filter
- Item Pipeline
- Base Spider
Scrapy_redis是工作流程
Scheduler:
Scrapy改造了python本來(lái)的collection.deque(雙向隊(duì)列)形成了自己的Scrapy queue(https://github.com/scrapy/queuelib/blob/master/queuelib/queue.py))急灭,但是Scrapy多個(gè)spider不能共享待爬取隊(duì)列Scrapy queue, 即Scrapy本身不支持爬蟲(chóng)分布式谷遂,scrapy-redis 的解決是把這個(gè)Scrapy queue換成redis數(shù)據(jù)庫(kù)(也是指redis隊(duì)列)葬馋,從同一個(gè)redis-server存放要爬取的request,便能讓多個(gè)spider去同一個(gè)數(shù)據(jù)庫(kù)里讀取。
Scrapy中跟“待爬隊(duì)列”直接相關(guān)的就是調(diào)度器Scheduler点楼,它負(fù)責(zé)對(duì)新的request進(jìn)行入列操作(加入Scrapy queue)扫尖,取出下一個(gè)要爬取的request(從Scrapy queue中取出)等操作。它把待爬隊(duì)列按照優(yōu)先級(jí)建立了一個(gè)字典結(jié)構(gòu)掠廓,比如:
{
優(yōu)先級(jí)0 : 隊(duì)列0
優(yōu)先級(jí)1 : 隊(duì)列1
優(yōu)先級(jí)2 : 隊(duì)列2
}
然后根據(jù)request中的優(yōu)先級(jí)换怖,來(lái)決定該入哪個(gè)隊(duì)列,出列時(shí)則按優(yōu)先級(jí)較小的優(yōu)先出列蟀瞧。為了管理這個(gè)比較高級(jí)的隊(duì)列字典沉颂,Scheduler需要提供一系列的方法。但是原來(lái)的Scheduler已經(jīng)無(wú)法使用悦污,所以使用Scrapy-redis的scheduler組件铸屉。
Duplication Filter
Scrapy中用集合實(shí)現(xiàn)這個(gè)request去重功能,Scrapy中把已經(jīng)發(fā)送的request指紋放入到一個(gè)集合中切端,把下一個(gè)request的指紋拿到集合中比對(duì)彻坛,如果該指紋存在于集合中,說(shuō)明這個(gè)request發(fā)送過(guò)了踏枣,如果沒(méi)有則繼續(xù)操作昌屉。這個(gè)核心的判重功能是這樣實(shí)現(xiàn)的:
def request_seen(self, request):
# 把請(qǐng)求轉(zhuǎn)化為指紋
fp = self.request_fingerprint(request)
# 這就是判重的核心操作 ,self.fingerprints就是指紋集合
if fp in self.fingerprints:
return True #直接返回
self.fingerprints.add(fp) #如果不在茵瀑,就添加進(jìn)去指紋集合
if self.file:
self.file.write(fp + os.linesep)
在scrapy-redis中去重是由Duplication Filter組件來(lái)實(shí)現(xiàn)的间驮,它通過(guò)redis的set 不重復(fù)的特性,巧妙的實(shí)現(xiàn)了Duplication Filter去重马昨。scrapy-redis調(diào)度器從引擎接受request竞帽,將request的指紋存?redis的set檢查是否重復(fù),并將不重復(fù)的request push寫(xiě)?redis的 request queue鸿捧。
引擎請(qǐng)求request(Spider發(fā)出的)時(shí)屹篓,調(diào)度器從redis的request queue隊(duì)列?里根據(jù)優(yōu)先級(jí)pop 出?個(gè)request 返回給引擎,引擎將此request發(fā)給spider處理匙奴。
Item Pipeline:
引擎將(Spider返回的)爬取到的Item給Item Pipeline堆巧,scrapy-redis 的Item Pipeline將爬取到的 Item 存?redis的 items queue。
修改過(guò)Item Pipeline可以很方便的根據(jù) key 從 items queue 提取item饥脑,從?實(shí)現(xiàn) items processes集群。
Base Spider
不在使用scrapy原有的Spider類(lèi)懦冰,重寫(xiě)的RedisSpider繼承了Spider和RedisMixin這兩個(gè)類(lèi)灶轰,RedisMixin是用來(lái)從redis讀取url的類(lèi)。
當(dāng)我們生成一個(gè)Spider繼承RedisSpider時(shí)刷钢,調(diào)用setup_redis函數(shù)笋颤,這個(gè)函數(shù)會(huì)去連接redis數(shù)據(jù)庫(kù),然后會(huì)設(shè)置signals(信號(hào)):
一個(gè)是當(dāng)spider空閑時(shí)候的signal,會(huì)調(diào)用spider_idle函數(shù)伴澄,這個(gè)函數(shù)調(diào)用schedule_next_request函數(shù)赋除,保證spider是一直活著的狀態(tài),并且拋出DontCloseSpider異常非凌。
一個(gè)是當(dāng)抓到一個(gè)item時(shí)的signal举农,會(huì)調(diào)用item_scraped函數(shù),這個(gè)函數(shù)會(huì)調(diào)用schedule_next_request函數(shù)敞嗡,獲取下一個(gè)request颁糟。
源碼自帶項(xiàng)目說(shuō)明:
使用scrapy-redis的example來(lái)修改 先從github上拿到scrapy-redis的示例,然后將里面的example-project目錄移到指定的地址:
clone github scrapy-redis源碼文件
git clone https://github.com/rolando/scrapy-redis.git
直接拿官方的項(xiàng)目范例喉悴,改名為自己的項(xiàng)目用(針對(duì)懶癌患者)
mv scrapy-redis/example-project ~/scrapyredis-project
我們clone到的 scrapy-redis 源碼中有自帶一個(gè)example-project項(xiàng)目棱貌,這個(gè)項(xiàng)目包含3個(gè)spider,分別是dmoz, myspider_redis箕肃,mycrawler_redis婚脱。
一、dmoz (class DmozSpider(CrawlSpider))
注意:這里只是用到Redis的去重和保存功能,并沒(méi)有實(shí)現(xiàn)分布式
這個(gè)爬蟲(chóng)繼承的是CrawlSpider勺像,它是用來(lái)說(shuō)明Redis的持續(xù)性障贸,當(dāng)我們第一次運(yùn)行dmoz爬蟲(chóng),然后Ctrl + C停掉之后咏删,再運(yùn)行dmoz爬蟲(chóng)惹想,之前的爬取記錄是保留在Redis里的。
分析起來(lái)督函,其實(shí)這就是一個(gè) scrapy-redis 版 CrawlSpider 類(lèi)嘀粱,需要設(shè)置Rule規(guī)則,以及callback不能寫(xiě)parse()方法辰狡。 執(zhí)行方式:
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/']
#定義了一個(gè)url的提取規(guī)則锋叨,將滿(mǎn)足條件的交給callback函數(shù)處理
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'):
#這里將獲取到的內(nèi)容交給引擎
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))
這個(gè)爬蟲(chóng)繼承了RedisSpider宛篇, 它能夠支持分布式的抓取娃磺,采用的是basic spider,需要寫(xiě)parse函數(shù)叫倍。 其次就是不再有start_urls了偷卧,取而代之的是redis_key,scrapy-redis將key從Redis里pop出來(lái)吆倦,成為請(qǐng)求的url地址听诸。
from scrapy_redis.spiders import RedisSpider
class MySpider(RedisSpider):
"""Spider that reads urls from redis queue (myspider:start_urls)."""
name = 'myspider_redis'
#手動(dòng)設(shè)置允許爬取的域
allowed_domains = ['設(shè)置允許爬取的域']
# 注意redis-key的格式:
redis_key = 'myspider:start_urls'
# 可選:等效于allowd_domains(),__init__方法按規(guī)定格式寫(xiě)蚕泽,使用時(shí)只需要修改super()里的類(lèi)名參數(shù)即可,一般不用
def __init__(self, *args, **kwargs):
# Dynamically define the allowed domains list.
domain = kwargs.pop('domain', '')
self.allowed_domains = filter(None, domain.split(','))
# 修改這里的類(lèi)名為當(dāng)前類(lèi)名
super(MySpider, self).__init__(*args, **kwargs)
def parse(self, response):
return {
'name': response.css('title::text').extract_first(),
'url': response.url,
}
注意: RedisSpider類(lèi) 不需要寫(xiě)start_urls:
- scrapy-redis 一般直接寫(xiě)allowd_domains來(lái)指定需要爬取的域晌梨,也可以從在構(gòu)造方法init()里動(dòng)態(tài)定義爬蟲(chóng)爬取域范圍(一般不用)。
- 必須指定redis_key,即啟動(dòng)爬蟲(chóng)的命令仔蝌,參考格式:redis_key = 'myspider:start_urls'
- 根據(jù)指定的格式泛领,start_urls將在 Master端的 redis-cli 里 lpush 到 Redis數(shù)據(jù)庫(kù)里,RedisSpider 將在數(shù)據(jù)庫(kù)里獲取start_urls敛惊。
執(zhí)行方式: - 1.通過(guò)runspider方法執(zhí)行爬蟲(chóng)的py文件(也可以分次執(zhí)行多條)渊鞋,爬蟲(chóng)(們)將處于等待準(zhǔn)備狀態(tài):
scrapy runspider myspider_redis.py
或者
scrapy crawl myspider_redis
- 2.在Master端的redis-cli輸入push指令,參考格式(指定起始url):
lpush myspider:start_urls http://www.dmoz.org/
- 3.Slaver端爬蟲(chóng)獲取到請(qǐng)求豆混,開(kāi)始爬取篓像。
三、mycrawler_redis (class MyCrawler(RedisCrawlSpider))
這個(gè)RedisCrawlSpider類(lèi)爬蟲(chóng)繼承了RedisCrawlSpider皿伺,能夠支持分布式的抓取员辩。因?yàn)椴捎玫氖莄rawlSpider,所以需要遵守Rule規(guī)則鸵鸥,以及callback不能寫(xiě)parse()方法奠滑。
同樣也不再有start_urls了,取而代之的是redis_key妒穴,scrapy-redis將key從Redis里pop出來(lái)宋税,成為請(qǐng)求的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'
allowed_domains = ['設(shè)置允許爬取的域']
redis_key = 'mycrawler:start_urls'
rules = (
# follow all links
Rule(LinkExtractor(), callback='parse_page', follow=True),
)
# __init__方法必須按規(guī)定寫(xiě)讼油,使用時(shí)只需要修改super()里的類(lèi)名參數(shù)即可(一般不用)
def __init__(self, *args, **kwargs):
# Dynamically define the allowed domains list.
domain = kwargs.pop('domain', '')
self.allowed_domains = filter(None, domain.split(','))
# 修改這里的類(lèi)名為當(dāng)前類(lèi)名
super(MyCrawler, self).__init__(*args, **kwargs)
def parse_page(self, response):
return {
'name': response.css('title::text').extract_first(),
'url': response.url,
}
注意: 同樣的杰赛,RedisCrawlSpider類(lèi)不需要寫(xiě)start_urls:
- scrapy-redis 一般直接寫(xiě)allowd_domains來(lái)指定需要爬取的域,也可以從在構(gòu)造方法init()里動(dòng)態(tài)定義爬蟲(chóng)爬取域范圍(一般不用)矮台。
- 必須指定redis_key乏屯,即啟動(dòng)爬蟲(chóng)的命令,參考格式:redis_key = 'myspider:start_urls'
- 根據(jù)指定的格式瘦赫,start_urls將在 Master端的 redis-cli 里 lpush 到 Redis數(shù)據(jù)庫(kù)里辰晕,RedisSpider 將在數(shù)據(jù)庫(kù)里獲取start_urls。
執(zhí)行方式: - 1.通過(guò)runspider方法執(zhí)行爬蟲(chóng)的py文件(也可以分次執(zhí)行多條)确虱,爬蟲(chóng)(們)將處于等待準(zhǔn)備狀態(tài):
scrapy runspider myspider_redis.py
或者
scrapy crawl myspider_redis
- 2.在Master端的redis-cli輸入push指令含友,參考格式(指定起始url):
lpush myspider:start_urls http://www.dmoz.org/
- 3.Slaver端爬蟲(chóng)獲取到請(qǐng)求,開(kāi)始爬取校辩。
總結(jié):
1 如果只是用到Redis的去重和保存功能窘问,就選第一種; 2 如果要寫(xiě)分布式宜咒,則根據(jù)情況惠赫,選擇第二種、第三種荧呐; 3 通常情況下汉形,會(huì)選擇用第三種方式編寫(xiě)深度聚焦爬蟲(chóng)。