Bloom Filter原理实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_1290259791/article/details/82419005

Bloom Filter

Scrapy-Redis的存储

Scrapy-Redis将Request的指纹存储到Redis集合中,存储为长度为40,每一位都是16进制。
指纹:8297512a01c44b17b44b5281d2758ca23342e2d6
每个16进制数占4bit,1个指纹用40个16进制表示,占用空间为20Byte,1万个指纹占用空间200KB,1亿指纹占用1.86GB。Redis还存储了爬取队列,内存会进一步提高,当爬取到亿级别的规模,就要用更节省内存的去重算法Bloom Filter。

Bloom Filter算法

使用位数组来辅助检查,声明一个m位的位(bit)数组,所有位都是0。
Bloom Filter算法中,首先使用K个相互独立、随机的散列函数将x映射到长度为m的位数组上,将散列函数得到的结果记作位置索引,将位数组改位置变为1。我们用k个散列函数映射k个位置将对应的值变为1。

符合公式:m > n*k

  • m:代表位数组的长度。
  • n:n个集合元素
  • k:k个散列函数

散列算法

class HashMap(object):

    def __init__(self, m, seed):
        self.m = m
        self.seed = seed

    def hash(self, value):
        ret = 0
        for i in range(len(value)):
            ret += self.seed * ret + ord(ret[i])
        return (self.m - 1) & ret

定义一个HashMap类,构造函数中,m表示数组的位数,seed表示种子的值,不用的散列函数要有不同的seed,这样散列函数才不会碰撞。
hash()方法中:*vaule要处理的内容,遍历value的每一位,并用ord求出对应的ASCII值,然后混洗seed进行迭代求和运算*,最后得到一个值。
这个值由seed和value来确定。我们将这个值和m进行位运算,就获取到m位数组的映射结果,这样就实现了由字符串和seed来确定的散列函数。
当m固定只要seed值相同,散列函数就是相同的,当seed不同散列函数就不同。

多个散列函数

当k个散列函数在一起就实现了Bloom Filter,我们对这几个散列函数指定相同的m不同的seed

class BlommFilter:

    def __init__(self, server, key, bit=30, hash_number=6):
        self.m = 1 << bit
        self.seeds = range(hash_number)
        self.maps = [HashMap(self.m, seed) for seed in self.seeds]
        self.server = server
        self.key = key
  • server:Redis连接对象
  • key:键的名称
  • bit:数组的位数
  • hash_number:几个散列函数

当需要亿级别的数据,n为1亿的时候,散列函数k个数取10个左右。
m > n * k:m的值大约为10亿左右。
1 << bit:表示将1移位操作向左移动bit位,当bit位30的时候,相当2的30次方,等于1073741824,数量级在10亿,由于是位数组占用的大小为(10*9/8/1024/1024 = 128MB)大小。
开头Scrapy-Redis使用指纹来存储当有1亿指纹占用空间1.86G。

  • self.m:将1向左进行30位移动
  • self.seeds:获取6个种子值
  • self.maps:构造不同seed值的HashMap对象,保存到数组中使用。
  • self.server:Redis连接对象
  • self.key:key的名称

接下来实现插入和判断重复的方法。

    def exists(self, value):
        if not value:
            return False
        exist = 1
        for map in self.maps:
            offset = map.hash(value)
            exist = exist & self.server.getbit(self.key, offset)
        return exist

    def insert(self, value):
        for f in self.maps:
            offset = f.hash(value)
            self.server.setbit(self.key, offset, 1)

insert方法

Bloom Filter算法逐个调用散列函数对放入集合中的元素进行运算,在得到m位位数组中映射的位置,将位数组对应的位置置为1。
我们通过for循环来遍历散列函数,然后调用hash()方法算出映射位置offset,再利用setbit()将该位置变为1。这里遍历了6个散列函数,就要有6个位从0变为1。

exists方法

定义一个exist置为1,然后遍历所有散列函数对value进行运算,得到映射位置,用getbit()方法获取映射位置的结果,和exist进行于运算。
只要有一次getbit()得到结果为0,即m位数组中有对应的0,那么最终结果exist就为False,表示集合中没有这个Value。

测试实例

from redis import StrictRedis


class HashMap:

    def __init__(self, m, seed):
        self.m = m
        self.seed = seed

    def hash(self, value):
        ret = 0
        for i in range(len(value)):
            ret += self.seed * ret + ord(value[i])
        return (self.m - 1) & ret


class BlommFilter:

    def __init__(self, server, key, bit=30, hash_number=6):
        self.m = 1 << bit   # 移动的位数
        self.seeds = range(hash_number)
        self.maps = [HashMap(self.m, seed) for seed in self.seeds]
        self.server = server
        self.key = key

    def exists(self, value):
        if not value:
            return False
        exist = 1
        for map in self.maps:
            offset = map.hash(value)
            print(offset)
            exist = exist & self.server.getbit(self.key, offset)
        return exist

    def insert(self, value):
        for f in self.maps:
            offset = f.hash(value)
            self.server.setbit(self.key, offset, 1)

redis = StrictRedis(host='localhost', port=6379)
bf = BlommFilter(redis, 'hash_map', 5, 1)
bf.insert('Hello')
bf.insert('World')
result = bf.exists('Hello')
print(bool(result))
result = bf.exists('Python')
print(bool(result))

运行结果为

11
8
True
False
[Finished in 0.3s]

我们查看Redis里面对应的key的值和类型,可知道类型为String。

127.0.0.1:6379> type hash_map
string
127.0.0.1:6379> get hash_map
"\x00\x90"
127.0.0.1:6379> 

我们可以来计算一下,1向左移动了128位,有1个散列函数,能存128个元素。

我们看到offset偏移量为11和8,我们将第11位和第八位置为1。0000 0000 1001 0000就是查询到的结果\x00\x90,记得下次在Redis里计算结果的时候del一下,不然还会保存上次存储的结果。

setbit和getbit的用法

在redis中,存储的字符串都是二进制存在的。
设置一个key-value,键名叫demo值为’a’,’a’的ASCII码为97二进制就是0b0110 0001。
在二进制中每一位都是offest,在’a’中offest0=0,offest1=1,从左往右计数,也就是从高位往低位。

127.0.0.1:6379> set demo 'a'
OK
127.0.0.1:6379> get demo
"a"

接下来我们将’a’变为’b’用bit来操作,’b’的ASCII码为98对应的二进制就是0b01100010,就是将offset6=1,offset7=0。

127.0.0.1:6379> setbit demo 6 1
(integer) 0
127.0.0.1:6379> setbit demo 7 0
(integer) 1
127.0.0.1:6379> get demo
"b"

其中返回的integer就是没进行setbit之前的offset位的值。

误区

BlommFilter中在设置bit等于5时,并不是1向左移动5位,1向左移动的位数时 1<< bit后的。所以self.m=32移动了32位是31个位数组。

总结

就是调用多个散列函数来映射多个位,用其来构造Bloom Filter。
到最后在Redis存储的类型是String,以前指纹存储的时候是列表。

猜你喜欢

转载自blog.csdn.net/qq_1290259791/article/details/82419005