為甚要學習scrapy_redis??
Scrapy_redis在scrapy的基礎上實現了更多,更強大的功能,具體體現在:reqeust去重,爬蟲持久化,和輕松實現分布式
Scrapy-redis提供了下面四種組件(components):(四種組件意味著這四個模塊都要做相應的修改)
- Scheduler (調度器)
- Duplication Filter (去重)
- Item Pipeline (引擎)
- Base Spider
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需要提供一系列的方法。但是原來的Scheduler已經無法使用,所以使用Scrapy-redis的scheduler組件。
Duplication Filter
Scrapy中用集合實現這個request去重功能,Scrapy中把已經發送的request指紋放入到一個集合中,把下一個request的指紋拿到集合中比對,如果該指紋存在于集合中,說明這個request發送過了,如果沒有則繼續操作。這個核心的判重功能是這樣實現的:
def request_seen(self, request):
# 把請求轉化為指紋
fp = self.request_fingerprint(request)
# 這就是判重的核心操作 ,self.fingerprints就是指紋集合
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處理
Item Pipeline:
引擎將(Spider返回的)爬取到的Item給Item Pipeline,scrapy-redis 的Item Pipeline將爬取到的 Item 存?redis的 items queue。
修改過Item Pipeline可以很方便的根據 key 從 items queue 提取item,從?實現 items processes集群
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。
源碼自帶項目說明:
git clone https://github.com/rolando/scrapy-redis.git
啟動后redis數據庫會多這3個
"xcfCrawlSpider:requests":存儲的是請求的request對象
"xcfCrawlSpider:items":存儲的爬蟲端獲取的items數據
"xcfCrawlSpider:dupefilter":存儲的指紋(為了實現去重)
127.0.0.1:6379> type xcfCrawlSpider:requests
zset
127.0.0.1:6379> type xcfCrawlSpider:items
list
127.0.0.1:6379> type xcfCrawlSpider:dupefilter
set
**一、dmoz (class DmozSpider(CrawlSpider))**
注意:這里只是用到Redis的去重和保存功能,并沒有實現分布式
這個爬蟲繼承的是CrawlSpider,它是用來說明Redis的持續性,當我們第一次運行dmoz爬蟲,然后Ctrl + C停掉之后,再運行dmoz爬蟲,之前的爬取記錄是保留在Redis里的。
**(第一中情況:只設置settings.py文件,并沒有實現分布式,知識使用了sctapy_redis的數據存儲和去重功能)**
分析起來,其實這就是一個 scrapy-redis 版 CrawlSpider 類,需要設置Rule規則,以及callback不能寫parse()方法。 執行方式:
```python
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/']
#定義了一個url的提取規則,將滿足條件的交給callback函數處理
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(),
}
二、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
#繼承制:RedisCrawlSpider
class MyCrawler(RedisCrawlSpider):
"""Spider that reads urls from redis queue (myspider:start_urls)."""
name = 'mycrawler_redis'
allowed_domains = ['dmoz.org']
#缺少了start_url,多了redis_key:根據redis_key從redis
#數據庫中獲取任務
redis_key = 'mycrawler:start_urls'
rules = (
# follow all links
Rule(LinkExtractor(), callback='parse_page', follow=True),
)
#動態獲取要爬取的域
# 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,
}
啟動爬蟲:scrapy crawl 爬蟲名稱
現象:爬蟲處于等待狀態
需要設置起始任務:
lpush mycrawler:start_urls 目標url
三、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'
#手動設置允許爬取的域
allowed_domains = ['設置允許爬取的域']
# 注意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,
}
啟動方法同上
總結:
1 如果只是用到Redis的去重和保存功能,就選第一種; 2 如果要寫分布式,則根據情況,選擇第二種、第三種; 3 通常情況下,會選擇用第二種方式編寫深度聚焦爬蟲。