点击“简说Python”,选择“星标公众号”
网上大部分python实现的布隆过滤器库如:pybloomfilter、pybloom 但都是基于py2且哈希函数用的都是sha1类、md5类,效率不如mmh3.所以决定自己实现,
git地址:https://github.com/Sssmeb/BloomFilter
第一次自己实现库 求星星!!也欢迎讨论、指教!!
布隆过滤器是一种多哈希函数映射的快速查找算法,通常应用在一些需要快速判断某个元素是否属于集合,但并不严格要求100%正确的场合。
本质上是一种数据结构,比较巧妙的概率型数据结构。
布隆过滤器可能会出现误判,但不会漏判。即,如果过滤器判断该元素不在集合中,则元素一定不在集合中,但如果过滤器判断该元素在集合中,有一定的概率判断错误(在合适的参数情况下,误判率可以降低到0.000级别甚至更低)。
因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter相比于其他常见的算法极大节省了空间(相较于直接存储,可节省上千倍的空间)。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是存在误识别率和删除困难。
常见适用的场景主要利用布隆过滤器减少磁盘io或网络请求等:
黑名单
例如 邮件黑名单过滤器,判断邮件地址是否存在黑名单中
网络爬虫去重
K-V系统快速判断某个key是否存在
例如 Hbase每个Region都包含一个BloomFilter,用于快速判断某个key在该region中是否存在
缓解缓存穿透
大量查询不存在数据的请求,越过redis缓存后,全部打到数据库中
可以在服务器内存中搭建一个布隆过滤器缓解
一般将数据存储用做去重判断的方法有:
将数据直接存储到数据库中
用HashSet(字典结构)/redis的set 将数据存储起来,实现O(1)时间复杂度的查询
经过MD5 或 SHA-1等 单向哈希后再保存到HashSet或数据库
Bit-Map。建立一个BitSet,将每份数据通过哈希函数映射到某一位(bit)。
当数据量较小时,前3种方法都是不错的选择。但是当数据量非常大时(几G、甚至几十G)会出现存储瓶颈。
当数据量较大时,上述四种方法的表现:
查询效率非常低,每检查一个数据是否存在时都需要扫描全表。
占用大量的内存空间(内存较昂贵)
由于字符串经过MD5或SHA-1处理后,长度只有128bit或160bit,所以当数据本身长度较大时,比方法2节省内存。
消耗内存少,但单一哈希函数发生冲突的概率太高。若要冲突率降到1%,就要将Bitset的长度设置为数据个数的100倍。
基于以上的背景,可以看到:当数据量非常大时,方法4是较好的选择。但该较大的问题是冲突率高,为了降低冲突,Bloom Filter使用多个哈希函数,而不是一个。
总结BloomFilter的核心思想:
多个hash,增大随机性,减少hash碰撞的概率
扩大数组范围,使hash值均匀分布,进一步减少hash碰撞的概率
创建一个m位的BitSet,先将所有位初始化为0
插入数据流程:
加入字符串,经过k个哈希函数,分别计算出k个范围是0 - m-1的值
将k个值对应的BitSet位 置1
将数据经过k个哈希函数,分别计算出k个值
若k个位都为1,则判断存在。(可能误判)
有任意1位是0,则肯定不存在。
通过上述流程也得,布隆过滤器需要提前预定位数组的大小。
经典的布隆过滤器可以支持 add 和 isExist操作。但是不支持delete操作。
例如,有两个值共同覆盖了一个位,当需要删除其中一个值时,会导致另一个值的该位也被删除,最终导致错判。
可以使用计数删除解决这个问题。即不再使用bit位,而存储一个数值。插入操作时不再是置1,而是加1操作。判断时不再判断0、1,而是判断是否大于0。但是这种做法明显增大了占用的内存,这里不展开。
简单总结经典哈希函数的5个特点:
输入域无穷
输出域有固定范围
相同的输入,输出一定相同
不同的输入,可能相同
产生哈希碰撞的原因
数据足够多的情况下,输出域近乎均匀
离散性
用来评判哈希函数优劣的关键。哈希函数越好,离散性越好(输出值分布越均匀)。
将其返回值对m取余(%m),得到的返回值可以认为也会均匀的分布在0~m-1位置上
哈希函数的选择对性能影响较大,一个好(离散性高)的哈希函数能近似等概率的将字符串映射到各个bit。选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。
显然,哈希函数个数越少、位数组越小误报率就越高,效率越低。
取自:https://www.jianshu.com/p/2104d11ee0a2
哈希函数的个数k、位数组大小m、加入的字符串数量n、误报率p 的关系。
通过简单的数学推导可以得出以下结论:
哈希函数的个数k、位数组大小m、加入的字符串数量n、误报率p 的关系。
在已得误报率p、数据量的情况下(通过用户输入),我们来建立关于p的表达式。
k 次哈希函数某一 bit 位未被置为 1 的概率为:
利用一点高数变化,当m很大时
取自:https://blog.csdn.net/wh_springer/article/details/52193110
性能很低的哈希函数不是个好选择,推荐 MurmurHash、Fnv 这些。
Redis 因其支持 setbit 和 getbit 操作,且纯内存性能高等特点,因此天然就可以作为布隆过滤器来使用。可以通过redis实现分布式的持久化去重。但是需要注意redis的bitmap是用字符串来实现的,而redis规定字符串最长为512MB(40多亿位),因此生产环境中建议对体积庞大的布隆过滤器进行拆分。
限于文章篇幅,以下仅使用简单实现说明。具体实现代码:
https://github.com/Sssmeb/BloomFilter/tree/master
求星星求start!!也非常欢迎讨论、指点~
基于以上分析,通过python实现一个简单的版本,核心函数add和contains都很好理解。初始化参数仅是数组大小和哈希函数个数。常见的实现是误判率(根据误判率来调整函数的个数)。
取自:https://blog.csdn.net/happytofly/article/details/80124542
from bitarray import bitarray
# 3rd party
import mmh3
class BloomFilter(set):
def __init__(self, size, hash_count):
super(BloomFilter, self).__init__()
self.bit_array = bitarray(size)
self.bit_array.setall(0)
self.size = size
self.hash_count = hash_count
def __len__(self):
return self.size
def __iter__(self):
return iter(self.bit_array)
def add(self, item):
for ii in range(self.hash_count):
index = mmh3.hash(item, ii) % self.size
self.bit_array[index] = 1
return self
def __contains__(self, item):
out = True
for ii in range(self.hash_count):
index = mmh3.hash(item, ii) % self.size
if self.bit_array[index] == 0:
out = False
return out
murmur hash是一种非加密型哈希函数,适用于一般的哈希检索操作。对于规律性较强的key,murmurhash的随机分布特征表现更良好。
redis在实现字典时用到了两种不同的哈希算法,murmur hash就是其中一种(另一种是djb)。
redis中数据库、集群、哈希键、阻塞操作等功能都用到了这个算法。
相比于md5,murmur hash在万次测试中,性能高4-5倍。
简单的实现把数据放在本地内存中,无法实现布隆过滤器的共享,我们可以把数据放在redis中,用redis实现布隆过滤器。
思路是将布隆过滤器的位数组用redis的bitmap代替,由于redis最大申请空间为512MB,可以通过多个键来扩充位数组。
由于redis自带setbit、getbit,所以实现起来更加便捷。
具体实现参看git:https://github.com/Sssmeb/BloomFilter/tree/master
scrapy自带了去重的功能,主要是通过fingerprint(指纹)标志过滤,用set实现去重功能。
在源码中的实现
class RFPDupeFilter(BaseDupeFilter):
def __init__(self, path=None, debug=False):
self.file = None
self.fingerprints = set() # 集合
xxx # 省略
# 通过request_fingerprint计算出请求的fp
# 根据是否存在于fingerprints集合中判断
def request_seen(self, request):
fp = self.request_fingerprint(request)
if fp in self.fingerprints:
return True
self.fingerprints.add(fp)
if self.file:
self.file.write(fp + os.linesep)
request_fingerprint方法用于计算请求的指纹(fp)。去重指纹是sha1(method + url + body + header)
# 计算请求fp函数
def request_fingerprint(request, include_headers=None):
# 判断是否带请求头信息
if include_headers:
include_headers = tuple([h.lower() for h in sorted(include_headers)])
# 获取该请求的缓存
cache = _fingerprint_cache.setdefault(request, {})
# 如果是新请求头信息
if include_headers not in cache:
# sha1算法
fp = hashlib.sha1()
fp.update(request.method)
fp.update(canonicalize_url(request.url))
fp.update(request.body or '')
if include_headers:
for hdr in include_headers:
if hdr in request.headers:
fp.update(hdr)
for v in request.headers.getlist(hdr):
fp.update(v)
cache[include_headers] = fp.hexdigest()
return cache[include_headers]
如果想自定义Filter,可以通过继承,重写request_seen
from scrapy.dupefilter import RFPDupeFilter
class SeenURLFilter(RFPDupeFilter):
"""A dupe filter that considers the URL"""
def __init__(self, path=None):
self.urls_seen = set()
RFPDupeFilter.__init__(self, path)
def request_seen(self, request):
if request.url in self.urls_seen:
return True
else:
self.urls_seen.add(request.url)
# 修改settings设置
DUPEFILTER_CLASS ='scraper.custom_filters.SeenURLFilter'
scrapy-redis的策略基本和scrapy相同,只是所用的数据结构不同。
去重结构使用的是redis中的集合,键名为XX:dupefilter。该结构中存储了已爬取的请求。
另外, 请求队列使用的是redis中的有序集合, 键名为XX:request, 存储了待爬取的请求
items数据使用的是redis中的列表, 键名为XX:items, 存储了爬取到的数据
redis是内存数据库,也就是说以上的三块数据:所有待爬取的请求、爬取到的items数据、去重的集合,都会存在内存中。
请求队列会随着爬取的进行,动态的出入,不会无限的叠加。爬取到的items数据一般会转移到其他的数据库中(mysql、mongodb),也不会无限的叠加。但是去重集合会随着爬取的进行,添加新的指纹,导致占用的内存空间越来越大,最终可能成为运行瓶颈。
以下只介绍修改流程,布隆过滤器实现见git:https://github.com/Sssmeb/BloomFilter/tree/master
1. 加入文件
(可以先复制一份scrapy_redis源码文件到当前scrapy工作目录下)将自己编写的bloomfilter.py文件加入scrapy_redis源码中
dupefilter.py(去重相关)文件中 导入布隆过滤器文件
from .bloomfilter import BloomFilter
在init函数中,加入实例化
self.bf = BloomFilter(server, key)
修改request_seen方法的去重规则
fp = self.request_fingerprint(request)
if self.bf.is_exist(fp):
return True
else:
self.bf.add(fp)
return False
像正常使用scrapy_redis一样修改即可。
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
如果想标识这个特别的scrapy_redis,可以修改scrapy_redis目录名称,在导入时修改对应的文件名即可
https://piaosanlang.gitbooks.io/spiders/content/09day/section9.1.html
完整Python基础知识要点