分佈式爬蟲原理之分佈式爬蟲原理

咱們在前面已經實現了Scrapy微博爬蟲,雖然爬蟲是異步加多線程的,可是咱們只能在一臺主機上運行,因此爬取效率仍是有限的,分佈式爬蟲則是將多臺主機組合起來,共同完成一個爬取任務,這將大大提升爬取的效率。
html

1、分佈式爬蟲架構

在瞭解分佈式爬蟲架構以前,首先回顧一下Scrapy的架構,以下圖所示。數據庫

Scrapy單機爬蟲中有一個本地爬取隊列Queue,這個隊列是利用deque模塊實現的。若是新的Request生成就會放到隊列裏面,隨後Request被Scheduler調度。以後,Request交給Downloader執行爬取,簡單的調度架構以下圖所示。bash

若是兩個Scheduler同時從隊列裏面取Request,每一個Scheduler都有其對應的Downloader,那麼在帶寬足夠、正常爬取且不考慮隊列存取壓力的狀況下,爬取效率會有什麼變化?沒錯,爬取效率會翻倍。微信

這樣,Scheduler能夠擴展多個,Downloader也能夠擴展多個。而爬取隊列Queue必須始終爲一個,也就是所謂的共享爬取隊列。這樣才能保證Scheduer從隊列裏調度某個Request以後,其餘Scheduler不會重複調度此Request,就能夠作到多個Schduler同步爬取。這就是分佈式爬蟲的基本雛形,簡單調度架構以下圖所示。網絡

咱們須要作的就是在多臺主機上同時運行爬蟲任務協同爬取,而協同爬取的前提就是共享爬取隊列。這樣各臺主機就不須要各自維護爬取隊列,而是從共享爬取隊列存取Request。可是各臺主機仍是有各自的Scheduler和Downloader,因此調度和下載功能分別完成。若是不考慮隊列存取性能消耗,爬取效率仍是會成倍提升。數據結構

2、維護爬取隊列

那麼這個隊列用什麼來維護?首先須要考慮的就是性能問題。咱們天然想到的是基於內存存儲的Redis,它支持多種數據結構,例如列表(List)、集合(Set)、有序集合(Sorted Set)等,存取的操做也很是簡單。多線程

Redis支持的這幾種數據結構存儲各有優勢。架構

  • 列表有lpush()lpop()rpush()rpop()方法,咱們能夠用它來實現先進先出式爬取隊列,也能夠實現先進後出棧式爬取隊列。併發

  • 集合的元素是無序的且不重複的,這樣咱們能夠很是方便地實現隨機排序且不重複的爬取隊列。異步

  • 有序集合帶有分數表示,而Scrapy的Request也有優先級的控制,咱們能夠用它來實現帶優先級調度的隊列。

咱們須要根據具體爬蟲的需求來靈活選擇不一樣的隊列。

3、如何去重

Scrapy有自動去重,它的去重使用了Python中的集合。這個集合記錄了Scrapy中每一個Request的指紋,這個指紋實際上就是Request的散列值。咱們能夠看看Scrapy的源代碼,以下所示:

import hashlib
def request_fingerprint(request, include_headers=None):
    if include_headers:
        include_headers = tuple(to_bytes(h.lower())
                                 for h in sorted(include_headers))
    cache = _fingerprint_cache.setdefault(request, {})
    if include_headers not in cache:
        fp = hashlib.sha1()
        fp.update(to_bytes(request.method))
        fp.update(to_bytes(canonicalize_url(request.url)))
        fp.update(request.body or b'')
        if include_headers:
            for hdr in include_headers:
                if hdr in request.headers:
                    fp.update(hdr)
                    for v in request.headers.getlist(hdr):
                        fp.update(v)
        cache[include_headers] = fp.hexdigest()
    return cache[include_headers]複製代碼

request_fingerprint()就是計算Request指紋的方法,其方法內部使用的是hashlib的sha1()方法。計算的字段包括Request的Method、URL、Body、Headers這幾部份內容,這裏只要有一點不一樣,那麼計算的結果就不一樣。計算獲得的結果是加密後的字符串,也就是指紋。每一個Request都有獨有的指紋,指紋就是一個字符串,斷定字符串是否重複比斷定Request對象是否重複容易得多,因此指紋能夠做爲斷定Request是否重複的依據。

