用簡單的方式講scrapy-redis爬蟲分佈式策略

1. 習慣性嘮叨點啥

晨曦無限好、溫暖如春、溫暖你我的心

  冬去春已來,但是感覺最近北京的春風它並不是把春天送到我們的身邊來,而是巴不得要把春天趕跑。風很大、天很藍、太陽很足! 北京有句老話叫“春脖子短”,當你正感受到它的到來時,它可能就要一閃而過了
在這裏插入圖片描述

2. 分佈式爬蟲策略

  作爲一名以爬蟲開發爲職業的工程師來講的話,在開發爬蟲的過程中。很多業務場景需要採集的站往往讓我們異想不到,因爲它不僅站多而且量還大。很多時候我們都是在自己的機器上開發爬蟲,爲了發揮爬蟲的效率我們也經常會用到多進程、多線程來提升我們爬蟲的效率!但是可能在職業生涯中大部分站由於數據量不是很大我們總是選擇了單機爬蟲的方式。

針對一些小站的話,單機Scrapy爬蟲方式完全夠用,殺雞焉用牛刀?
針對一些大站的話,這個時候可能就顯得有些無力了。這個時候如果你還是繼續選擇單機Scrapy採集…過了幾天後…
老大或者老闆:嗨!採集的怎麼樣了?數據都採集完了吧?
你說:這個網站數據量真的是巨大啊!我都跑了三天三夜了。正採集着呢!放心吧,我剛初步瞄了一下應該再採三天三夜基本就差不多了!說到這裏!
Ta可能扛着40米的牛刀正朝你走來…

  本期聊點什麼呢?根據之前自己對源碼的一些探測跟一些官方資料及自身使用的感受,我們就來聊聊關於爬蟲分佈式的那點事可好?所以本期聊盤一下分佈式爬蟲策略。說到分佈式爬蟲開發及部署,很多人都會聯想到Scrapy-redis

某爬蟲“大佬”:Scrapy-redis這個框架我用過、我經常用!很好、很強大!
瞎說什麼玩意
  NO!哦買噶!他剛剛說了什麼?他跟我說Scrapy-redis是一個框架!可是實際上Scrapy-redis它並不是一個框架!也不是一套什麼可以單獨運行的東西(要考的哦!)它其實是一套基於Scrapy框架之上的一套組件,它是一個提供可以支持分佈式的組件,Scrapy-redis重寫了Scrapy一些比較關鍵的代碼,從而用來替換Scrapy本身的一些東西,讓Scrapy擁有了支持分佈式的功能。

  如果沒有使用Scrapy-redis而開啓多個Scrapy爬蟲以爲它就能幫助我們達到分佈式的效果或者達到高速採集的話這個是個錯誤的想法!這樣做的話第一它的數據重複採集的,因爲多個進程之間的內存是不能共享的,所以它們都不知道對方採集了哪些沒采集哪些!其實都是自己在玩自己的,怎來效率一說。

  其實!最終說到底。Scrapy它不支持分佈式主要就是請求、去重都是基於自身內存而不是共享的!所以Scrapy-redis到底做了什麼從而讓Scrapy支持分佈式供能的呢?它將Scrapy中的調度器組件單獨抽出來放到了一個大家都能共享的地方!我簡單的畫一個草圖讓它更直觀一些,大家也能更好的理解Scrapy-redis它的角色
