布隆過濾器詳解(python)


1、什么是布隆過濾器

  布隆過濾器是一種概率空間高效的數據結構,特點是高效地插入和查詢,用來告訴你 “某樣東西一定不存在或者可能存在”。
  相比於傳統的 List、Set、Map 等數據結構,它更高效、占用空間更少,但是缺點是其返回的結果是概率性的,而不是確切的。

2、實現原理

  (1)我們先來仔細地看看它的空間效率。如果你想在集合中存儲一系列的元素,有很多種不同的做法。你可以把數據映射到 HashMap 的 Key,然后在 O(1) 的時間復雜度內,hashmap的插入和查詢的效率都非常高。但是,由於hashmap直接存儲內容,所以空間利用率並不高。一旦你的值很多例如上億的時候,那 HashMap 占據的內存大小就變得很可觀了。還比如說你的數據集存儲在遠程服務器上,本地服務接受輸入,而數據集非常大不可能一次性讀進內存構建 HashMap 的時候,也會存在問題。
  (2)如果希望提高空間利用率,我們可以在元素插入集合之前做一次哈希變換。還有其它方法呢?我們可以用位數組來存儲元素的哈希值。還有嗎,還有嗎?我們也允許在位數組中存在哈希沖突。這正是布隆過濾器的工作原理,它們就是基於允許哈希沖突的位數組,可能會造成一些誤報。在布隆過濾器的設計階段就允許哈希沖突的存在,否則空間使用就不夠緊湊了。

3、布隆過濾器基礎

  布隆過濾器是N位的位數組,其中N是位數組的大小。它還有另一個參數k,表示使用哈希函數的個數。這些哈希函數用來設置位數組的值。當往過濾器中插入元素x時,h1(x), h2(x), ..., hk(x)所對應索引位置的值被置為“1”,索引值由各個哈希函數計算得到。注意,如果我們增加哈希函數的數量,誤報的概率會趨近於0.但是,插入和查找的時間開銷更大,布隆過濾器的容量也會減小。

  為了用布隆過濾器檢驗元素是否存在,我們需要校驗是否所有的位置都被置“1”,與我們插入元素的過程非常相似。如果所有位置都被置“1”,那也就意味着該元素很有可能存在於布隆過濾器中。若有位置未被置“1”,那該元素一定不存在。

4、布隆過濾器數據結構

  布隆過濾器是一個 bit 向量或者說 bit 數組,長這樣:
  現在我們新建一個長度為16的布隆過濾器,默認值都是0,就像下面這樣:

  現在需要添加一個數據:

  我們通過某種計算方式,比如Hash1,計算出了Hash1(數據)=5,我們就把下標為5的格子改成1,就像下面這樣:

  我們又通過某種計算方式,比如Hash2,計算出了Hash2(數據)=9,我們就把下標為9的格子改成1,就像下面這樣:

  還是通過某種計算方式,比如Hash3,計算出了Hash3(數據)=2,我們就把下標為2的格子改成1,就像下面這樣:

  這樣,剛才添加的數據就占據了布隆過濾器“5”,“9”,“2”三個格子。
  可以看出,僅僅從布隆過濾器本身而言,根本沒有存放完整的數據,只是運用一系列隨機映射函數計算出位置,然后填充二進制向量。
  你只需利用上面的三種固定的計算方式,計算出這個數據占據哪些格子,然后看看這些格子里面放置的是否都是1,如果有一個格子不為1,那么就代表這個數字不在其中。這很好理解吧,比如現在又給你了剛才你添加進去的數據,你通過三種固定的計算方式,算出的結果肯定和上面的是一模一樣的,也是占據了布隆過濾器“5”,“9”,“2”三個格子。
  如果這些格子里面放置的都是1,不一定代表給定的數據一定重復,因為隨着增加的值越來越多,被置為 1 的 bit 位也會越來越多,這樣某個值 數據 即使沒有被存儲過,但是萬一哈希函數返回的三個 bit 位都被其他值置為了 1 ,那么程序還是會判斷 數據 這個值存在。
  注意,如果我們增加哈希函數的數量,誤報的概率會趨近於0.但是,插入和查找的時間開銷更大,布隆過濾器的容量也會減小。

