本文偏應用和代碼實踐,理論請參考本文末尾參考文章python
一句話簡介:
過濾器,判斷這個元素在與不在,不在則100%不在;在則去查詢,b確認在不在。git
詳細簡介:
BloomFilter,中文名稱叫作布隆過濾器,是1970年由 Bloom 提出的,它能夠被用來檢測一個元素是否在一個集合中,它的空間利用效率很高,使用它能夠大大節省存儲空間。BloomFilter 使用位數組表示一個待檢測集合,並能夠快速地經過幾率算法判斷一個元素是否存在於這個集合中,因此利用這個算法咱們能夠實現去重效果。github
它的優勢是空間效率和查詢時間都遠遠超過通常算法,缺點是有必定的誤識別率和刪除困難。redis
一、大量爬蟲數據去重算法
二、保護數據安全:
廣告精確投放 :廣告主經過設備id,計算hash算法,在數據包(數據提供方)中去查找,若是在存在,則證實該設備id屬於目標人羣,進行投放廣告,同時保證設備id不泄露。數據提供方和廣告主都沒有暴露本身擁有的設備id。間接用戶畫像且不違數據安全法。詳見:https://zhuanlan.zhihu.com/p/...segmentfault
三、比特幣網絡轉帳確認
數組
SPV節點:SPV是「Simplified Payment Verification」(簡單支付驗證)的縮寫。中本聰論文簡要地說起了這一律念,指出:不運行徹底節點也可驗證支付,用戶只須要保存全部的block header就能夠了。用戶雖然不能本身驗證交易,但若是可以從區塊鏈的某處找到相符的交易,他就能夠知道網絡已經承認了這筆交易,並且獲得了網絡的多少個確認。安全
先去訪問布隆過濾器,去判斷交易記錄是否在某個block(區塊)裏存在。從海量數據(十億個區塊,每一個區塊1-2M的交易記錄,),快速獲得結果。
詳見:https://www.youtube.com/watch...網絡
四、分佈式系統(Map-Reduce)
把大任務切分紅塊,分配和驗證一個子任務是否在一個子系統上。併發
省空間,提高效率
咱們首先來回顧一下 ScrapyRedis 的去重機制,它將 Request 的指紋存儲到了 Redis 集合中,每一個指紋的長度爲 40,例如 27adcc2e8979cdee0c9cecbbe8bf8ff51edefb61 就是一個指紋,它的每一位都是 16 進制數。
讓咱們來計算一下用這種方式耗費的存儲空間,每一個 16 進制數佔用 4b,1 個指紋用 40 個 16 進制數表示,佔用空間爲 20B,因此 1 萬個指紋即佔用空間 200KB,1 億個指紋即佔用 2G,因此當咱們的爬取數量達到上億級別時,Redis 的佔用的內存就會變得很高,並且這僅僅是指紋的存儲,另外 Redis 還存儲了爬取隊列,內存佔用會進一步提升,更別說有多個 Scrapy 項目同時爬取的狀況了。因此當爬取達到億級別規模時 ScrapyRedis 提供的集合去重已經不能知足咱們的要求,因此在這裏咱們須要使用一個更加節省內存的去重算法,它叫作 BloomFilter。
https://github.com/jaybaird/p...
安裝:
pip install pybloom
該模塊包含兩個類實現布隆過濾器功能。BloomFilter
是定容。ScalableBloomFilter
能夠自動擴容
使用:
>>> from pybloom import BloomFilter >>> f = BloomFilter(capacity=1000, error_rate=0.001) # capacity是容量, error_rate 是能容忍的誤報率 >>> f.add('Traim304') # 當不存在該元素,返回False False >>> f.add('Traim304') # 若存在,返回 True True >>> 'Traim304' in f # 值得注意的是若返回 True。該元素可能存在, 也可能不存在。過濾器能允許存在必定的錯誤 True >>> 'Jacob' in f # 可是 False。則一定不存在 False >>> len(f) # 當前存在的元素 1 >>> f = BloomFilter(capacity=1000, error_rate=0.001) >>> from pybloom import ScalableBloomFilter >>> sbf = ScalableBloomFilter(mode=ScalableBloomFilter.SMALL_SET_GROWTH) >>> # sbf.add() 與 BloomFilter 同
超過誤報率時拋出異常
>>> f = BloomFilter(capacity=1000, error_rate=0.0000001) >>> for a in range(1000): ... _ = f.add(a) ... >>> len(a) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: object of type 'int' has no len() >>> len(f) 1000 >>> f.add(1000) False >>> f.add(1001) # 當誤報率超過 error_rate 會報錯 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/local/lib/python2.7/site-packages/pybloom/pybloom.py", line 182, in add raise IndexError("BloomFilter is at capacity") IndexError: BloomFilter is at capacity
大數據量,多用Redis持久化版本的布隆過濾器
# 布隆過濾器 redis版本實現 import hashlib import redis import six # 1. 多個hash函數的實現和求值 # 2. hash表實現和實現對應的映射和判斷 class MultipleHash(object): '''根據提供的原始數據,和預約義的多個salt,生成多個hash函數值''' def __init__(self, salts, hash_func_name="md5"): self.hash_func = getattr(hashlib, hash_func_name) if len(salts) < 3: raise Exception("請至少提供3個salt") self.salts = salts def get_hash_values(self, data): '''根據提供的原始數據, 返回多個hash函數值''' hash_values = [] for i in self.salts: hash_obj = self.hash_func() hash_obj.update(self._safe_data(data)) hash_obj.update(self._safe_data(i)) ret = hash_obj.hexdigest() hash_values.append(int(ret, 16)) return hash_values def _safe_data(self, data): ''' python2 str === python3 bytes python2 uniocde === python3 str :param data: 給定的原始數據 :return: 二進制類型的字符串數據 ''' if six.PY3: if isinstance(data, bytes): return data elif isinstance(data, str): return data.encode() else: raise Exception("請提供一個字符串") # 建議使用英文來描述 else: if isinstance(data, str): return data elif isinstance(data, unicode): return data.encode() else: raise Exception("請提供一個字符串") # 建議使用英文來描述 class BloomFilter(object): '''''' def __init__(self, salts, redis_host="localhost", redis_port=6379, redis_db=0, redis_key="bloomfilter"): self.redis_host = redis_host self.redis_port = redis_port self.redis_db = redis_db self.redis_key = redis_key self.client = self._get_redis_client() self.multiple_hash = MultipleHash(salts) def _get_redis_client(self): '''返回一個redis鏈接對象''' pool = redis.ConnectionPool(host=self.redis_host, port=self.redis_port, db=self.redis_db) client = redis.StrictRedis(connection_pool=pool) return client def save(self, data): '''''' hash_values = self.multiple_hash.get_hash_values(data) for hash_value in hash_values: offset = self._get_offset(hash_value) self.client.setbit(self.redis_key, offset, 1) return True def is_exists(self, data): hash_values = self.multiple_hash.get_hash_values(data) for hash_value in hash_values: offset = self._get_offset(hash_value) v = self.client.getbit(self.redis_key, offset) if v == 0: return False return True def _get_offset(self, hash_value): # 512M長度哈希表 # 2**8 = 256 # 2**20 = 1024 * 1024 # (2**8 * 2**20 * 2*3) 表明hash表的長度 若是同一項目中不能更改 return hash_value % (2**8 * 2**20 * 2*3) if __name__ == '__main__': data = ["asdfasdf", "123", "123", "456","asf", "asf"] bm = BloomFilter(salts=["1","2","3", "4"],redis_host="172.17.0.2") for d in data: if not bm.is_exists(d): bm.save(d) print("映射數據成功: ", d) else: print("發現重複數據:", d)
代碼已經打包成了一個 Python 包併發布到了 PyPi,連接爲:https://pypi.python.org/pypi/...,所以咱們之後若是想使用 ScrapyRedisBloomFilter 直接使用就行了,不須要再本身實現一遍。
咱們能夠直接使用Pip來安裝,命令以下:
pip3 install scrapy-redis-bloomfilter
使用的方法和 ScrapyRedis 基本類似,在這裏說明幾個關鍵配置:
# 去重類,要使用BloomFilter請替換DUPEFILTER_CLASS DUPEFILTER_CLASS = "scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter" # 哈希函數的個數,默認爲6,能夠自行修改 BLOOMFILTER_HASH_NUMBER = 6 # BloomFilter的bit參數,默認30,佔用128MB空間,去重量級1億 BLOOMFILTER_BIT = 30
DUPEFILTER_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 億,請務必將此參數對應調高。
測試
Spider 文件: from scrapy import Request, Spider class TestSpider(Spider): name = 'test' base_url = 'https://www.baidu.com/s?wd=' def start_requests(self): for i in range(10): url = self.base_url + str(i) yield Request(url, callback=self.parse) # Here contains 10 duplicated Requests for i in range(100): url = self.base_url + str(i) yield Request(url, callback=self.parse) def parse(self, response): self.logger.debug('Response of ' + response.url)
在 start_requests() 方法中首先循環 10 次,構造參數爲 0-9 的 URL,而後從新循環了 100 次,構造了參數爲 0-99 的 URL,那麼這裏就會包含 10 個重複的 Request,咱們運行項目測試一下:
scrapy crawl test
能夠看到最後的輸出結果以下:
{'bloomfilter/filtered': 10, 'downloader/request_bytes': 34021, 'downloader/request_count': 100, 'downloader/request_method_count/GET': 100, 'downloader/response_bytes': 72943, 'downloader/response_count': 100, 'downloader/response_status_count/200': 100, 'finish_reason': 'finished', 'finish_time': datetime.datetime(2017, 8, 11, 9, 34, 30, 419597), 'log_count/DEBUG': 202, 'log_count/INFO': 7, 'memusage/max': 54153216, 'memusage/startup': 54153216, 'response_received_count': 100, 'scheduler/dequeued/redis': 100, 'scheduler/enqueued/redis': 100, 'start_time': datetime.datetime(2017, 8, 11, 9, 34, 26, 495018)}
能夠看到最後統計的第一行的結果:
'bloomfilter/filtered': 10,
這就是 BloomFilter 過濾後的統計結果,能夠看到它的過濾個數爲 10 個,也就是它成功將重複的 10 個 Reqeust 識別出來了,測試經過。
本文偏應用,難以描述的原理,最後說。
一個很長的二進制向量和一個映射函數。
一、https://zhuanlan.zhihu.com/p/...
二、https://www.youtube.com/watch...
三、《python3網絡爬蟲開發實戰》崔慶才
四、https://www.jianshu.com/p/f57...