Python中如何实现Bloomfilter?小白求指点

第一次写了一个完整的小工具,不知道写得怎么样,求各位大大指点一下 Github pybloomfilter


Python中如何实现Bloomfilter?小白求指点
12 回复

提个小建议,README 里面,ops 的 ps 已经有每秒的意思,就不需要 /s 了。


在Python里实现一个简单的Bloom Filter不难,核心就三部分:一个大的比特数组(位向量)、几个哈希函数,以及“添加”和“查询”两个方法。下面我直接给你一个能跑的代码,你一看就懂。

import mmap
import hashlib
from bitarray import bitarray
from math import log

class BloomFilter:
    def __init__(self, capacity, error_rate=0.001):
        """
        初始化布隆过滤器
        :param capacity: 预期要存放的元素数量
        :param error_rate: 可接受的误判率
        """
        # 计算需要的比特数组大小 (m) 和哈希函数个数 (k)
        self.capacity = capacity
        self.error_rate = error_rate
        self.num_bits = int(-(capacity * log(error_rate)) / (log(2) ** 2))  # 公式计算位数组大小
        self.num_hashes = int((self.num_bits / capacity) * log(2))  # 公式计算哈希函数个数

        # 初始化位数组,所有位设为0
        self.bit_array = bitarray(self.num_bits)
        self.bit_array.setall(0)

    def _hash(self, item, seed):
        """使用双重哈希(或多次哈希)模拟多个独立的哈希函数"""
        # 先用一个哈希函数(如MD5)生成一个大的摘要
        hash_obj = hashlib.md5(item.encode())
        digest = int(hash_obj.hexdigest(), 16)
        # 然后通过不同的种子(seed)来生成不同的哈希值
        hash_val = (digest + seed) % self.num_bits
        return hash_val

    def add(self, item):
        """将一个元素添加到布隆过滤器中"""
        for i in range(self.num_hashes):
            # 对每个哈希函数,计算在位数组中的位置,并设为1
            index = self._hash(item, i)
            self.bit_array[index] = 1

    def __contains__(self, item):
        """检查一个元素是否可能在布隆过滤器中(可能有误判)"""
        for i in range(self.num_hashes):
            index = self._hash(item, i)
            if not self.bit_array[index]:
                # 只要有一个位置是0,肯定不存在
                return False
        # 所有位置都是1,可能存在(但有误判可能)
        return True

# ---------- 使用示例 ----------
if __name__ == "__main__":
    # 创建一个预期存放10000个元素,误判率0.1%的布隆过滤器
    bf = BloomFilter(capacity=10000, error_rate=0.001)

    # 添加一些元素
    items_to_add = ["apple", "banana", "cherry", "date"]
    for item in items_to_add:
        bf.add(item)
        print(f"已添加: {item}")

    # 测试查询
    test_items = ["apple", "banana", "grape", "fig"]
    for item in test_items:
        if item in bf:
            print(f"'{item}' 可能在集合中(或发生了误判)")
        else:
            print(f"'{item}' 肯定不在集合中")

代码解释:

  1. 初始化 (__init__): 根据你预期的元素数量 (capacity) 和可接受的误判率 (error_rate),用公式计算出需要多大的比特数组 (num_bits) 和多少个哈希函数 (num_hashes)。这里用了 bitarray 库来高效地管理大的位数组,你需要先 pip install bitarray
  2. 哈希函数 (_hash): 这是关键。我们不可能真的写N个不同的哈希算法。这里用一个“种子”技巧:先用MD5(或SHA256)生成一个大的整数摘要,然后通过加不同的种子 (seed) 并取模,来模拟多个独立的哈希函数的结果。num_hashes 是多少,我们就循环多少次,每次种子 i 不同。
  3. 添加 (add): 添加一个元素时,用所有哈希函数算出N个位置,把位数组中这些位置都设为1。
  4. 查询 (__contains__): 查询时,同样用所有哈希函数算出N个位置。如果任何一个位置是0,那这个元素肯定没添加过。如果所有位置都是1,那这个元素可能添加过(但也可能是其他元素把这些位碰巧都设成了1,这就是“误判”)。

运行一下,你会看到:

  • “apple” 和 “banana” 我们添加过,所以会报告“可能在”。
  • “grape” 和 “fig” 没添加过,所以会报告“肯定不在”。(在很小的误判率下,它们也可能被误判为存在,但这里大概率不会)。

一句话总结:用多个哈希函数把元素映射到位数组上,查询时全为1才可能存在,否则肯定不存在。

对于生产环境,建议直接使用成熟的库如 pybloom-live (pip install pybloom-live),它们经过优化,功能更全。但自己实现一遍对理解原理非常有帮助。

恩,谢谢,我这就去改正回来,想问下 ops 是 operation per sec 吗?

挺不错的,建议除了可以 save 和 restore 之外 还可以直接在持久化的文件上进行 add 另外 fpp 那个测试还是草率了点,弄点实际的 URL 去测吧…

恩,这个测试是太草率了点…

>>> filter_ = Bloomfilter(1000, 0.001)

# set size of input 1000, error rate 1%

1% 不应该是 0.01 么?

应该是的吧 :)

嗯嗯,前面写测试的时候手贱了,看得好仔细

前面查资料的时候也有看过你这个博客

可以贴一下各项速度和 python 内置的 set 对比图么

刚才粗略的测试了一下,输入在一亿数量级往下的时候 set 还是快不少的,用不着 bloomfilter,但是当 set 的内存开销达到系统瓶颈时,性能会急剧下降,在我的机器上测试了三亿次插入操作,速度已经降到了 355ops,不过我的机器也就 8G 内存。

回到顶部