5、布隆過濾器的優缺點:

  優點:

    (1)由於存放的不是完整的數據,所以占用的內存很少,而且新增,查詢速度夠快;
  缺點:
    (1)隨着數據的增加,誤判率隨之增加;無法做到刪除數據;只能判斷數據是否一定不存在,而無法判斷數據是否一定存在。
    (2)無法返回元素本身
    (3)無法刪除某個元素
    (4)在不同語言中的實現,有兩個原因,其中之一是選擇好的哈希函數和實現方法能有效改善錯誤率的分布。其次,它需要通過實戰測試,錯誤率和容量大小都要經得起實戰檢驗。

 6、布隆過濾器應用

  (1) 緩存穿透
    我們經常會把一部分數據放在Redis等緩存,比如產品詳情。這樣有查詢請求進來,我們可以根據產品Id直接去緩存中取數據,而不用讀取數據庫,這是提升性能最簡單,最普遍,也是最有效的做法。一般的查詢請求流程是這樣           的:先查緩存,有緩存的話直接返回,如果緩存中沒有,再去數據庫查詢,然后再把數據庫取出來的數據放入緩存,一切看起來很美好。但是如果現在有大量請求進來,而且都在請求一個不存在的產品Id,會發生什么?既然產品Id都         不存在,那么肯定沒有緩存,沒有緩存,那么大量的請求都懟到數據庫,數據庫的壓力一下子就上來了,還有可能直接導致數據庫掛掉。
      雖然有很多辦法都可以解決這問題,但是我們的主角是“布隆過濾器”,沒錯,“布隆過濾器”就可以解決(緩解)緩存穿透問題。至於為什么說是“緩解”,看下去你就明白了。

  (2) 大量數據,判斷給定的是否在其中
    現在有大量的數據,而這些數據的大小已經遠遠超出了服務器的內存,現在再給你一個數據,如何判斷給你的數據在不在其中。如果服務器的內存足夠大,那么用HashMap是一個不錯的解決方案,理論上的時間復雜度可以達到          O(1),但是現在數據的大小已經遠遠超出了服務器的內存,所以無法使用HashMap,這個時候就可以使用“布隆過濾器”來解決這個問題。但是還是同樣的,會有一定的“誤判率”。

  (3) 爬蟲url的去重

7、Python實現布隆過濾器

1、第一種

import mmh3
import redis
import math
import time


class PyBloomFilter():
    #內置100個隨機種子
    SEEDS = [543, 460, 171, 876, 796, 607, 650, 81, 837, 545, 591, 946, 846, 521, 913, 636, 878, 735, 414, 372,
             344, 324, 223, 180, 327, 891, 798, 933, 493, 293, 836, 10, 6, 544, 924, 849, 438, 41, 862, 648, 338,
             465, 562, 693, 979, 52, 763, 103, 387, 374, 349, 94, 384, 680, 574, 480, 307, 580, 71, 535, 300, 53,
             481, 519, 644, 219, 686, 236, 424, 326, 244, 212, 909, 202, 951, 56, 812, 901, 926, 250, 507, 739, 371,
             63, 584, 154, 7, 284, 617, 332, 472, 140, 605, 262, 355, 526, 647, 923, 199, 518]

    #capacity是預先估計要去重的數量
    #error_rate表示錯誤率
    #conn表示redis的連接客戶端
    #key表示在redis中的鍵的名字前綴
    def __init__(self, capacity=1000000000, error_rate=0.00000001, conn=None, key='BloomFilter'):
        self.m = math.ceil(capacity*math.log2(math.e)*math.log2(1/error_rate))      #需要的總bit位數
        self.k = math.ceil(math.log1p(2)*self.m/capacity)                           #需要最少的hash次數
        self.mem = math.ceil(self.m/8/1024/1024)                                    #需要的多少M內存
        self.blocknum = math.ceil(self.mem/512)                                     #需要多少個512M的內存塊,value的第一個字符必須是ascii碼,所有最多有256個內存塊
        self.seeds = self.SEEDS[0:self.k]
        self.key = key
        self.N = 2**31-1
        self.redis = conn
        print(self.m)
        print(self.k)
        print(self.mem)
        print(self.k)

    def add(self, value):
        name = self.key + "_" + str(ord(value[0])%self.blocknum)
        hashs = self.get_hashs(value)
        for hash in hashs:
            self.redis.setbit(name, hash, 1)

    def is_exist(self, value):
        name = self.key + "_" + str(ord(value[0])%self.blocknum)
        hashs = self.get_hashs(value)
        exist = True
        for hash in hashs:
            exist = exist & self.redis.getbit(name, hash)
        return exist

    def get_hashs(self, value):
        hashs = list()
        for seed in self.seeds:
            hash = mmh3.hash(value, seed)
            if hash >= 0:
                hashs.append(hash)
            else:
                hashs.append(self.N - hash)
        return hashs


pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=0)
conn = redis.Redis(connection_pool=pool)

start = time.time()
bf = PyBloomFilter(conn=conn)
bf.add('www.jobbole.com')
bf.add('www.zhihu.com')
print(bf.is_exist('www.zhihu.com'))
print(bf.is_exist('www.lagou.com'))

 第二種

(1)生成hash方法

