1. 程式人生 > >基於Redis的Bloomfilter去重(附Python程式碼)

基於Redis的Bloomfilter去重(附Python程式碼)

“去重”是日常工作中會經常用到的一項技能,在爬蟲領域更是常用,並且規模一般都比較大。去重需要考慮兩個點:去重的資料量、去重速度。為了保持較快的去重速度,一般選擇在記憶體中進行去重。

資料量不大時,可以直接放在記憶體裡面進行去重,例如python可以使用set()進行去重。

當去重資料需要持久化時可以使用redis的set資料結構。

當資料量再大一點時,可以用不同的加密演算法先將長字串壓縮成 16/32/40 個字元,再使用上面兩種方法去重;

當資料量達到億(甚至十億、百億)數量級時,記憶體有限,必須用“位”來去重,才能夠滿足需求。Bloomfilter就是將去重物件對映到幾個記憶體“位”,通過幾個位的 0/1值來判斷一個物件是否已經存在。

然而Bloomfilter執行在一臺機器的記憶體上,不方便持久化(機器down掉就什麼都沒啦),也不方便分散式爬蟲的統一去重。如果可以在Redis上申請記憶體進行Bloomfilter,以上兩個問題就都能解決了。

本文即是用Python,基於Redis實現Bloomfilter去重。下面先放程式碼,最後附上說明。

程式碼:

# encoding=utf-8

import redis

from hashlib import md5

class SimpleHash(object):

def init(self, cap, seed):

self.cap = cap

self.seed = seed

def hash(self, value):

ret = 0

for i in range(len(value)):

ret += self.seed * ret + ord(value[i])

return (self.cap - 1) & ret

class BloomFilter(object):

def init(self, host=‘localhost’, port=6379, db=0, blockNum=1, key=‘bloomfilter’):

“”"

:param host: the host of Redis

:param port: the port of Redis

:param db: witch db in Redis

:param blockNum: one blockNum for about 90,000,000; if you have more strings for filtering, increase it.

:param key: the key’s name in Redis

“”"

self.server = redis.Redis(host=host, port=port, db=db)

self.bit_size = 1 << 31 # Redis的String型別最大容量為512M,現使用256M

self.seeds = [5, 7, 11, 13, 31, 37, 61]

self.key = key

self.blockNum = blockNum

self.hashfunc = []

for seed in self.seeds:

self.hashfunc.append(SimpleHash(self.bit_size, seed))

def isContains(self, str_input):

if not str_input:

return False

m5 = md5()

m5.update(str_input)

str_input = m5.hexdigest()

ret = True

name = self.key + str(int(str_input[0:2], 16) % self.blockNum)

for f in self.hashfunc:

loc = f.hash(str_input)

ret = ret & self.server.getbit(name, loc)

return ret

def insert(self, str_input):

m5 = md5()

m5.update(str_input)

str_input = m5.hexdigest()

name = self.key + str(int(str_input[0:2], 16) % self.blockNum)

for f in self.hashfunc:

loc = f.hash(str_input)

self.server.setbit(name, loc, 1)

if name == ‘main’:

“”" 第一次執行時會顯示 not exists!,之後再執行會顯示 exists! “”"

bf = BloomFilter()

if bf.isContains(‘http://www.baidu.com’): # 判斷字串是否存在

print ‘exists!’

else:

print ‘not exists!’

說明:

Bloomfilter演算法如何使用位去重,這個百度上有很多解釋。簡單點說就是有幾個seeds,現在申請一段記憶體空間,一個seed可以和字串雜湊對映到這段記憶體上的一個位,幾個位都為1即表示該字串已經存在。插入的時候也是,將映射出的幾個位都置為1。

需要提醒一下的是Bloomfilter演算法會有漏失概率,即不存在的字串有一定概率被誤判為已經存在。這個概率的大小與seeds的數量、申請的記憶體大小、去重物件的數量有關。下面有一張表,m表示記憶體大小(多少個位),n表示去重物件的數量,k表示seed的個數。例如我程式碼中申請了256M,即1<<31(m=2^31,約21.5億),seed設定了7個。看k=7那一列,當漏失率為8.56e-05時,m/n值為23。所以n = 21.5/23 = 0.93(億),表示漏失概率為8.56e-05時,256M記憶體可滿足0.93億條字串的去重。同理當漏失率為0.000112時,256M記憶體可滿足0.98億條字串的去重。

基於Redis的Bloomfilter去重,其實就是利用了Redis的String資料結構,但Redis一個String最大隻能512M,所以如果去重的資料量大,需要申請多個去重塊(程式碼中blockNum即表示去重塊的數量)。

程式碼中使用了MD5加密壓縮,將字串壓縮到了32個字元(也可用hashlib.sha1()壓縮成40個字元)。它有兩個作用,一是Bloomfilter對一個很長的字串雜湊對映的時候會出錯,經常誤判為已存在,壓縮後就不再有這個問題;二是壓縮後的字元為 0~f 共16中可能,我截取了前兩個字元,再根據blockNum將字串指定到不同的去重塊進行去重。

總結:

基於Redis的Bloomfilter去重,既用上了Bloomfilter的海量去重能力,又用上了Redis的可持久化能力,基於Redis也方便分散式機器的去重。在使用的過程中,要預算好待去重的資料量,則根據上面的表,適當地調整seed的數量和blockNum數量(seed越少肯定去重速度越快,但漏失率越大)。

另外針對基於Scrapy+Redis框架的爬蟲,我使用Bloomfilter作了一些優化,只需替換scrapy_redis模組即可使用Bloomfilter去重,並且去重佇列和種子隊列可以拆分到不同的機器上,詳情見:《scrapy_redis去重優化(已有7億條資料),附Demo福利》,程式碼見:Scrapy_Redis_Bloomfilter。