億級數據去重之布隆過濾器

標題很唬人吧,標題黨就是本喵了哈哈哈哈哈哈哈。好了迴歸正題,在咱們進行一些爬蟲爬取數據的時候,若是保證去重呢,今天和你們聊一聊使用布隆過濾器去重。html

首先什麼是布隆過濾器呢,讓咱們依舊來看看百度百科。python

  • 布隆過濾器(Bloom Filter)是1970年由布隆提出的。它其實是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器能夠用於檢索一個元素是否在一個集合中。它的優勢是空間效率和查詢時間都比通常的算法要好的多,缺點是有必定的誤識別率和刪除困難。

看傻了吧,其實我剛開始也給直接幹懵了。實際上上邊所說的二進制向量就是一個位數組(好比redis中就提供了一個2^32長度的位數組,大小是512MB),而一系列隨機映射函數實際上就是多個哈希函數。那麼先和你們來扯扯哈希吧redis

hash函數

哈希函數通常也叫散列函數,就是能夠把任意長度的輸入,經過散列算法,變成固定長度數輸出,輸出的結果就是散列值。常見的哈希函數有md五、sha一、sha256等,也能夠將哈希函數理解成一種加密手段,可是哈希函數是不可逆的,就是你不能夠經過輸出域獲得散列值逆向推出輸入域的值。(由於它裏邊使用很厲害的科學計算致使它不可逆。)而後說說哈希的幾個特性吧算法

  • 輸入域無限,輸出域有限(就是你輸入域放一句helloword和放一個10G的小電影,輸出域都輸出的是一段固定長度的輸出。輸入域哪怕只有一個字符的不一樣,獲得的散列值都是千差萬別毫無規律的)
  • 相同的輸入必定獲得相同的輸出(這個就沒得說了吧,你用同一個哈希函數放兩次一樣的10G小電影獲得的散列值是同樣的)
  • 不一樣的輸入也可能獲得相同的輸出結果,這種現象被稱爲哈希碰撞(由於輸入域無限輸出域有限,必然會致使有可能你輸入的兩個不一樣的值,輸出域卻獲得了相同的散列值)
  • 輸出域的每一個結果在整個輸出域中是均勻分佈的,也成爲哈希函數的離散性(一個哈希函數的好通常就體如今它的離散性上,輸出結果分佈的越平均,離散性就越好)

哈希函數的用途很是很是多,舉個栗子數組

看這個,當你下載完英雄聯盟的時候,如何肯定你完整的下載成功了?dom

這個時候官方會把這個文件丟到哈希函數(md5)中加密,獲得如上圖所示的散列值,而後當你下載完成的時候,把你下載好的文件用一樣的哈希函數加密,若是獲得的值和官方所提供的值同樣的話,這時候你絕對是完整的下載了這個文件。這種文件校驗使用哈希函數比較常見。ide

因此我這樣說大家應該聽得懂吧(小聲bb....)函數

嗯!既然你們都懂了那麼接下來進入主題說說布隆過濾器 (來自本喵不要臉的自問自答哈哈哈哈哈哈~)大數據

布隆過濾器

首先來講說布隆過濾器的流程ui

  • 首先將數據經過k個哈希函數進行哈希獲得k個散列值(h1,h2,...hk)
  • 肯定一個位數組的長度假設爲m,將這k個散列值分別對m取模運算,獲得k個值(v1,v2....vk)確保每一個值在位數組長度範圍以內[0,m-1]
  • 若是須要保存,在維數組中,將k個由上一步取模獲得的值做爲下標值,找到這個k個下標所對應的值改成1便可
  • 若是須要查詢這個數據是否存在,看這k個下標所對應的值是否所有爲1,若是所有爲1表示存在,不然不存在

我猜你看完以後確定有一些懵逼,這個時候你把位數組想象成一張白紙

而後假設當前尚未添加過數據,你經過了4個哈希函數而且取模獲得了4個值(這4個值是必定在這個 位數組長度範圍以內的,由於進行了取模操做),這4個值就在這張白紙的某些位置,把這4個位置塗黑

因此當你添加了好多數據的時候,這張白紙會有好多的小黑點(我就不畫了,畫的醜,還累。。。。)

如何判斷重複呢,當你經過多個哈希計算並取模得出來的值所對應的位置在這張紙上都是黑的,這時候就認爲這條數據是重複的。只要有一個位置爲白,那麼就不重複,繼續添加,把位置爲白的塗黑,已是黑色的位置就不用管了。

因此判斷數據重複只須要看看位數組中對應的下標位置是否都爲1,都爲1,就重複,這條數據就能夠不要了。

我估摸着大家 都看明白了吧......emmmm無論大家明不明白,我接着說!

當咱們採用布隆過濾器進行去重的時候,是必定存在失誤率的(由於哈希碰撞的特性),可是可以保證重複的數據必定能判斷出來!(這個意思就是會有很小的機率當你數據沒有重複的時候,我就說你重複了!就不讓你添加數據!義正詞嚴!!!)

而且布隆過濾器還有個特色,就是它只關注數據量的多少,不考慮單個數據樣本的大小(這個由哈希的性質1,輸入域無限,輸出域有限決定的),因此布隆過濾器對於大數據大樣本的海量數據去重特別有效。

這個時候就會發現這個失誤率和位數組的長度m、和哈希函數的個數k息息相關。這個時候如何設計布隆過濾器呢?不用擔憂!已經有科學家幫咱們研究出了公式,咱們只須要根據本身樣本數量和預計失誤率計算出你須要的東西啦!就是這麼得勁!

經過樣本數據個數n,和失誤率p,能夠計算出m(位數組的長度)

根據m、n,計算哈希函數的個數k

經過m、k、n的值,計算出實際真實的失誤率

所獲得的數組長度和函數個數都向上取整,好比計算出來的k是8.23個 ,那麼就直接取9個哈希函數,這樣失誤率也會比預期的有所降低

因此說,布隆過濾器能夠經過很小的代價來進行大量數據的去重,大概就這樣!

扯蛋扯了這麼多,也該上代碼了哈!

代碼(Python)

位數組,我這裏就直接使用redis中提供的bit了哈,首先須要多個不一樣的哈希函數(這個是在麻省理工上找的哈)

#
#**************************************************************************
#* *
#* 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
複製代碼

接下來纔是bloom的實現了

from BloomFilter.GeneralHashFunctions import *
import redis
import pickle
import base64


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):
        self.key = key
        self.redis = redis.StrictRedis(host=host, port=port, charset='utf-8')

    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: :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


class Stu(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age


if __name__ == '__main__':
    bloom = BloomFilterRedis("bloom_obj")
	# 對對象進行去重,必須實現序列化
    data = pickle.dumps(Stu("xiaohong", 18))
    data1 = base64.b64encode(data).decode()
    ret = bloom.do_filter(data1)
    print(ret)

    data = pickle.dumps(Stu("xiaohong", 18))
    data1 = base64.b64encode(data).decode()
    ret = bloom.do_filter(data1)
    print(ret)

    bloom = BloomFilterRedis("bloom_url")
    ret = bloom.do_filter("http://www.baidu.com")
    print(ret)
    ret = bloom.do_filter("http://www.baidu.com")
    print(ret)
複製代碼

好了 ,代碼也擼上來了,歡迎各位小夥伴一塊兒提意見哈哈哈哈哈哈,那麼今天就到這裏,我先撤!!!

相關文章
相關標籤/搜索