分布式爬蟲python
A. python分布式爬蟲是什麼意思
一、分布式爬蟲架構
在了解分布式爬蟲架構之前,首先回顧一下Scrapy的架構,如下圖所示。
我們需要做的就是在多台主機上同時運行爬蟲任務協同爬取,而協同爬取的前提就是共享爬取隊列。這樣各台主機就不需要各自維護爬取隊列,而是從共享爬取隊列存取Request。但是各台主機還是有各自的Scheler和Downloader,所以調度和下載功能分別完成。如果不考慮隊列存取性能消耗,爬取效率還是會成倍提高。
二、維護爬取隊列
那麼這個隊列用什麼來維護?首先需要考慮的就是性能問題。我們自然想到的是基於內存存儲的Redis,它支持多種數據結構,例如列表(List)、集合(Set)、有序集合(Sorted Set)等,存取的操作也非常簡單。
Redis支持的這幾種數據結構存儲各有優點。
列表有lpush()、lpop()、rpush()、rpop()方法,我們可以用它來實現先進先出式爬取隊列,也可以實現先進後出棧式爬取隊列。
集合的元素是無序的且不重復的,這樣我們可以非常方便地實現隨機排序且不重復的爬取隊列。
有序集合帶有分數表示,而Scrapy的Request也有優先順序的控制,我們可以用它來實現帶優先順序調度的隊列。
我們需要根據具體爬蟲的需求來靈活選擇不同的隊列。
三、如何去重
Scrapy有自動去重,它的去重使用了Python中的集合。這個集合記錄了Scrapy中每個Request的指紋,這個指紋實際上就是Request的散列值。我們可以看看Scrapy的源代碼,如下所示:
importhashlib
defrequest_fingerprint(request, include_headers=None):
ifinclude_headers:
include_headers = tuple(to_bytes(h.lower())
forhinsorted(include_headers))
cache = _fingerprint_cache.setdefault(request, {})
ifinclude_headersnotincache:
fp = hashlib.sha1()
fp.update(to_bytes(request.method))
fp.update(to_bytes(canonicalize_url(request.url)))
fp.update(request.bodyorb'')
ifinclude_headers:
forhdrininclude_headers:
ifhdrinrequest.headers:
fp.update(hdr)
forvinrequest.headers.getlist(hdr):
fp.update(v)
cache[include_headers] = fp.hexdigest()
returncache[include_headers]
request_fingerprint()就是計算Request指紋的方法,其方法內部使用的是hashlib的sha1()方法。計算的欄位包括Request的Method、URL、Body、Headers這幾部分內容,這里只要有一點不同,那麼計算的結果就不同。計算得到的結果是加密後的字元串,也就是指紋。每個Request都有獨有的指紋,指紋就是一個字元串,判定字元串是否重復比判定Request對象是否重復容易得多,所以指紋可以作為判定Request是否重復的依據。
那麼我們如何判定重復呢?Scrapy是這樣實現的,如下所示:
def__init__(self):
self.fingerprints = set()
defrequest_seen(self, request):
fp = self.request_fingerprint(request)
iffpinself.fingerprints:
returnTrue
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的去重。
四、防止中斷
在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的隊列不為空時,爬蟲便會接著上次中斷之處繼續爬取。
五、架構實現
我們接下來就需要在程序中實現這個架構了。首先實現一個共享的爬取隊列,還要實現去重的功能。另外,重寫一個Scheer的實現,使之可以從共享的爬取隊列存取Request。
幸運的是,已經有人實現了這些邏輯和架構,並發布成叫Scrapy-Redis的Python包。接下來,我們看看Scrapy-Redis的源碼實現,以及它的詳細工作原理