scrapy-redis分佈式
  不止做爬蟲的小夥伴知道Scrapy,很多人都知道Scrapy它是一個通用的爬蟲框架,但是呢!並不能支持分佈式。而Scrapy-redis則是爲了更方便的實現Scrapy分佈式採集而提供了以redis爲基礎的組件(所以呢!這些組件必須跟Scrapy結合在一起才能用起來

  這套組件的核心就是Redis數據庫!數據會統一放到Redis數據庫,主要由Master端分配任務,Slaver端也就是我們的各個爬蟲端負責採集數據,並且將所有采集的的數據最後全部提交到Master端的redis數據庫裏。所有的爬蟲端它們共享一個redis數據庫,說到這裏,你擁有它了嗎?:

pip install scrapy-redis

Scrapy-redis它主要提供了以下四種組件,這四個組件呢!其實就是替換了原來的Scrapy本身的組件(同時也意味着這四個模塊都要做一些修改):

  • Scheduler
  • Duplication Filter
  • Item Pipeline
  • Base Spider
  1. 首先就是調度器Scheduler
    Scrapy改造了Python本來的Collection.deque形成了自己的Scrapy,但是Scrapy多個Spider不能共享待採集隊列Scrapy.queue,即Scrapy本身不支持分佈式,所以Scrapy-redis的作用就是解決就是把這個Scrapy隊列換成redis數據庫(也就是redis隊列)。從同一個redis-server存放要採集的request,便能多個spider去同一個數據庫讀取
  2. 然後就是去重Duplication Filter
    做過爬蟲開發的工程師都知道Scrapy本身是有去重機制的,它的去重主要是在內存中執行的。但是呢!如果我們的請求數到一定量級的時候,會發現Scrapy內存佔用非常高!所有在這裏我們把這些去重的指紋放到redis數據庫裏那不僅更方便了,而且還能做持久化!
    其實在Scrapy本身中用集合實現的request去重功能,Scrapy中已經發送的request指紋放入到一個集合中,把下一個request的指紋拿到集合中比對,如果該指紋存在指紋中,說明這個request發送過,如果沒有則繼續操作,這個核心的去重功能是這樣實現的:
class RFPDupeFilter(BaseDupeFilter):
    """Request Fingerprint duplicates filter"""
 
    def __init__(self, path=None, debug=False):
        self.file = None
        self.fingerprints = set()
        self.logdupes = True
        self.debug = debug
        self.logger = logging.getLogger(__name__)
        if path:
            #打開去重文件requests.seen
            self.file = open(os.path.join(path, 'requests.seen'), 'a+')
            self.file.seek(0)
            self.fingerprints.update(x.rstrip() for x in self.file)
 
    @classmethod
    def from_settings(cls, settings):
        debug = settings.getbool('DUPEFILTER_DEBUG')
        return cls(job_dir(settings), debug)
def request_seen(self, request):
    # self.request_fingerprint就是一個指紋集合
    fp = self.request_fingerprint(request=request)
    
    # 這就是去重的核心操作
    if fd 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的requestqueue隊列裏面根據優先級pop出一個request返回給引擎,引擎將此request發給spider處理

舉個栗子: 我們現在有一個爬蟲,它已經運行採集了一段時間,但是這個時候呢,可能因爲人爲操作或者異常情況導致它中斷了。那麼我們再執行的時候它會接着讀取redis數據庫裏面的請求指紋,之前採集過的它自然就不會再去發送了。如果這個爬蟲我們用Scrapy來做的話,它就不能像以上情況一樣,一旦中斷內存就會被清空了,再次採集就要從頭繼續了!也就是說一招回到解放前…

  1. 第三個就是管道文件這塊Item Pipeline
    它也提供了一套模板,我們都知道之前在Scrapy裏面數據都是直接交給管道文件最後進行存儲操作的。那現在呢?這個數據不再是交給管道了,而是交到redis數據庫統一管理。最後取提取我們的數據也是有一套模板從redis數據庫抽取存儲到我們本地的數據庫系統或文件系統做一個持久化!因爲redis它也是基於內存,萬一哪一天你關機也會清空
  2. 第四個就是我們爬蟲的類也會改變Base Spider
    Scrapy原生的框架有兩個類:Spider、CrawlSpider。這兩個類在scrapy-redis裏面也會做修改。它會被修改成以redis爲核心的兩個爬蟲類!不再使用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。

我們來看看官方的Scrapy-redis架構圖,做一些詳細的梳理:
scrapy-redis框架圖

  首先!我們可以看到在Scrapy-redis架構圖中調度器將所有的請求不再放到下載器裏面,而是放到redis數據庫裏面。redis數據庫分別放有存數據、存請求隊列、存請求指紋的三個庫!那麼這些請求發鬆到redis數據庫裏面首先要做什麼呢?你知道嗎?當然是先做一個指紋比對,確定這個請求之前有沒有被收集過(每個request到redis數據庫裏面都會留下一個指紋

  請求全部進到隊列之後,然後redis數據庫會把這些請求再挨個出隊列,交給調度器,這個時候調度器纔會把請求交給下載器去下載。也就是說!原來這個調度器進的這個Scrapy框架的調度會把請求打到Scrapy本身的請求隊列裏,Scrapy它也有自己的去重,最後再交給下載器去下載。但是現在統一交給了redis數據庫!

另外指紋到底是什麼呢?問得很好!

聽這個字面意思可能很多人至少都判斷出它是唯一的,畢竟我們人類本身的手指指紋不就是嘛,我以前看的那些什麼警匪片,在犯罪現場警察叔叔都會帶上手套在那裏細心收集着什麼,那就是在收集犯罪份子遺留在現場的證據痕跡其中就包括指紋。其實指紋在這裏的意思就是如果請求URL資源位置是同樣的話,那麼這個指紋就是相同的。如果redis數據庫之前的一個指紋存在那麼新增的就會被捨棄!

  另外在Scrapy中個跟“待爬隊列”直接相關的就是調度器Scheduler它負責對新的request進行入列操作(加入到Scrapyqueue),取出下一個要採集的request等操作。它把待採集隊列按照優先級建立了一個字典結構,如下:

{
	優先級0:隊列0
	優先級1:隊列1
	優先級2:隊列2
}

根據request中的優先級,來決定該入哪個隊列,出列時則按優先級較小的優先出列,再來看看Scrapy中的Scheduler:

    def enqueue_request(self, request):
      """add一個請求到隊列"""
          # 負責檢查request是否已被請求 如果是則返回True
        if not request.dont_filter and self.df.request_seen(request):
            # 如果request的dont_filter我們沒有設置True則去重,不進隊列
            self.df.log(request, self.spider)
            return False
        # 將request add到磁盤隊列
        dqok = self._dqpush(request)
        if dqok:
              # 如果成功 記錄一次狀態
            self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider)
        else:
              # 不能add到磁盤隊列則會add到內存隊列
            self._mqpush(request)
            self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider)
        self.stats.inc_value('scheduler/enqueued', spider=self.spider)
        return True
 
    def next_request(self):
      """從隊列中獲取一個request"""
          # 優先從內存的隊列中pop
        request = self.mqs.pop()
        if request:
            self.stats.inc_value('scheduler/dequeued/memory', spider=self.spider)
        else:
              # 不能獲取的時候從磁盤隊列隊裏獲取
            request = self._dqpop()
            if request:
                self.stats.inc_value('scheduler/dequeued/disk', spider=self.spider)
        if request:
            self.stats.inc_value('scheduler/dequeued', spider=self.spider)
        # 最後再將獲取到的request返回給引擎
        return request
 