#
#**************************************************************************
#*                                                                        *
#*          General Purpose Hash Function Algorithms Library              *
#*                                                                        *
#* Author: Arash Partow - 2002                                            *
#* URL: http://www.partow.net                                             *
#* URL: http://www.partow.net/programming/hashfunctions/index.html        *
#*                                                                        *
#* Copyright notice:                                                      *
#* Free use of the General Purpose Hash Function Algorithms Library is    *
#* permitted under the guidelines and in accordance with the MIT License. *
#* http://www.opensource.org/licenses/MIT                                 *
#*                                                                        *
#**************************************************************************
#


def rs_hash(key):
    a = 378551
    b = 63689
    hash_value = 0
    for i in range(len(key)):
        hash_value = hash_value * a + ord(key[i])
        a = a * b
    return hash_value


def js_hash(key):
    hash_value = 1315423911
    for i in range(len(key)):
        hash_value ^= ((hash_value << 5) + ord(key[i]) + (hash_value >> 2))
    return hash_value


def pjw_hash(key):
    bits_in_unsigned_int = 4 * 8
    three_quarters = (bits_in_unsigned_int * 3) / 4
    one_eighth = bits_in_unsigned_int / 8
    high_bits = 0xFFFFFFFF << int(bits_in_unsigned_int - one_eighth)
    hash_value = 0
    test = 0

    for i in range(len(key)):
        hash_value = (hash_value << int(one_eighth)) + ord(key[i])
        test = hash_value & high_bits
    if test != 0:
        hash_value = ((hash_value ^ (test >> int(three_quarters))) & (~high_bits))
    return hash_value & 0x7FFFFFFF


def elf_hash(key):
    hash_value = 0
    for i in range(len(key)):
        hash_value = (hash_value << 4) + ord(key[i])
        x = hash_value & 0xF0000000
        if x != 0:
            hash_value ^= (x >> 24)
        hash_value &= ~x
    return hash_value


def bkdr_hash(key):
    seed = 131  # 31 131 1313 13131 131313 etc..
    hash_value = 0
    for i in range(len(key)):
        hash_value = (hash_value * seed) + ord(key[i])
    return hash_value


def sdbm_hash(key):
    hash_value = 0
    for i in range(len(key)):
        hash_value = ord(key[i]) + (hash_value << 6) + (hash_value << 16) - hash_value;
    return hash_value


def djb_hash(key):
    hash_value = 5381
    for i in range(len(key)):
        hash_value = ((hash_value << 5) + hash_value) + ord(key[i])
    return hash_value


def dek_hash(key):
    hash_value = len(key);
    for i in range(len(key)):
        hash_value = ((hash_value << 5) ^ (hash_value >> 27)) ^ ord(key[i])
    return hash_value


def bp_hash(key):
    hash_value = 0
    for i in range(len(key)):
        hash_value = hash_value << 7 ^ ord(key[i])
    return hash_value


def fnv_hash(key):
    fnv_prime = 0x811C9DC5
    hash_value = 0
    for i in range(len(key)):
        hash_value *= fnv_prime
        hash_value ^= ord(key[i])
    return hash_value


def ap_hash(key):
    hash_value = 0xAAAAAAAA
    for i in range(len(key)):
        if (i & 1) == 0:
            hash_value ^= ((hash_value << 7) ^ ord(key[i]) * (hash_value >> 3))
        else:
            hash_value ^= (~((hash_value << 11) + ord(key[i]) ^ (hash_value >> 5)))
    return hash_value

(2)布隆過濾器

import redis

from GeneralHashFunctions import *


class BloomFilterRedis(object):
    # 哈希哈數列表
    hash_list = [rs_hash, js_hash, pjw_hash, elf_hash, bkdr_hash, sdbm_hash, djb_hash, dek_hash]

    def __init__(self, key, host='127.0.0.1', port=6379, hash_list=hash_list):
        self.key = key
        self.redis = redis.StrictRedis(host=host, port=port, charset='utf-8')
        self.hash_list = hash_list

    def random_generator(self, hash_value):
        '''
        將hash函數得出的函數值映射到[0, 2^32-1]區間內
        '''
        return hash_value % (1 << 32)

    def do_filter(self, item, save=True):
        '''
        過濾,判斷是否存在
        :param item:
        :param value:
        :return:
        '''
        flag = True  # 設置默認存在
        for hash in self.hash_list:
            # 計算哈希值
            hash_value = hash(item)
            # 獲取映射到位數組的下標值
            index_value = self.random_generator(hash_value)
            # 判斷指定位置標記是否為0
            if self.redis.getbit(self.key, index_value) == 0:
                if save:
                    self.redis.setbit(self.key, index_value, 1)
                flag = False

        return flag


if __name__ == '__main__':
    bloom = BloomFilterRedis("bloom_url")
    ret = bloom.do_filter("http://www.baidu2.com")
    print(ret)
    ret = bloom.do_filter("http://www.baidu.com")
    print(ret)
    ret = bloom.do_filter("http://www.baidu3.com1")
    print(ret)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM