布隆過濾器(Bloom Filter)的簡單實現
最近在部署Scrapy專案時,瞭解到Scrapy_Redis的去重機制並不太友好。查詢之後發現了一個更好的去重方式——布隆過濾器。
使用布隆過濾器的原因:
關於布隆過濾器的詳細原理及介紹,推薦一個部落格:https://www.cnblogs.com/haippy/archive/2012/07/13/2590351.html
需要從Scrapy_Redis的去重機制談起:
1:對每個Request都產生一個指紋,該指紋由40個16進位制的字元組成
2:將該指紋存入Redis集合(在記憶體中),利用集合的特性實現去重
這種方式會帶來一個問題:當資料量太大時,會消耗大量記憶體。
(具體分析):
# 一個子節佔8b,為1B(兩個16進位制數為一個位元組) |
# 一個16進位制數佔用4b存,則一個指紋佔用20B空間 |
# 一萬個指紋佔用200KB |
# 一億個指紋佔用2GB |
# 當資料量達到上億級別時將佔用太多記憶體 |
此時就需要布隆過濾器了:
# 該過濾器使用位陣列表示一個待檢測集合並通過概率演算法判斷一個元素是否存在某集合 |
# 位陣列:只存放0和1,並只進行位運算的陣列 |
實現過程的具體分析:
1:宣告一個m位的位陣列,初始值都為0
2:佇列中有n個待檢測的Request
3: BloomFilter中有k個雜湊函式(用於將Request對映到位陣列的函式)
4:每個待檢測Request依次經過k個雜湊函式,則得到k個位置
5:將上步的到的k個位置依次經過位陣列,將對應位置的0變為1
6:判斷某個Request是否已經存在,則將該Request經過k個雜湊函式得到k個位置後
在位陣列中一次查詢這k個位置,若都為 1,則表示該Request存在
若有一個不為 1,則表示該Request不存在於爬取佇列中
### 注:m(位陣列長度) > n(Request的個數)*k(雜湊函式的個數)(為了防止出現過多的誤判率)
### 該過濾器存在一定的誤判率,隨著n的增加誤判率隨之增加
程式碼實現:
1:定義一個包含雜湊函式的類(該類主要作用就是用於提供雜湊函式)
class MyHashMap(object):
"""該類用於實現一個簡單的雜湊函式的類"""
def __init__(self, m, seed):
self.m = m; # 位陣列的長度
self.seed = seed; # 表示是哪個雜湊函式(因為這裡只有一個雜湊函式,因此需要一個引數用於區分當前是第幾個雜湊函式)
def hash(self, value):
"""
簡單的雜湊函式
:param value: 待檢測的資料
:return: 要對映到位陣列的位置
"""
ret = 0;
# 遍歷資料中的每個數值
for i in range(len(value)):
# 對每個數值取ASCII碼值,混合seed進行迭代求和
ret += self.seed * ret + ord(value[i]);
# 返回一個位運算之後的值
return (self.m - 1) & ret;
2:定義兩個全域性變數(雜湊函式的個數和位陣列的長度):
BLOOMFILTER_HASH_NUM = 6; # 雜湊函式的個數
BLOOMFILTER_BIT = 30; # 位陣列的長度
3:布隆過濾器的主體類
class BloomFilter(object):
def __init__(self, serve, key, bit = BLOOMFILTER_BIT, hash_num = BLOOMFILTER_HASH_NUM):
"""
:param serve: Redis陣列的連線物件
:param key: m位位陣列的名稱
:param bit: 位陣列的長度
:param hash_num: 雜湊函式的個數
"""
self.m = 1 << bit;# 位陣列長度,位運算相當於2^30
self.seeds = range(hash_num); # 列表中每個數表示一個雜湊函式
# 雜湊函式類的物件
# self.maps = [];
# for s in self.seeds:
# self.maps.append(MyHashMap(self.m,s));
self.maps = [MyHashMap(self.m, seed) for seed in self.seeds];
self.serve = serve;
self.key = key;
def is_exists(self, value):
"""
判斷資料是否存在的方法
:param value: 待判斷的資料
:return: o或1(0:不存在。1:存在)
"""
if not value:
return False;
exist = 1;
for m in self.maps:
offset = m.hash(value); # 獲得對應的位置
# 用乘法也一樣
exist = exist & self.serve.getbit(self.key, offset);
return exist;
def insert(self, value):
"""
將value對映到位陣列中
:param value: 待對映的資料
:return:None
"""
for m in self.maps:
offset = m.hash(value);
# 將對應offset位置的值設為1
self.serve.setbit(self.key,offset,1);
4:測試所寫的布隆過濾器(測試方法,不重要)
if __name__ == '__main__':
"""測試BloomFilter"""
# from redis import StrictRedis;
import redis;
conn = redis.Redis(host="localhost",port=6379,password="123456");
bf = BloomFilter(conn,"testBF",5,6);
bf.insert("HELLO");
bf.insert("world!");
res = bf.is_exists("hello");
print(bool(res));
res = bf.is_exists("world");
print(bool(res));
res = bf.is_exists("world!");
print(bool(res));
最後附上完整程式碼(github):https://github.com/shyorange/CommonlyUsedToolsProjects/blob/master/BloomFilter.py
在分散式Scrapy中使用布隆過濾器的方法:
1:在scrapy_redis.dupefilter.RFPDupeFilter類的初始化方法中(生成一個BloomFilter的物件)
2:在RFPDupeFilter類的request_seen()方法中修改部分程式碼。如下:
# 將:
# added = self.server.sadd(self.key, fp)
# return added == 0
# 修改為:
# if self.bf.is_exists(fp):
# return True;
# self.bf.insert(fp);
# return False