1 常见去重方式
我抓取百度新闻首页的内容后,抽取到了很多超链接,那么我的需求肯定不是一个网站,我肯定监控着几十个上百个网站抓取新闻,那么肯定会出现如下情况:
a站点收录了一个新闻网页,b站点也收录了这个页面,且url相同。
针对这个情况需要读抓到的链接进行去重,常见的去重方式如下:
1 数据库去重:每次拿url去数据库中验证一次 2 缓存去重:同样的那url去缓存中验证,常见的分布式缓存如redis
大都是将历史数据存储起来进行验证的。那么问题来了,采用上面两种的话我需要安装一些额外的工具,比如redis,我还需要安装一台redis,同样的,我还需要去维护这个redis。
同样的历史数据存储这一块,如果我拿到一个url就直接插入到缓存中,或者是数据库中,那么就会占用大量的存储以及内存资源。简单的方法是简化url,常见的简化方式如下:
1 加密(如MD5) 2 设定规则简化(剔除http://,站点表示_页面表示等)
2 布隆过滤去重
通过上面的方式都可以进行验证,下面介绍另外一种验证方式:布隆过滤器
布隆过滤器:
![网络爬虫学习笔记(四) <wbr>数据去重]()
(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的。它实际上是由一个很长的二进制向量和一系列随机映射函数组成,布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率(假正例False positives,即Bloom Filter报告某一元素存在于某集合中,但是实际上该元素并不在集合中)和删除困难,但是没有识别错误的情形(即假反例False negatives,如果某个元素确实没有在该集合中,那么Bloom Filter 是不会报告该元素存在于集合中的,所以不会漏报)。
假 定我们存储一亿个电子邮件地址,我们先建立一个十六亿二进制(比特),即两亿字节的向量,然后将这十六亿个二进制全部设置为零。对于每一个电子邮件地址 X,我们用八个不同的随机数产生器(F1,F2, …,F8) 产生八个信息指纹(f1, f2, …, f8)。再用一个随机数产生器 G 把这八个信息指纹映射到 1 到十六亿中的八个自然数 g1, g2, …,g8。现在我们把这八个位置的二进制全部设置为一。当我们对这一亿个 email 地址都进行这样的处理后。一个针对这些 email 地址的布隆过滤器就建成了。(见下图)
![网络爬虫学习笔记(四) <wbr>数据去重]()
现 在,让我们看看如何用布隆过滤器来检测一个可疑的电子邮件地址 Y 是否在黑名单中。我们用相同的八个随机数产生器(F1, F2, …, F8)对这个地址产生八个信息指纹 s1,s2,…,s8,然后将这八个指纹对应到布隆过滤器的八个二进制位,分别是 t1,t2,…,t8。如果 Y 在黑名单中,显然,t1,t2,..,t8 对应的八个二进制一定是一。这样在遇到任何在黑名单中的电子邮件地址,我们都能准确地发现。
3 布隆过滤器的代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
|
public class BloomFilter { private static BloomFilter filter = new BloomFilter();
// 构造函数 private BloomFilter() { // 初始化布隆过滤器 init(); }
public static BloomFilter getInstance(){ return filter; }
// DEFAULT_SIZE为2的28次方 private final int DEFAULT_SIZE = 2 << 28;
private final int[] seeds = new int[] { 5, 7, 11, 13, 31, 37, 61 };
// BitSet实际是由“二进制位”构成的一个Vector。假如希望高效率地保存大量“开-关”信息,就应使用BitSet. // BitSet的最小长度是一个长整数(Long)的长度:64位 private BitSet bits = new BitSet(DEFAULT_SIZE);
private SimpleHash[] func = new SimpleHash[seeds.length];
// 将字符串标记到bits中,即设置字符串的8个hash值函数为1 public synchronized void insert(String value) { for (SimpleHash f : func) { bits.set(f.hash(value), true); }
}
// 判断字符串是否已经被bits标记 public synchronized boolean contains(String value) { // 确保传入的不是空值 if (value == null) { return false; }
// 计算7种hash算法下各自对应的hash值,并判断 for (SimpleHash f : func) { if (!bits.get(f.hash(value))) return false; } return true; }
public void init() {
for (int i = 0; i < seeds.length; i++) { // 给出所有的hash值,共计seeds.length个hash值。共8位。 // 通过调用SimpleHash.hash(),可以得到根据7种hash函数计算得出的hash值。 // 传入DEFAULT_SIZE(最终字符串的长度),seeds[i](一个指定的质数)即可得到需要的那个hash值的位置。 func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]); }
InputStream is = null; BufferedReader br = null; try { is = new FileInputStream(Constant.BLOOM_FILTER_FILE); br = new BufferedReader(new InputStreamReader(is, Constant.CHARSET_UTF8)); String line = null; while ((line = br.readLine()) != null) { if (!"".equals(line)) { insert(line.trim()); } } } catch (FileNotFoundException e) { // 布隆配置文件未加载到 // 需要记录日志 e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally{ Common.closeStream(is, null, br, null); }
}
public static class SimpleHash { // cap为DEFAULT_SIZE的值,即用于结果的最大的字符串长度。 // seed为计算hash值的一个给定key,具体对应上面定义的seeds数组 private int cap; private int 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++) { result = seed * result + value.charAt(i); } return (cap - 1) & result; }
} }
|