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


布隆过滤器误判率表

这里写图片描述

相关文章
相关标签/搜索