那麼咱們如何斷定重複呢?Scrapy是這樣實現的,以下所示:

def __init__(self):
    self.fingerprints = set()

def request_seen(self, request):
    fp = self.request_fingerprint(request)
    if fp in self.fingerprints:
        return True
    self.fingerprints.add(fp)複製代碼

在去重的類RFPDupeFilter中,有一個request_seen()方法,這個方法有一個參數request,它的做用就是檢測該Request對象是否重複。這個方法調用request_fingerprint()獲取該Request的指紋,檢測這個指紋是否存在於fingerprints變量中,而fingerprints是一個集合,集合的元素都是不重複的。若是指紋存在,那麼就返回True,說明該Request是重複的,不然這個指紋加入到集合中。若是下次還有相同的Request傳遞過來,指紋也是相同的,那麼這時指紋就已經存在於集合中,Request對象就會直接斷定爲重複。這樣去重的目的就實現了。

Scrapy的去重過程就是,利用集合元素的不重複特性來實現Request的去重。

對於分佈式爬蟲來講,咱們確定不能再用每一個爬蟲各自的集合來去重了。由於這樣仍是每一個主機單獨維護本身的集合,不能作到共享。多臺主機若是生成了相同的Request,只能各自去重,各個主機之間就沒法作到去重了。

那麼要實現去重,這個指紋集合也須要是共享的,Redis正好有集合的存儲數據結構,咱們能夠利用Redis的集合做爲指紋集合,那麼這樣去重集合也是利用Redis共享的。每臺主機新生成Request以後,把該Request的指紋與集合比對,若是指紋已經存在,說明該Request是重複的,不然將Request的指紋加入到這個集合中便可。利用一樣的原理不一樣的存儲結構咱們也實現了分佈式Reqeust的去重。

4、防止中斷

在Scrapy中,爬蟲運行時的Request隊列放在內存中。爬蟲運行中斷後,這個隊列的空間就被釋放,此隊列就被銷燬了。因此一旦爬蟲運行中斷,爬蟲再次運行就至關於全新的爬取過程。

要作到中斷後繼續爬取,咱們能夠將隊列中的Request保存起來,下次爬取直接讀取保存數據便可獲取上次爬取的隊列。咱們在Scrapy中指定一個爬取隊列的存儲路徑便可,這個路徑使用JOB_DIR變量來標識,咱們能夠用以下命令來實現:

scrapy crawl spider -s JOB_DIR=crawls/spider複製代碼

更加詳細的使用方法能夠參見官方文檔,連接爲:https://doc.scrapy.org/en/latest/topics/jobs.html。

在Scrapy中,咱們實際是把爬取隊列保存到本地,第二次爬取直接讀取並恢復隊列便可。那麼在分佈式架構中咱們還用擔憂這個問題嗎?不須要。由於爬取隊列自己就是用數據庫保存的,若是爬蟲中斷了,數據庫中的Request依然是存在的,下次啓動就會接着上次中斷的地方繼續爬取。

因此,當Redis的隊列爲空時,爬蟲會從新爬取;當Redis的隊列不爲空時,爬蟲便會接着上次中斷之處繼續爬取。

5、架構實現

咱們接下來就須要在程序中實現這個架構了。首先實現一個共享的爬取隊列,還要實現去重的功能。另外,重寫一個Scheduer的實現,使之能夠從共享的爬取隊列存取Request。

幸運的是,已經有人實現了這些邏輯和架構,併發布成叫Scrapy-Redis的Python包。接下來,咱們看看Scrapy-Redis的源碼實現,以及它的詳細工做原理。


本資源首發於崔慶才的我的博客靜覓: Python3網絡爬蟲開發實戰教程 | 靜覓

如想了解更多爬蟲資訊,請關注個人我的微信公衆號:進擊的Coder

weixin.qq.com/r/5zsjOyvEZ… (二維碼自動識別)

相關文章
相關標籤/搜索