簡介布隆過濾器
當咱們要對海量URL進行抓取的時候,咱們經常關心一件事,就是URL的去重問題,對已經抓取過的URL咱們不須要在進行從新抓取。在進行URL去重的時候,咱們的基本思路是將拿到的URL與已經抓取過的URL隊列進行比對,看當前URL是否在此隊列中,若是在已抓取過的隊列中,則將此URL進行捨棄,若是沒有在,則對此URL進行抓取。看到這,若是有哈希表基礎的同窗,很天然的就會想到那麼若是用哈希表對URL進行存儲管理的話,那麼咱們對於URL去重直接使用HashSet進行URL存儲不就好了。事實上,在URL非海量的狀況下,這的確是一種很不錯的方法,但哈希表的缺點很明顯:費存儲空間。html
對於像Gmail那樣公衆電子郵件提供商來講,老是須要過濾掉來自發送垃圾郵件的人和來及郵件的E-mail地址。然而全世界少說也有幾十億個發垃圾郵件的地址,將他們都存儲起來須要大量的網絡服務器。若是用哈希表,每存儲一億個E-mail地址,就須要1.6GB的內存(用哈希表實現的具體實現方式是將每個E-mail地址對應成一個八字節的信息指紋,而後將這個信息存儲在哈希表中,可是因爲哈希表的存儲效率通常只有50%,一旦存儲空間大於表長的50%,查找速度就會明顯的降低(容易發生衝突),即存儲一個E-mail咱們須要給它分配十六字節的大小,一億個地址的大小大約就要1.6GB內存)。所以存儲幾十億的地址就要須要大約上百GB的內存,除非是超級計算機,通常服務器是沒法存儲的。java
關於哈希表的相關知識,請戳這篇博客—查找–理解哈希算法並實現哈希表算法
具體實現思想
在這種狀況下,巴頓·布隆在1970年提出了布隆過濾器,它只須要哈希表的1/8到1/4的大小就能夠解決一樣的問題。咱們來看一下其工做原理:
首先咱們須要一串很長的二進制向量,與其說是二進制向量,我以爲不如說是一串很長的「位空間」,其具體原理你們能夠了解一下Java中BitSet類的算法思想。它用位空間來存儲咱們日常的整數,能夠將數據的存儲空間急劇壓縮。而後須要一系列隨機映射函數(哈希函數)來將咱們的URL映射成一系列的數,咱們將其稱爲一系列的「信息指紋」。
服務器
而後咱們須要將剛纔產生的一系列信息指紋對應至布隆過濾器中,也就是咱們剛纔設置的那一串很長的位空間(二進制向量)中。位空間中各個位的初始值爲0。咱們須要將每一個信息指紋都與其布隆過濾器中的對應位進行比較,看看其標誌位是否已經被設置過,若是判斷以後發現一系列的信息指紋都已被設置,那麼就將此URL進行過濾(說明此URL可能存在於布隆過濾器中)。事實上,咱們將每一個URL用隨機映射函數來產生一系列的數之因此能被稱之爲信息之紋,就是由於這一系列的數基本上是獨一無二的,每一個URL都有其獨特的指紋。雖然布隆過濾器還有極小的可能將一個沒有抓取過的URL誤判爲已經抓取過,但它絕對不會對已經抓取過的URL進行從新抓取。而後剛纔的誤判率通常來講咱們基本上能夠忽略不計,等下我給你們列出一張表格你們直觀感覺一下。網絡
對於爲何會出現誤判的狀況,請參考此篇博客—布隆過濾器(Bloom Filter)的原理和實現函數
算法總結
如今咱們來總結一下該怎麼設計一個布隆過濾器:this
- 建立一個布隆過濾器,開闢一個足夠的位空間(二進制向量);
- 設計一些種子數,用來產生一系列不一樣的映射函數(哈希函數);
- 使用一系列的哈希函數對此URL中的每個元素(字符)進行計算,產生一系列的隨機數,也就是一系列的信息指紋;
- 將一系列的信息指紋在布隆過濾器中的相應位,置爲1。
代碼實現(Java)
import static java.lang.System.out; public class SimpleBloomFilter { // 設置布隆過濾器的大小 private static final int DEFAULT_SIZE = 2 << 24; // 產生隨機數的種子,可產生6個不一樣的隨機數產生器 private static final int[] seeds = new int[] { 7, 11, 13, 31, 37, 61}; // Java中的按位存儲的思想,其算法的具體實現(布隆過濾器) private BitSet bits = new BitSet(DEFAULT_SIZE); // 根據隨機數的種子,建立6個哈希函數 private SimpleHash[] func = new SimpleHash[seeds.length]; // 設置布隆過濾器所對應k(6)個哈希函數 public SimpleBloomFilter() { for (int i = 0; i < seeds.length; i++) { func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]); } } public static void main(String[] args) { String value = "stone2083@yahoo.cn"; SimpleBloomFilter filter = new SimpleBloomFilter(); out.println(filter.contains(value)); } public static class SimpleHash { private int cap; private int seed; // 默認構造器,哈希表長默認爲DEFAULT_SIZE大小,此哈希函數的種子爲seed public SimpleHash(int cap, int seed) { this.cap = cap; this.seed = seed; } public int hash(String value) { int result = 0; int len = value.length(); for (int i = 0; i < len; i++) { // 將此URL用哈希函數產生一個值(使用到了集合中的每個元素) result = seed * result + value.charAt(i); } // 產生單個信息指紋 return (cap - 1) & result; } } // 是否已經包含該URL public boolean contains(String value) { if (value == null) { return false; } boolean ret = true; // 根據此URL獲得在布隆過濾器中的對應位,並判斷其標誌位(6個不一樣的哈希函數產生6種不一樣的映射) for (SimpleHash f : func) { ret = ret && bits.get(f.hash(value)); } return ret; } }
代碼的註解已經足夠詳細,若是你們還有什麼疑惑,能夠在評論區進行討論交流~~spa