Scrapy_Redis分布式爬蟲

為甚要學習scrapy_redis??

Scrapy_redis在scrapy的基礎上實現了更多,更強大的功能,具體體現在:reqeust去重,爬蟲持久化,和輕松實現分布式

Scrapy-redis提供了下面四種組件(components):(四種組件意味著這四個模塊都要做相應的修改)

  • Scheduler (調度器)
  • Duplication Filter (去重)
  • Item Pipeline (引擎)
  • Base Spider
1

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 通常情況下,會選擇用第二種方式編寫深度聚焦爬蟲。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373