第47讲:scrapy-redis分布式爬虫介绍
我們在前面幾節課了解了 Scrapy 爬蟲框架的用法。但這些框架都是在同一臺主機上運行的,爬取效率比較低。如果能夠實現多臺主機協同爬取,那么爬取效率必然會成倍增長,這就是分布式爬蟲的優勢。
接下來我們就來了解一下分布式爬蟲的基本原理,以及 Scrapy 實現分布式爬蟲的流程。
我們在前面已經實現了 Scrapy 基本的爬蟲功能,雖然爬蟲是異步加多線程的,但是我們卻只能在一臺主機上運行,所以爬取效率還是有限的,而分布式爬蟲則是將多臺主機組合起來,共同完成一個爬取任務,這將大大提高爬取的效率。
1.分布式爬蟲架構
在了解分布式爬蟲架構之前,首先回顧一下 Scrapy 的架構,如圖所示。
Scrapy 單機爬蟲中有一個本地爬取隊列 Queue,這個隊列是利用 deque 模塊實現的。如果新的 Request 生成就會放到隊列里面,隨后 Request 被 Scheduler 調度。之后,Request 交給 Downloader 執行爬取,簡單的調度架構如圖所示。
如果兩個 Scheduler 同時從隊列里面獲取 Request,每個 Scheduler 都會有其對應的 Downloader,那么在帶寬足夠、正常爬取且不考慮隊列存取壓力的情況下,爬取效率會有什么變化呢?沒錯,爬取效率會翻倍。
這樣,Scheduler 可以擴展多個,Downloader 也可以擴展多個。而爬取隊列 Queue 必須始終為一個,也就是所謂的共享爬取隊列。這樣才能保證 Scheduer 從隊列里調度某個 Request 之后,其他 Scheduler 不會重復調度此 Request,就可以做到多個 Schduler 同步爬取。這就是分布式爬蟲的基本雛形,簡單調度架構如圖所示。
我們需要做的就是在多臺主機上同時運行爬蟲任務協同爬取,而協同爬取的前提就是共享爬取隊列。這樣各臺主機就不需要維護各自的爬取隊列了,而是從共享爬取隊列存取 Request。但是各臺主機還有各自的 Scheduler 和 Downloader,所以調度和下載功能是分別完成的。如果不考慮隊列存取性能消耗,爬取效率還是可以成倍提高的。
2.維護爬取隊列
那么如何維護這個隊列呢?我們首先需要考慮的就是性能問題,那什么數據庫存取效率高呢?這時我們自然想到了基于內存存儲的 Redis,而且 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 Trueself.fingerprints.add(fp)在去重的類 RFPDupeFilter 中,有一個 request_seen 方法,這個方法有一個參數 request,它的作用就是檢測該 Request 對象是否重復。這個方法調用 request_fingerprint 獲取該 Request 的指紋,檢測這個指紋是否存在于 fingerprints 變量中,而 fingerprints 是一個集合,集合的元素都是不重復的。
如果指紋存在,那么就返回 True,說明該 Request 是重復的,否則將這個指紋加入集合中。如果下次還有相同的 Request 傳遞過來,指紋也是相同的,那么這時指紋就已經存在于集合中,Request 對象就會直接判定為重復。這樣去重的目的就實現了。
Scrapy 的去重過程就是,利用集合元素的不重復特性來實現 Request 的去重。
對于分布式爬蟲來說,我們肯定不能再使用每個爬蟲各自的集合來去重了。因為這樣還是每臺主機單獨維護自己的集合,不能做到共享。多臺主機如果生成了相同的 Request,只能各自去重,各個主機之間就無法做到去重了。
那么要實現多臺主機去重,這個指紋集合也需要是共享的,Redis 正好有集合的存儲數據結構,我們可以利用 Redis 的集合作為指紋集合,那么這樣去重集合也是共享的。
每臺主機新生成 Request 之后,會把該 Request 的指紋與集合比對,如果指紋已經存在,說明該 Request 是重復的,否則將 Request 的指紋加入這個集合中即可。利用同樣的原理不同的存儲結構我們也可以實現分布式 Reqeust 的去重。
4.防止中斷
在 Scrapy 中,爬蟲運行時的 Request 隊列放在內存中。爬蟲運行中斷后,這個隊列的空間就被釋放,此隊列就被銷毀了。所以一旦爬蟲運行中斷,爬蟲再次運行就相當于全新的爬取過程。
要做到中斷后繼續爬取,我們可以將隊列中的 Request 保存起來,下次爬取直接讀取保存數據即可獲取上次爬取的隊列。我們在 Scrapy 中指定一個爬取隊列的存儲路徑即可,這個路徑使用 JOB_DIR 變量來標識,我們可以用如下命令來實現:
scrapy crawl spider -s JOBDIR=crawls/spider更加詳細的使用方法可以參見官方文檔,鏈接為:https://doc.scrapy.org/en/latest/topics/jobs.html。
在 Scrapy 中,我們實際是把爬取隊列保存到本地,第二次爬取直接讀取并恢復隊列即可。那么在分布式架構中我們還用擔心這個問題嗎?不需要。因為爬取隊列本身就是用數據庫保存的,如果爬蟲中斷了,數據庫中的 Request 依然是存在的,下次啟動就會接著上次中斷的地方繼續爬取。
所以,當 Redis 的隊列為空時,爬蟲會重新爬取;當 Redis 的隊列不為空時,爬蟲便會接著上次中斷之處繼續爬取。
5.架構實現
我們接下來就需要在程序中實現這個架構了。首先需要實現一個共享的爬取隊列,還要實現去重功能。另外,還需要重寫一個 Scheduer 的實現,使之可以從共享的爬取隊列存取 Request。
幸運的是,已經有人實現了這些邏輯和架構,并發布成了叫作 Scrapy-Redis 的 Python 包。
在下一節,我們便看看 Scrapy-Redis 的源碼實現,以及它的詳細工作原理。
總結
以上是生活随笔為你收集整理的第47讲:scrapy-redis分布式爬虫介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第12讲:Ajax 的原理和解析
- 下一篇: 第46讲:遇到动态页面怎么办?详解渲染页