网络爬虫:URL去重策略之布隆过滤器(BloomFilter)的使用

前言:

  最近被网络爬虫中的去重策略所困扰。使用一些其他的“理想”的去重策略,不过在运行过程中总是会不太听话。不过当我发现了BloomFilter这个东西的时候,的确,这里是我目前找到的最靠谱的一种方法。

  如果,你说URL去重嘛,有什么难的。那么你可以看完下面的一些问题再说这句话。


关于BloomFilter:

  Bloom filter 是由 Howard Bloom 在 1970 年提出的二进制向量数据结构,它具有很好的空间和时间效率,被用来检测一个元素是不是集合中的一个成员。如果检测结果为是,该元素不一定在集合中;但如果检测结果为否,该元素一定不在集合中。因此Bloom filter具有100%的召回率。这样每个检测请求返回有“在集合内(可能错误)”和“不在集合内(绝对不在集合内)”两种情况,可见 Bloom filter 是牺牲了正确率以节省空间。


以前的去重策略:

1.想到过的URL去重策略

  • 在数据库中创建字段的UNIQUE属性
  • 在数据库中创建一个唯一的索引,在插入数据之前检查待插入的数据是否存在
  • 使用Set或HashSet保存数据,确保唯一
  • 使用Map或是一个定长数组记录某一个URL是否被访问过


2.以上去重策略存在的问题

  (1)对于在数据库中创建字段的UNIQUE属性, 的确是可以避免一些重复性操作。不过在多次MySQL报错之后,程序可能会直接崩溃,因此这种方式不可取

  (2)如果我们要在每一次插入数据之前都去检查待插入的数据是否存在,这样势必会影响程序的效率

  (3)这种方式是我在第一次尝试的时候使用的,放弃继续使用的原因是:OOM。当然,这里并不是程序的内存泄露,而程序中真的有这么多内存需要被占用(因为从待访问队列中解析出来的URL要远比它本身要多得多)

  (4)在前几篇博客中,我就有提到使用Map对象来保存URL的访问信息。不过,现在我要否定它。因为,在长时间运行之后,Map也是会占用大量的内存。只不过,会比第3种方式要小一些。下面是使用Map<Integer, Integer>去重,在长时间运行中内存的使用情况:

  网络爬虫:URL去重策略之布隆过滤器(BloomFilter)的使用


BloomFilter的使用:

1.一般情况下BloomFilter使用内存的情况:

  网络爬虫:URL去重策略之布隆过滤器(BloomFilter)的使用


2.爬虫程序中BloomFilter使用内存的情况(已运行4小时):

  网络爬虫:URL去重策略之布隆过滤器(BloomFilter)的使用

3.程序结构图

  网络爬虫:URL去重策略之布隆过滤器(BloomFilter)的使用

 

4.BloomFilter的一般使用

  此处关于BloomFilter的Java代码部分,参考于:http://www.cnblogs.com/heaad/archive/2011/01/02/1924195.html

  如果你看了上面的文章,相信你已经了解到布隆过滤器的空间复杂度是S(n)=O(n)。关于这一点,相信你已经从上面的内存使用情况中了解到了这一点。那么以下会是一些相关的Java代码展示。而在查重过程也很有效率,时间复杂度是T(n)=O(1)。


BloomFilter.java

[java] view plain copy
  1. import java.util.BitSet;  
  2.   
  3. public class BloomFilter {  
  4.       
  5.     /* BitSet初始分配2^24个bit */  
  6.     private static final int DEFAULT_SIZE = 1 << 25;  
  7.       
  8.     /* 不同哈希函数的种子,一般应取质数 */  
  9.     private static final int[] seeds = new int[] { 571113313761 };  
  10.       
  11.     private BitSet bits = new BitSet(DEFAULT_SIZE);  
  12.       
  13.     /* 哈希函数对象 */  
  14.     private SimpleHash[] func = new SimpleHash[seeds.length];  
  15.   
  16.     public BloomFilter() {  
  17.         for (int i = 0; i < seeds.length; i++) {  
  18.             func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);  
  19.         }  
  20.     }  
  21.   
  22.     // 将字符串标记到bits中  
  23.     public void add(String value) {  
  24.         for (SimpleHash f : func) {  
  25.             bits.set(f.hash(value), true);  
  26.         }  
  27.     }  
  28.   
  29.     // 判断字符串是否已经被bits标记  
  30.     public boolean contains(String value) {  
  31.         if (value == null) {  
  32.             return false;  
  33.         }  
  34.           
  35.         boolean ret = true;  
  36.         for (SimpleHash f : func) {  
  37.             ret = ret && bits.get(f.hash(value));  
  38.         }  
  39.           
  40.         return ret;  
  41.     }  
  42.   
  43.     /* 哈希函数类 */  
  44.     public static class SimpleHash {  
  45.         private int cap;  
  46.         private int seed;  
  47.   
  48.         public SimpleHash(int cap, int seed) {  
  49.             this.cap = cap;  
  50.             this.seed = seed;  
  51.         }  
  52.   
  53.         // hash函数,采用简单的加权和hash  
  54.         public int hash(String value) {  
  55.             int result = 0;  
  56.             int len = value.length();  
  57.             for (int i = 0; i < len; i++) {  
  58.                 result = seed * result + value.charAt(i);  
  59.             }  
  60.             return (cap - 1) & result;  
  61.         }  
  62.     }  
  63. }  
     
