基於Redis的Bloomfilter去重(轉載)

轉載:http://blog.csdn.net/bone_ace/article/details/53107018

前言

  「去重」是平常工做中會常常用到的一項技能,在爬蟲領域更是經常使用,而且規模通常都比較大。去重須要考慮兩個點:去重的數據量、去重速度。爲了保持較快的去重速度,通常選擇在內存中進行去重。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

相關文章
相關標籤/搜索