笔记-爬虫-去重/bloomfilter

1.      去重

为什么要去重?

  1. 页面重复:爬的多了,总会有重复的页面,对已爬过的页面肯定不愿意再爬一次。
  2. 页面更新:很多页面是会更新的,爬取这种页面时就需要进行判断,是否有更新。

在爬虫中新页面或页面更新称为增量,爬取就叫增量爬取了。

识别增量,有以下几种可能的方法:

  1. url识别:适合旧页面不会改变,只会有新页面出现的网站;
  2. 解析后内容识别:适合页面内容会更新的网站;
  3. 写入前与已存储部分进行匹配:最后一道防线。

目前主要的方法是url过滤,大体上是哈希后匹配。至于说哈希什么内容(一般是url),怎么匹配(set,内存数据库)则需要视情况而定。

2.      增量识别

识别增量网页的常用方法:

1.url:常规;

2.304页面http状态码

当第二次请求页面访问的时候,该页面如果未更新,则会反馈一个304代码,而搜索引擎也会利用这个304http状态码来进行判断页面是否更新。

首先第一次肯定是要爬取网页的,假设是A.html,这个网页存储在磁盘上,相应地有个修改时间(也即是更新这个文件的时间)。

那么第二次爬取的时候,如果发现这个网页本地已经有了,例如A.html,这个时候,你只需要向服务器发送一个If-Modified-Since的请求,把A.html的修改时间带上去。

如果这段时间内,A.html更新了,也就是A.html过期了,服务器就会HTTP状态码200,并且把新的文件发送过来,这时候只要更新A.html即可。         如果这段时间内,A.html的内容没有变,服务器就会返返回HTTP状态码304(不返回文件内容),这个时候就不需要更新文件。

以电影网站6v为例,可以抓包看到:

3.Last-Modified文件最后修改时间

这是http头部信息中的一个属性,主要是记录页面最后一次的修改时间,往往我们会发现,一些权重很高的网站,及时页面内容不更新,但是快照却还是能够每日更新,这其中就有Last-Modified的作用。通产情况下,下载网页我们使用HTTP协议,向服务器发送HEAD请求,可以得到页面的最后修改时间LastModifed,或者标签ETag。将这两个变量和上次下载记录的值的比较就可以知道一个网页是否跟新。这个策略对于静态网页是有效的。是对于绝大多数动态网页如ASP,JSP来说,LastModifed就是服务器发送Response的时间,并非网页的最后跟新时间,而Etag通常为空值。所以对于动态网页使用LastModifed和Etag来判断是不合适的,因此Last-Modified只是蜘蛛判断页面是否更新的一个参考值,而不是条件。

4.比对文件大小

搜索引擎还会取出之前页面文件,和现在的文件进行对比,不过因为大部分网站都是一种替换式更新,往往比对文件大小很难说明问题,因此常见与页面链接变化配合使用。

5.MD5数字签名

每次下载网页时,把服务器返回的数据流ResponseStream先放在内存缓冲区,然后对ResponseStream生成MD5数字签名S1,下次下载同样生成签名S2,比较S2和S1,如果相同,则页面没有跟新,否则网页就有跟新。需要说明的是用md5算法对文本刘签名的速度是极快的,M级的数据可以在毫秒内完成。这种策略虽然也把页面数据从服务器传输到了本地机,但是省掉了页面的I/O操作,对系统性能的提升是很有帮助的。

实际中使用最多的应该还是url去重。

3.      判重-bloomfilter

数量大到一定程度后数量本身就会带来很多问题,万以内用set,数万个得用内存数据库了,更多的时候就得考虑bloomfilter。

BloomFilter:布朗过滤器

3.1.    原理

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

3.1.1.   基本原理:

判断一个元素是不是在一个集合里,一般思路是将所有元素保存起来,然后通过比较确定。链表,树等等数据结构都是这种思路. 但是随着集合中元素的增加,所需要的存储空间越来越大,检索速度也越来越慢。

