scrapy去重原理,scrapy_redis去重原理和布隆过滤器的使用
1.去重的應用場景:
如果你只是做一些簡單的爬蟲,可能不會遇到這種問題,可是如果你正在做一個大型的全站爬蟲,或是一個持久化的爬蟲,那你一定會遇到這樣的問題:剛開始爬蟲速度還可以,隨著待爬取的隊列達到數億級甚至更多的時候,爬蟲會變得非常慢,為什么會出現這樣的問題呢? 我們以scrapy為例來說明并解決這個問題。
2.scrapy去重原理
Scrapy 有自動去重功能,它的去重使用了 Python 中的集合。這個集合記錄了 Scrapy 中每個 Request 的指紋,這個指紋實際上就是 Request 的散列值。
在scrapy中發出一個請求時,會有一個參數dont_filter,而scrapy會根據這個參數來判斷是否去重。
scrapy 對 request 不做去重很簡單,只需要在 request 對象中設置dont_filter為 True。默認是False, 默認是去重,改為True就不去重了。比如說:
yield scrapy.Request(url, callback=self.get_response, dont_filter=True)那么,scrapy是怎么去重的呢,讓我們從源碼角度來分析一下scrapy是怎么去重的。我們可以看看 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 方法。然后再使用 to_bytes方法,計算的字段包括 Request 的 Method、URL、Body、Headers 這幾部分內容,這里只要有一點不同,那么計算的結果就不同。計算得到的結果是加密后的字符串,也就是指紋。
to_bytes方法核心代碼如下:
def to_bytes(text, encoding=None, errors='strict'):"""Return the binary representation of `text`. If `text`is already a bytes object, return it as-is."""if isinstance(text, bytes):return textif not isinstance(text, six.string_types):raise TypeError('to_bytes must receive a unicode, str or bytes ''object, got %s' % type(text).__name__)if encoding is None:encoding = 'utf-8'return text.encode(encoding, errors)每個 Request 都有獨有的指紋,指紋就是一個字符串,判定字符串是否重復比判定 Request 對象是否重復容易得多,所以指紋可以作為判定 Request 是否重復的依據。
那么我們如何判定是否重復呢?Scrapy 是這樣實現的,如下所示:
class RFPDupeFilter(BaseDupeFilter):"""Request Fingerprint duplicates filter"""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)if self.file:self.file.write(fp + os.linesep)def request_fingerprint(self, request):return request_fingerprint(request)在去重的類 RFPDupeFilter 中,有一個 request_seen 方法,這個方法有一個參數 request,它的作用就是檢測該 Request 對象是否重復。這個方法調用 request_fingerprint 獲取該 Request 的指紋,檢測這個指紋是否存在于 fingerprints 變量中,而 fingerprints 是一個集合,集合的元素都是不重復的。
如果指紋存在,那么就返回 True,說明該 Request 是重復的,否則將這個指紋加入集合中。如果下次還有相同的 Request 傳遞過來,指紋也是相同的,那么這時指紋就已經存在于集合中,Request 對象就會直接判定為重復。這樣去重的目的就實現了。
簡單的總結一句話就是,Scrapy 的去重過程就是,利用集合元素的不重復特性來實現 Request 的去重。
參考博客:第47講:大幅提速,分布式爬蟲理念
3.scrapy_redis去重原理
Scrapy 的去重是利用集合來實現的,而scrapy-redis的去重就需要利用共享的集合,只不過是將指紋池儲存到了redis中。
核心代碼如下:
def request_seen(self, request):fp = self.request_fingerprint(request)# This returns the number of values added, zero if already exists.added = self.server.sadd(self.key, fp)return added == 0def request_fingerprint(self, request):return request_fingerprint(request)這里同樣實現了一個 request_seen 方法,和 Scrapy 中的 request_seen 方法實現極其類似。不過這里集合使用的是 server 對象的 sadd 操作,也就是集合不再是一個簡單數據結構了,而是直接換成了數據庫的存儲方式。
鑒別重復的方式還是使用指紋,指紋同樣是依靠 request_fingerprint 方法來獲取的。獲取指紋之后就直接向集合添加指紋,如果添加成功,說明這個指紋原本不存在于集合中,返回值 1。代碼中最后的返回結果是判定添加結果是否為 0,如果剛才的返回值為 1,那這個判定結果就是 False,也就是不重復,否則判定為重復。
這樣我們就成功利用 Redis 的集合完成了指紋的記錄和重復的驗證。
scrapy-redis 重寫了 scrapy 的調度器和去重隊列,所以需要在 settings 中修改如下兩列
# Enables scheduling storing requests queue in redis. SCHEDULER = "scrapy_redis.scheduler.Scheduler"# Ensure all spiders share same duplicates filter through redis. DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"有關scrapy_redis去重原理詳細的介紹,可以看下面這篇博客。
第48講:分布式利器 Scrapy-Redis 原理
3.Bloom Filter布隆過濾器原理
3.1.了解 BloomFilter
Bloom Filter,中文名稱叫作布隆過濾器,是 1970 年由 Bloom 提出的,它可以被用來檢測一個元素是否在一個集合中。Bloom Filter 的空間利用效率很高,使用它可以大大節省存儲空間。Bloom Filter 使用位數組表示一個待檢測集合,并可以快速地通過概率算法判斷一個元素是否存在于這個集合中。利用這個算法我們可以實現去重效果。
3.2. BloomFilter 的算法
在 Bloom Filter 中使用位數組來輔助實現檢測判斷。在初始狀態下,我們聲明一個包含 m 位的位數組,它的所有位都是 0,如圖 所示。
現在我們有了一個待檢測集合,我們表示為 S={x1, x2, …, xn},我們接下來需要做的就是檢測一個 x 是否已經存在于集合 S 中。在 BloomFilter 算法中首先使用 k 個相互獨立的、隨機的哈希函數來將這個集合 S 中的每個元素 x1、x2、…、xn 映射到這個長度為 m 的位數組上,哈希函數得到的結果記作位置索引,然后將位數組該位置索引的位置 1。例如這里我們取 k 為 3,即有三個哈希函數,x1 經過三個哈希函數映射得到的結果分別為 1、4、8,x2 經過三個哈希函數映射得到的結果分別為 4、6、10,那么就會將位數組的 1、4、6、8、10 這五位置 1,如圖 所示:
這時如果再有一個新的元素 x,我們要判斷 x 是否屬于 S 這個集合,我們便會將仍然用 k 個哈希函數對 x 求映射結果,如果所有結果對應的位數組位置均為 1,那么我們就認為 x 屬于 S 這個集合,否則如果有一個不為 1,則 x 不屬于 S 集合。
例如一個新元素 x 經過三個哈希函數映射的結果為 4、6、8,對應的位置均為 1,則判斷 x 屬于 S 這個集合。如果結果為 4、6、7,7 對應的位置為 0,則判定 x 不屬于 S 這個集合。
注意這里 m、n、k 滿足的關系是 m>nk,也就是說位數組的長度 m 要比集合元素 n 和哈希函數 k 的乘積還要大。
這樣的判定方法很高效,但是也是有代價的,它可能把不屬于這個集合的元素誤認為屬于這個集合,我們來估計一下它的錯誤率。當集合 S={x1, x2,…, xn} 的所有元素都被 k 個哈希函數映射到 m 位的位數組中時,這個位數組中某一位還是 0 的概率是:
因為哈希函數是隨機的,所以任意一個哈希函數選中這一位的概率為 1/m,那么 1-1/m 就代表哈希函數一次沒有選中這一位的概率,要把 S 完全映射到 m 位數組中,需要做 kn 次哈希運算,所以最后的概率就是 1-1/m 的 kn 次方。
一個不屬于 S 的元素 x 如果要被誤判定為在 S 中,那么這個概率就是 k 次哈希運算得到的結果對應的位數組位置都為 1,所以誤判概率為:
根據:
可以將誤判概率轉化為:
在給定 m、n 時,可以求出使得 f 最小化的 k 值為:
在這里將誤判概率歸納如下:
表 中第一列為 m/n 的值,第二列為最優 k 值,其后列為不同 k 值的誤判概率,可以看到當 k 值確定時,隨著 m/n 的增大,誤判概率逐漸變小。當 m/n 的值確定時,當 k 越靠近最優 K 值,誤判概率越小。另外誤判概率總體來看都是極小的,在容忍此誤判概率的情況下,大幅減小存儲空間和判定速度是完全值得的。
3.3 布隆過濾器對接scrapy的使用:
安裝scrapy-redis-bloomfilter 模塊
pip install scrapy-redis-bloomfilter修改setting 文件:
# Ensure use this Scheduler SCHEDULER = "scrapy_redis_bloomfilter.scheduler.Scheduler"# 把去重模塊更改為scrapy-redis-bloomfilter寫好的模塊 DUPEFILTER_CLASS = "scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter"# Redis URL REDIS_URL = 'redis://localhost:6379/0'# 散列函數的個數,個人偏向設置為10,不設置則默認為6, BLOOMFILTER_HASH_NUMBER = 6# Bloom Filter的bit參數,默認30(一億級指紋池) BLOOMFILTER_BIT = 30# Persist SCHEDULER_PERSIST = TrueDUPEFILTER_CLASS 是去重類,如果要使用 BloomFilter 需要將 DUPEFILTER_CLASS 修改為該包的去重類。
BLOOMFILTER_HASH_NUMBER 是 BloomFilter 使用的哈希函數的個數,默認為 6,可以根據去重量級自行修改。
BLOOMFILTER_BIT 即前文所介紹的 BloomFilter 類的 bit 參數,它決定了位數組的位數,如果 BLOOMFILTER_BIT 為 30,那么位數組位數為 2 的 30 次方,將占用 Redis 128MB 的存儲空間,去重量級在 1 億左右,即對應爬取量級 1 億左右。如果爬取量級在 10 億、20 億甚至 100 億,請務必將此參數對應調高。
參考博客:崔慶才的 14.4–Bloom Filter 的對接
總結
以上是生活随笔為你收集整理的scrapy去重原理,scrapy_redis去重原理和布隆过滤器的使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Scrapy框架发送POST请求
- 下一篇: docker下安装Pillow模块