爲了管理這個比較高級的隊列字典,Scheduler需要提供一系列的方法。但是原來的Scheduler已經無法使用,所以使用Scrapy-redis的Scheduler組件

管道文件這裏我再細說一下!如果我們單獨寫的Scrapy爬蟲項目,數據在管道里面比如我們存到本地JSON或者本地數據庫都可以在管道文件裏面寫。但是現在這個管道文件我們可以寫!也可以不寫!爲什麼呢?因爲如果寫的話,就不能再做修改或者把數據再存儲到我們的本地!當然你如果非要這麼做也是可以存儲到本地的,因爲它畢竟要經過管道文件這一塊,但是!這樣做的話就失去了分佈式的意義了!

  這樣做的話,我們的數據沒有做集中存儲,最後都存儲在各個爬蟲端,如果我們個人或者公司的爬蟲項目部署在不同的地區,美國那邊有幾個,香港那邊有幾個,菲律賓也有幾個…最後如果都存儲在爬蟲端的話,後期再集中整合也是非常費勁的一件事!(如果我們爬蟲架構這麼幹的話估計第二天就要…哈哈。所有千萬不能這麼幹

  所以我們統一存儲在redis數據庫最後再單獨寫一個ItemProcesses把它們抽取出來!當然也可以不拿,一直放在redis數據庫裏,但是這種luo奔的方式還是有較高的風險係數,因爲遲早有一天這些數據會丟失(噓!baby,我們一起讓時間說真話,好嗎?

  這就是Scrapy-redis這套組件的整體流程跟一些策略以及它跟原生的Scrapy之間的一些區別。其實它跟原生的scrapy框架流程不一樣的只是所有調度都以redis組件爲核心來展開!

3. 致謝

好了,到這裏又到了跟大家說再見的時候了。我只是一個會寫爬蟲的段子手而已,一個希望有朝一日能夠實現財富自由,能夠早日榮歸故里的遊子罷了。希望我的文章能帶給您知識,帶給您歡笑!同時也謝謝您能抽出寶貴的時間閱讀,創作不易,如果您喜歡的話,點個關注再走吧。您的支持是我創作的動力,希望今後能帶給大家更多優質的文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章