合适的Hash可以将一个元素映射成位阵列(Bit Array)中的一个点。这样,只要看看这个点是不是 1 就知道可以集合中有没有它了。这就是布隆过滤器的基本思想。

Hash面临的问题是冲突。假设 Hash 函数是良好的,如果我们的位阵列长度为 m 个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳 m/100 个元素。显然这就不叫空间有效了(Space-efficient)。解决方法就是使用多个 Hash,如果它们有一个说元素不在集合中,那肯定就不在。如果它们都说在,虽然也有一定可能性它们在说谎,不过直觉上判断这种事情的概率是比较低的。

3.1.2.   优缺点

优点

相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数。另外, Hash 函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。

布隆过滤器可以表示全集,其它任何数据结构都不能;

k 和 m 相同,使用同一组 Hash 函数的两个布隆过滤器的交并差运算可以使用位操作进行。

缺点

布隆过滤器的缺点和优点一样明显。误算率(False Positive)是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。

另外,一般情况下不能从布隆过滤器中删除元素. 虽然很容易把位列阵变成整数数组(CounterBloom Filter),每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。

3.1.3.    算法描述

一个empty bloom filter是一个有m bits的bit array,每一个bit位都初始化为0。并且定义有k个不同的hash function,每个都以uniform random distribution将元素hash到m个不同位置中的一个。在下面的介绍中n为元素数,m为布隆过滤器或哈希表的slot数,k为布隆过滤器hash function数。

为了add一个元素,用k个hash function将它hash得到bloom filter中k个bit位,将这k个bit位置1。

为了query一个元素,即判断它是否在集合中,用k个hash function将它hash得到k个bit位。若这k bits全为1,则此元素在集合中;若其中任一位不为1,则此元素比不在集合中(因为如果在,则在add时已经把对应的k个bits位置为1)。

不允许remove元素,因为那样的话会把相应的k个bits位置为0,而其中很有可能有其他元素对应的位。因此remove会引入false negative,这是绝对不被允许的。

当k很大时,设计k个独立的hash function是不现实并且困难的。对于一个输出范围很大的hash function(例如MD5产生的128 bits数),如果不同bit位的相关性很小,则可把此输出分割为k份。或者可将k个不同的初始值(例如0,1,2, … ,k-1)结合元素,feed给一个hash function从而产生k个不同的数。

当add的元素过多时,即n/m过大时(n是元素数,m是bloom filter的bits数),会导致false positive过高,此时就需要重新组建filter,但这种情况相对少见。

3.1.4.   bloomfilter算法相关数学描述

Bloom Filter的hash函数选择会影响算法的效果,这句话的意思是需要根据数组m大小和元素个数n选定合适的hash函数数量k。

当hash函数个数k=(ln2)*(m/n)时错误率最小。在错误率不大于E的情况 下,m至少要等于n*lg(1/E) 才能表示任意n个元素的集合。但m还应该更大些,因为还要保证bit数组里至少一半为0,则m应 该>=nlg(1/E)*lge ,大概就是nlg(1/E)1.44倍(lg表示以2为底的对数)。

举个例子我们假设错误率为0.01,则此时m应大概是n的13倍。这样k大概是8个。

一个Bloom Filter有以下参数:

m

bit数组的宽度(bit数)

n

加入其中的key的数量

k

使用的hash函数的个数

f

False Positive的比率

Bloom Filter的f满足下列公式:

在给定m和n时,能够使f最小化的k值为:

此时给出的f为:

根据以上公式,对于任意给定的f,我们有:

n = m ln(0.6185) / ln(f)    [1]

同时,我们需要k个hash来达成这个目标:

k = - ln(f) / ln(2)             [2]

由于k必须取整数,我们在Bloom Filter的程序实现中,还应该使用上面的公式来求得实际的f:

