Java網絡爬蟲(九)--海量URL去重之布隆過濾器

簡介布隆過濾器

當我們要對海量URL進行抓取的時候,我們常常關心一件事,就是URL的去重問題,對已經抓取過的URL我們不需要在進行重新抓取。在進行URL去重的時候,我們的基本思路是將拿到的URL與已經抓取過的URL隊列進行比對,看當前URL是否在此隊列中,如果在已抓取過的隊列中,則將此URL進行捨棄,如果沒有在,則對此URL進行抓取。看到這,如果有哈希表基礎的同學,很自然的就會想到那麼如果用哈希表對URL進行存儲管理的話,那麼我們對於URL去重直接使用HashSet進行URL存儲不就行了。事實上,在URL非海量的情況下,這的確是一種很不錯的方法,但哈希表的缺點很明顯:費存儲空間。

對於像Gmail那樣公衆電子郵件提供商來說,總是需要過濾掉來自發送垃圾郵件的人和來及郵件的E-mail地址。然而全世界少說也有幾十億個發垃圾郵件的地址,將他們都存儲起來需要大量的網絡服務器。如果用哈希表,每存儲一億個E-mail地址,就需要1.6GB的內存(用哈希表實現的具體實現方式是將每一個E-mail地址對應成一個八字節的信息指紋,然後將這個信息存儲在哈希表中,但是由於哈希表的存儲效率一般只有50%,一旦存儲空間大於表長的50%,查找速度就會明顯的下降(容易發生衝突),即存儲一個E-mail我們需要給它分配十六字節的大小,一億個地址的大小大約就要1.6GB內存)。因此存儲幾十億的地址就要需要大約上百GB的內存,除非是超級計算機,一般服務器是無法存儲的。

關於哈希表的相關知識,請戳這篇博客—查找–理解哈希算法並實現哈希表


具體實現思想

在這種情況下,巴頓·布隆在1970年提出了布隆過濾器,它只需要哈希表的1/8到1/4的大小就可以解決同樣的問題。我們來看一下其工作原理:
首先我們需要一串很長的二進制向量,與其說是二進制向量,我覺得不如說是一串很長的“位空間”,其具體原理大家可以瞭解一下Java中BitSet類的算法思想。它用位空間來存儲我們平常的整數,可以將數據的存儲空間急劇壓縮。然後需要一系列隨機映射函數(哈希函數)來將我們的URL映射成一系列的數,我們將其稱爲一系列的“信息指紋”。

然後我們需要將剛纔產生的一系列信息指紋對應至布隆過濾器中,也就是我們剛纔設置的那一串很長的位空間(二進制向量)中。位空間中各個位的初始值爲0。我們需要將每個信息指紋都與其布隆過濾器中的對應位進行比較,看看其標誌位是否已經被設置過,如果判斷之後發現一系列的信息指紋都已被設置,那麼就將此URL進行過濾(說明此URL可能存在於布隆過濾器中)。事實上,我們將每個URL用隨機映射函數來產生一系列的數之所以能被稱之爲信息之紋,就是因爲這一系列的數基本上是獨一無二的,每個URL都有其獨特的指紋。雖然布隆過濾器還有極小的可能將一個沒有抓取過的URL誤判爲已經抓取過,但它絕對不會對已經抓取過的URL進行重新抓取。然後剛纔的誤判率一般來說我們基本上可以忽略不計,等下我給大家列出一張表格大家直觀感受一下。

對於爲什麼會出現誤判的情況,請參考此篇博客—布隆過濾器(Bloom Filter)的原理和實現


算法總結

現在我們來總結一下該怎麼設計一個布隆過濾器:

  1. 創建一個布隆過濾器,開闢一個足夠的位空間(二進制向量);
  2. 設計一些種子數,用來產生一系列不同的映射函數(哈希函數);
  3. 使用一系列的哈希函數對此URL中的每一個元素(字符)進行計算,產生一系列的隨機數,也就是一系列的信息指紋
  4. 將一系列的信息指紋在布隆過濾器中的相應位,置爲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 = "[email protected]";
        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;
    }
}

代碼的註解已經足夠詳細,如果大家還有什麼疑惑,可以在評論區進行討論交流~~


布隆過濾器誤判率表

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章