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

簡介布隆過濾器

當咱們要對海量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

  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 = "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


布隆過濾器誤判率表

這裏寫圖片描述

相關文章
相關標籤/搜索