f = (1 – e-kn/m)k             [3]

以上3个公式是程序实现Bloom Filter的关键公式。

4.      scrapy-redis+bloomfilter

一般需要用到bloomfilter的爬虫只能是分布式的,所以是scrapy-redis+bloomfilter。

# 设定bitmap大小、hash函数数量

from .defaults import BLOOMFILTER_BIT, BLOOMFILTER_HASH_NUMBER

class HashMap(object):

def __init__(self, m, seed):

self.m = m

self.seed = seed

def hash(self, value):

"""

Hash Algorithm

:param value: Value

:return: Hash Value

"""

ret = 0

for i in range(len(value)):

ret += self.seed * ret + ord(value[i])

return (self.m - 1) & ret

class BloomFilter(object):

def __init__(self, server, key, bit=BLOOMFILTER_BIT, hash_number=BLOOMFILTER_HASH_NUMBER):

"""

Initialize BloomFilter

:param server: Redis Server

:param key: BloomFilter Key

:param bit: m = 2 ^ bit

:param hash_number: the number of hash function

"""

# default to 1 << 30 = 10,7374,1824 = 2^30 = 128MB, max filter 2^30/hash_number = 1,7895,6970 fingerprints

self.m = 1 << bit

self.seeds = range(hash_number)

self.server = server

self.key = key

self.maps = [HashMap(self.m, seed) for seed in self.seeds]

def exists(self, value):

"""

if value exists

:param value:

:return:

"""

if not value:

return False

exist = True

for map in self.maps:

offset = map.hash(value)

exist = exist & self.server.getbit(self.key, offset)

return exist

def insert(self, value):

"""

add value to bloom

:param value:

:return:

"""

for f in self.maps:

offset = f.hash(value)

self.server.setbit(self.key, offset, 1)

上面的案例中使用了常用的一种hash构造方法,通过seed不同生成不同的值。

关键点在于hash算法的选择,m,k大小的选择。

当然后面还是有些工作要做的,需要修改去重类的指向,或者在settings中设置去重类。

5.      参考文档

bloomfilter案例:

https://github.com/jaybaird/python-bloomfilter

scrapy结合bloomfilter:

https://github.com/LiuXingMing/Scrapy_Redis_Bloomfilter

最新文章

  1. android 选择图片或拍照时旋转了90度问题
  2. js数组操作总结
  3. ZOJ-1239 Hanoi Tower Troubles Again!
  4. FJOI省队集训 chessboard
  5. 用户信息 Froms验证票证
  6. require和include的区别
  7. /proc/sysrq-trigger的功能 介绍
  8. 运用Real Spy Monitor监控网络
  9. 基于url拦截实现权限控制
  10. 在Ubuntu 12.10 上安装部署Openstack
  11. ThinkPHP常量参考
  12. input[type=file]样式更改以及图片上传预览
  13. mysql进阶(十四) 批量更新与批量更新多条记录的不同值实现方法
  14. haproxy代理配置段参数设定
  15. springcloud第四步:ribbon搭建服务负载均衡
  16. Record &amp;&amp; Limit
  17. linux常见系统调用函数列表
  18. 转: 日期格式参考extjs api文档中的Date类型
  19. java的final关键字——修饰变量
  20. Submission Details [leetcode] 算法的改进

热门文章

  1. python3基础14(有关日期的使用2)
  2. 01、Scala介绍与安装
  3. JavaScript 面向对象编程(三):非构造函数对象的继承
  4. 【JavaScript 封装库】BETA 4.0 测试版发布!
  5. 二进制安装mysql5.6
  6. ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed /usr/bin/ssh-copy-id
  7. 【CCPC-Wannafly Winter Camp Day3 (Div1) D】精简改良(状压DP)
  8. 2017.10.10 java中的继承与多态(重载与重写的区别)
  9. Yarn下Map数控制
  10. 正定矩阵(Positive-definite Matrix)