笔记-爬虫-去重/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

笔记-爬虫-去重/bloomfilter的更多相关文章

  1. 笔记-爬虫-scrapy-srcapy-redis组件

    笔记-爬虫-scrapy-srcapy-redis组件 1.      简介 scrapy是一个爬虫框架,但不支持分布式,scrapy-redis是为了更方便的实现scrapy分布式爬虫的组件. 可以 ...

  2. Scrapy爬虫学习笔记 - 爬虫基础知识

    一.正则表达式 二.深度和广度优先                                三.爬虫去重策略

  3. 笔记-爬虫-selenium常用方法

    笔记-爬虫-selenium常用方法 1.      查找元素 常用的查找方法 find_element_by_name find_element_by_xpath find_element_by_l ...

  4. 笔记-爬虫-js代码解析

    笔记-爬虫-js代码解析 1.      js代码解析 1.1.    前言 在爬取网站时经常会有js生成关键信息,而且js代码是混淆过的. 以瓜子二手车为例,直接请求https://www.guaz ...

  5. 笔记-爬虫-XPATH

    笔记-爬虫-XPATH 1.      xpath XPath是W3C的一个标准.它最主要的目的是为了在XML1.0或XML1.1文档节点树中定位节点所设计.目前有XPath1.0和XPath2.0两 ...

  6. 笔记-爬虫-robots.txt

    笔记-爬虫-robots.txt 1.      robots.txt文件简介 1.1.    是什么 robots.txt是用来告诉搜索引擎网站上哪些内容可以被访问.哪些不能被访问.当搜索引擎访问一 ...

  7. 笔记-爬虫部署及运行工具-scrapydweb

    笔记-爬虫部署及运行工具-scrapydweb 1.      简介 scrapyd是爬虫部署工具,但它的ui比较简单,使用不是很方便. scrapydweb以scrapyd为基础,增加了ui界面和监 ...

  8. scrapy-redis的搭建 分布式爬虫 去重

    master:一.spider文件1.需要更改继承的类from scrapy_redis.spiders import RedisSpider 2.注释掉start_urls 3.在爬虫目录下新创建一 ...

  9. python学习笔记——爬虫学习中的重要库urllib

    1 urllib概述 1.1 urllib库中的模块类型 urllib是python内置的http请求库 其提供了如下功能: (1)error 异常处理模块 (2)parse url解析模块 (3)r ...

随机推荐

  1. angular2-搭建环境

    npm  模块将被下载安装到[全局目录]中.[全局目录]通过 npm config set prefix "目录路径" 来设置.通过 npm config get prefix 来 ...

  2. 安全漏洞 : XSS CSRF

    https://my.oschina.net/hc24/blog/527099 XSS成因概括 : XSS其实就是Html的注入问题,攻击者A的输入没有经过严格的控制进入了数据库,最终显示给来访的用户 ...

  3. Linux命令之查看内存和CPU消耗命令TOP使用

    1)输入top ,按enter键,即可查看服务器内存消耗情况 注意:其中PID表示进程号 :%cpu表示cpu消耗情况:%M表示内存消耗情况:通常在做性能测试的时候用到该命令: 默认为实时刷新:按s键 ...

  4. javascript中的循环引用对象处理

    先说明一下什么是循环引用对象: var a={"name":"zzz"}; var b={"name":"vvv"}; ...

  5. 是否应该提供一个dao.insertIgnoreNull ? (像updateIgnoreNull一样)

     是否应该提供一个dao.insertIgnoreNull ? (像updateIgnoreNull一样)  发布于 406天前  作者 SayingCode  153 次浏览  复制  上一个帖子  ...

  6. DP,得到最多苹果,(POJ2385)

    题目链接:http://poj.org/problem?id=2385 题意: 牛在两棵苹果树下收集苹果,牛只能在这两棵树之间走动w次,在t时刻,某棵树会掉下苹果. 解题报告: ///dp[t][w] ...

  7. Kruskal算法求最小生成树(POJ2485)

    题目链接:http://poj.org/problem?id=2485 #include <iostream> #include <stdio.h> #include < ...

  8. chrome 浏览器插件开发(二)—— 通信 获取页面变量 编写chrome插件专用的库

    在chrome插件的开发过程中,我遇到了一些问题,在网上找了不少文章,可能是浏览器升级的原因,有一些是有效的也有无效的.下面我简单的分享一下我遇到的坑,以及我把这些坑的解决方案整理而成的js库 —— ...

  9. python—命名空间、作用域查找顺序、闭包

    名称空间 name space,如下图: x = 1, 1存放在内存中,1 会有一个内存地址,x 则 存放在 name space 里,并同时记录了 1的内存地址, 即 名称空间是存放了变量x与1绑定 ...

  10. js 动态创建标记

    innerHTML:一旦使用了这个属性,它的全部内容都要被替换掉.且不会返回任何对刚插入的内容的引用 与document.write()方法一样,innerHTML属性也是HTML专有属性,不能用于任 ...