Test.java
[java] view plain copy
  1. public class Test {  
  2.   
  3.     private final String[] URLS = {  
  4.             "http://www.csdn.net/",  
  5.             "http://www.baidu.com/",  
  6.             "http://www.google.com.hk",  
  7.             "http://www.cnblogs.com/",  
  8.             "http://www.zhihu.com/",  
  9.             "https://www.shiyanlou.com/",  
  10.             "http://www.google.com.hk",  
  11.             "https://www.shiyanlou.com/",  
  12.             "http://www.csdn.net/"  
  13.     };  
  14.       
  15.     private void testBloomFilter() {  
  16.         BloomFilter filter = new BloomFilter();  
  17.         for (int i = 0; i < URLS.length; i++) {  
  18.             if (filter.contains(URLS[i])) {  
  19.                 System.out.println("contain: " + URLS[i]);  
  20.                 continue;  
  21.             }  
  22.               
  23.             filter.add(URLS[i]);  
  24.         }  
  25.     }  
  26.   
  27.     public static void main(String[] args) {  
  28.         Test t = new Test();  
  29.         t.testBloomFilter();  
  30.     }  
  31. }  

                    

5.BloomFilter在爬虫中过滤重复的URL

[java] view plain copy
  1. public class ParserRunner implements Runnable {  
  2.   
  3.     private SpiderSet mResultSet = null;  
  4.     private WebInfoModel mInfoModel = null;  
  5.     private int mIndex;  
  6.     private final boolean DEBUG = false;  
  7.       
  8.     private SpiderBloomFilter mFlagBloomFilter = null;  
  9.       
  10.     public ParserRunner(SpiderSet set, WebInfoModel model, int index, SpiderBloomFilter filter) {  
  11.         mResultSet = set;  
  12.         mInfoModel = model;  
  13.         mIndex = index;  
  14.         mFlagBloomFilter = filter;  
  15.     }  
  16.       
  17.       
  18.     @Override  
  19.     public void run() {  
  20.         long t = System.currentTimeMillis();  
  21.   
  22.         SpiderQueue tmpQueue = new SpiderQueue();  
  23.         PythonUtils.fillAddressQueueByPython(tmpQueue, mInfoModel.getAddress(), mInfoModel.getLevel());  
  24.           
  25.         WebInfoModel model = null;  
  26.         while (!tmpQueue.isQueueEmpty()) {  
  27.             model = tmpQueue.poll();  
  28.             if (model == null || mFlagBloomFilter.contains(model.getAddress())) {  
  29.                 continue;  
  30.             }  
  31.               
  32.             mResultSet.add(model);  
  33.             mFlagBloomFilter.add(model.getAddress());  
  34.         }  
  35.           
  36.         tmpQueue = null;  
  37.         model = null;  
  38.           
  39.         System.err.println("Thread-" + mIndex + ", UsedTime-" + (System.currentTimeMillis() - t) + ", SetSize = " + mResultSet.size());  
  40.         t = 0;  
  41.     }  
  42.   
  43.     @SuppressWarnings("unused")  
  44.     private void sleep(long millis) {  
  45.         try {  
  46.             Thread.sleep(millis);  
  47.         } catch (InterruptedException e) {  
  48.             e.printStackTrace();  
  49.         }  
  50.     }  
  51. }  
  如果你看过我之前的博客,那么上面的这一段代码相信你会比较熟悉。

  这段代码的功能是:生产者。从待访问队列中消费一个model,然后调用Python生产链接的列表Queue,并将生成的列表Queue offer到结果SpiderSet中。