用Python从零开始实现一个Bloomfilter

简介

如果你不知道什么是 Bloomfilter,可以在这里找到详尽的描述Bloomfilter 介绍。简单来说Bloomfilter是一个概率数据结构,功能上类似于集合的一个子集,可以向里面添加一个元素,或者判断一个元素是否在其中。不过你只能准确判断一个数据不在其中,对于那些Bloomfilter判定其中的元素,只能保证它有非常大的概率在其中(这个概率一般高达99.9%+)。

需要什么样的功能接口?

Bloomfilter需要存储输入数据的某种状态,每当向其中添加一个元素,它的状态就会发生变化,所以可以实现为一个类,用字节数组来保存状态。然后来考虑其初始化方法,一个Bloomfilter有三个参数,分别是输入数据规模n,字节数组大小m以及可以接受的错误率k(即错误率上限)
Bloomfilter 有两个主要的功能,添加一个元素的 add 和 测试一个元素是否在里面的 test,但是这个方法可以利用Python关键字 in更好的实现。

# bloomfilter.py

class Bloomfilter(object):

    def __init__(self, m, n, k):
        pass

    def add(self, element):
        pass

    def __contains__self, element):
        pass

用起来大概是这样

>>> from fastbloom import BloomFilter
>>> bf = BloomFilter() # 创建
>>> bf.add('http://www.github.com') # 添加元素
>>> 'http://www.github.com' in bf # 测试一个元素是否在其中
>>> True

需要什么样的底层支撑?

Bloomfilter 最大的优点就是内存占用小,带来的额外开销就是运行时间变长,所以其实从通用的角度讲只有一条标准,那就是尽可能的快
而Bloomfilter实际上由两部分组成:一个是作为实际存储空间的字节数组,因为实际的数组会非常大,所以需要能够快速的插入和查询;而另一个就是对输入元素进行映射的哈希函数,由于每一次插入和查询操作都需要用到这个函数,所以它的性能至关重要。

字节数组

这里用mmap作为底层存储,关于mmap你可以看这篇博客『认真分析mmap:是什么 为什么 怎么用』。它一大的好处就是对于内存的高效访问,同时在Python里的mmap模块实际实现使用C写的,所以可以大幅减少运行时间。但是mmap本身提供的接口太原始,所以需要对其进行封装。
实际上,我们所需要的就是一个比特数组,然后可以在这个比特数组上随机的进行访问和修改,所以实现了一个基于mmap的bitset。主要是实现了两个方法,一个是写入一个指定比特,另一个是测试一个指定比特是否为1。

# bitset.py

import mmap


PAGE_SIZE = 4096
Byte_SIZE = 8


class MmapBitSet(object):

    def __init__(self, size):
        byte_size = ((size / 8) / PAGE_SIZE + 1) * PAGE_SIZE
        self._data_store = mmap.mmap(-1, byte_size)
        self._size = byte_size * 8

    def _write_byte(self, pos, byte):
        self._data_store.seek(pos)
        self._data_store.write_byte(byte)

    def _read_byte(self, pos):
        self._data_store.seek(pos)
        return self._data_store.read_byte()

    def set(self, pos, val=True):
        assert isinstance(pos, int)
        if pos < 0 or pos > self._size:
            raise ValueError('Invalid value bit {bit}, '
                             'should between {start} - {end}'.format(bit=pos,
                                                                     start=0,
                                                                     end=self._size))
        byte_no = pos / Byte_SIZE
        inside_byte_no = pos % Byte_SIZE

        raw_byte = ord(self._read_byte(byte_no))
        if val:  # set to 1
            set_byte = raw_byte | (2 ** inside_byte_no)
        else:  # set to 0
            set_byte = raw_byte & (2 ** Byte_SIZE - 1 - 2 ** inside_byte_no)
        if set_byte == raw_byte:
            return
        set_byte_char = chr(set_byte)
        self._write_byte(byte_no, set_byte_char)

    def test(self, pos):
        byte_no = pos / Byte_SIZE
        inside_byte_no = pos % Byte_SIZE

        raw_byte = ord(self._read_byte(byte_no))
        bit = raw_byte & 2 ** inside_byte_no
        return True if bit else False

哈希函数

在哈希函数的选取上,由于Bloomfilter的特性需要快速,所以所有基于密钥的哈希算法都被排除在外,这里选择的是Murmur哈希和Spooky哈希,这两个是目前性能最好的字符串哈希函数之一,这里直接使用的pyhash的实现,因为它使用boost写的所以性能比较好。同时,本次实现的Bloomfilter支持对哈希函数的替换,只需要满足如下规则:
- 一个函数,接收字符串为参数,返回一个128 bit 的数字;
- 一个类,实现了call方法,其余同上。
除此之外,由于在实际的Bloomfilter中会用到多个哈希函数,而它们的数量又是不确定的,这里我们使用一个叫做 double hashing 的方法来产生互不相关的hash函数,可以得到和多个完全不同的哈希函数同样的性能。即new_hash = h1 + i * h2其中 i 为正整数。实现如下,非常简单:

# hash_tools.py

def double_hashing(delta, h1, h2):
    def new_hash(msg):
        return h1(msg) + delta * h2(msg)
    return new_hash

在生成一系列哈希函数时,由于对于给定的输入,h1和h2的输出值是确定的,每一个哈希函数值之间只是相差了一个delta权值。所以不需要每次单独计算多个哈希函数,只需要计算两个哈希值并产生多个哈希值即可。

# hash_tools.py

def hashes(msg, h1, h2, number):
    h1_value, h2_value = h1(msg), h2(msg)
    result = []
    for i in xrange(number):
        yield ( h1_value + i*h2_value )

回到Bloomfilter

参数的确定

到目前为止还有一个悬而未决的问题,那就是Bloomfilter有三个参数,需要如何确定呢?其实这取决于你对运行速度,内存占用以及错误率的综合考量,在不同情况下可以采用不同的方法。实际上错误率为(1-e-kn/m)k,可以通过这个式子,对参数进行确定。
而本次实现中采用的是官方推荐的方法,通过确定n和m来求最优的k=(m/n)ln(2),然后不断迭代使得错误率达到要求,这样可以使得占用空间m最小,具体实现如下:

# bloomfilter.py

class Bloomfilter(object):
    def _adjust_param(self, bits_size, expected_error_rate):
        n, estimated_m, estimated_k, error_rate = self.capacity, int(bits_size / 2), None, 1
        weight, e = math.log(2), math.exp(1)
        while error_rate > expected_error_rate:
            estimated_m *= 2
            estimated_k = int((float(estimated_m) / n) * weight) + 1
            error_rate = (1 - math.exp(- (estimated_k * n) / estimated_m)) ** estimated_k
        return estimated_m, estimated_k

实现Bloomfilter的接口

最后剩下的就只有一开始设计的几个接口了,有了前面的基础,其实已经比较简单了,只是简单的插入和查询操作。

# bloomfilter.py

class Bloomfilter(Object):
    def add(self, msg):
        if not isinstance(msg, str):
            msg = str(msg)
        positions = []
        for _hash_value in self._hashes(msg):
            positions.append(_hash_value % self.num_of_bits)
        for pos in sorted(positions):
            self._data_store.set(int(pos))

    def __contains__(self, msg):
        if not isinstance(msg, str):
            msg = str(msg)
        positions = []
        for _hash_value in self._hashes(msg):
            positions.append(_hash_value % self.num_of_bits)
        for position in sorted(positions):
            if not self._data_store.test(position):
                return False
        return True

完整代码 Github pybloomfilter

猜你喜欢

转载自blog.csdn.net/preyta/article/details/72970887