网络爬虫学习笔记(四) 数据去重

1 常见去重方式

我抓取百度新闻首页的内容后,抽取到了很多超链接,那么我的需求肯定不是一个网站,我肯定监控着几十个上百个网站抓取新闻,那么肯定会出现如下情况:

a站点收录了一个新闻网页,b站点也收录了这个页面,且url相同。

针对这个情况需要读抓到的链接进行去重,常见的去重方式如下:

1 数据库去重:每次拿url去数据库中验证一次 2 缓存去重:同样的那url去缓存中验证,常见的分布式缓存如redis

大都是将历史数据存储起来进行验证的。那么问题来了,采用上面两种的话我需要安装一些额外的工具,比如redis,我还需要安装一台redis,同样的,我还需要去维护这个redis。

同样的历史数据存储这一块,如果我拿到一个url就直接插入到缓存中,或者是数据库中,那么就会占用大量的存储以及内存资源。简单的方法是简化url,常见的简化方式如下:

1 加密(如MD5) 2 设定规则简化(剔除http://,站点表示_页面表示等)

2 布隆过滤去重

通过上面的方式都可以进行验证,下面介绍另外一种验证方式:布隆过滤器

布隆过滤器:

(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 地址的布隆过滤器就建成了。(见下图)

现 在,让我们看看如何用布隆过滤器来检测一个可疑的电子邮件地址 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;
}

}
}
posted @ 2016-10-13 18:25  知为谁开  阅读(256)  评论(0)    收藏  举报