「去重」是平常工做中會常常用到的一項技能,在爬蟲領域更是經常使用,而且規模通常都比較大。去重須要考慮兩個點:去重的數據量、去重速度。爲了保持較快的去重速度,通常選擇在內存中進行去重。python
一、數據量不大時,能夠直接放在內存裏面進行去重,例如python可使用set()進行去重。git
二、當去重數據須要持久化時可使用redis的set數據結構。github
三、當數據量再大一點時,能夠用不一樣的加密算法先將長字符串壓縮成 16/32/40 個字符,再使用上面兩種方法去重;redis
四、當數據量達到億(甚至十億、百億)數量級時,內存有限,必須用「位」來去重,纔可以知足需求。Bloomfilter就是將去重對象映射到幾個內存「位」,經過幾個位的 0/1值來判斷一個對象是否已經存在。算法
五、然而Bloomfilter運行在一臺機器的內存上,不方便持久化(機器down掉就什麼都沒啦),也不方便分佈式爬蟲的統一去重。若是能夠在Redis上申請內存進行Bloomfilter,以上兩個問題就都能解決了。數據結構
# coding=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__': bf = BloomFilter() if bf.isContains('http://www.baidu.com'): # 判斷字符串是否存在 print 'exists!' else: print 'not exists!' bf.insert('http://www.baidu.com')
一、Bloomfilter算法如何使用位去重,這個百度上有不少解釋。簡單點說就是有幾個seeds,如今申請一段內存空間,一個seed能夠和字符串哈希映射到這段內存上的一個位,幾個位都爲1即表示該字符串已經存在。app
插入的時候也是,將映射出的幾個位都置爲1。框架
二、須要提醒一下的是Bloomfilter算法會有漏失機率,即不存在的字符串有必定機率被誤判爲已經存在。這個機率的大小與seeds的數量、申請的內存大小、去重對象的數量有關。下面有一張表,m表示內存大小(多少個位),scrapy
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。