转载:http://blog.csdn.net/bone_ace/article/details/53107018

前言

  “去重”是日常工作中会经常用到的一项技能,在爬虫领域更是常用,并且规模一般都比较大。去重需要考虑两个点:去重的数据量、去重速度。为了保持较快的去重速度,一般选择在内存中进行去重。

  1、数据量不大时,可以直接放在内存里面进行去重,例如python可以使用set()进行去重。

  2、当去重数据需要持久化时可以使用redis的set数据结构。

  3、当数据量再大一点时,可以用不同的加密算法先将长字符串压缩成 16/32/40 个字符,再使用上面两种方法去重;

  4、当数据量达到亿(甚至十亿、百亿)数量级时,内存有限,必须用“位”来去重,才能够满足需求。Bloomfilter就是将去重对象映射到几个内存“位”,通过几个位的 0/1值来判断一个对象是否已经存在。

  5、然而Bloomfilter运行在一台机器的内存上,不方便持久化(机器down掉就什么都没啦),也不方便分布式爬虫的统一去重。如果可以在Redis上申请内存进行Bloomfilter,以上两个问题就都能解决了。

代码

# coding=utf-8
import redis
from hashlib import md5 class SimpleHash(object):
def __init__(self, cap, seed):
self.cap = cap
self.seed = seed def hash(self, value):
ret = 0
for i in range(len(value)):
ret += self.seed * ret + ord(value[i])
return (self.cap - 1) & ret class BloomFilter(object):
def __init__(self, host='localhost', port=6379, db=0, blockNum=1, key='bloomfilter'):
"""
:param host: the host of Redis
:param port: the port of Redis
:param db: witch db in Redis
:param blockNum: one blockNum for about 90,000,000; if you have more strings for filtering, increase it.
:param key: the key's name in Redis
"""
self.server = redis.Redis(host=host, port=port, db=db)
self.bit_size = 1 << 31 # Redis的String类型最大容量为512M,现使用256M
self.seeds = [5, 7, 11, 13, 31, 37, 61]
self.key = key
self.blockNum = blockNum
self.hashfunc = []
for seed in self.seeds:
self.hashfunc.append(SimpleHash(self.bit_size, seed)) def isContains(self, str_input):
if not str_input:
return False
m5 = md5()
m5.update(str_input)
str_input = m5.hexdigest()
ret = True
name = self.key + str(int(str_input[0:2], 16) % self.blockNum)
for f in self.hashfunc:
loc = f.hash(str_input)
ret = ret & self.server.getbit(name, loc)
return ret def insert(self, str_input):
m5 = md5()
m5.update(str_input)
str_input = m5.hexdigest()
name = self.key + str(int(str_input[0:2], 16) % self.blockNum)
for f in self.hashfunc:
loc = f.hash(str_input)
self.server.setbit(name, loc, 1) if __name__ == '__main__': bf = BloomFilter()
if bf.isContains('http://www.baidu.com'): # 判断字符串是否存在
print 'exists!'
else:
print 'not exists!'
bf.insert('http://www.baidu.com')

说明

  1、Bloomfilter算法如何使用位去重,这个百度上有很多解释。简单点说就是有几个seeds,现在申请一段内存空间,一个seed可以和字符串哈希映射到这段内存上的一个位,几个位都为1即表示该字符串已经存在。

    插入的时候也是,将映射出的几个位都置为1。

  2、需要提醒一下的是Bloomfilter算法会有漏失概率,即不存在的字符串有一定概率被误判为已经存在。这个概率的大小与seeds的数量、申请的内存大小、去重对象的数量有关。下面有一张表,m表示内存大小(多少个位),

   n表示去重对象的数量,k表示seed的个数。例如我代码中申请了256M,即1<<31(m=2^31,约21.5亿),seed设置了7个。看k=7那一列,当漏失率为8.56e-05时,m/n值为23。所以n = 21.5/23 = 0.93(亿),

   表示漏失概率为8.56e-05时,256M内存可满足0.93亿条字符串的去重。同理当漏失率为0.000112时,256M内存可满足0.98亿条字符串的去重。

  

  3、基于Redis的Bloomfilter去重,其实就是利用了Redis的String数据结构,但Redis一个String最大只能512M,所以如果去重的数据量大,需要申请多个去重块(代码中blockNum即表示去重块的数量)。

  4、代码中使用了MD5加密压缩,将字符串压缩到了32个字符(也可用hashlib.sha1()压缩成40个字符)。它有两个作用,一是Bloomfilter对一个很长的字符串哈希映射的时候会出错,经常误判为已存在,

    压缩后就不再有这个问题;二是压缩后的字符为 0~f 共16中可能,我截取了前两个字符,再根据blockNum将字符串指定到不同的去重块进行去重。

总结

    基于redis的Bloomfilter去重,既用上了Bloomfilter的海量去重能力,又用上了Redis的可持久化能力,基于Redis也方便分布式机器的去重。在使用的过程中,要预算好待去重的数据量,则根据上面的表,

  适当地调整seed的数量和blockNum数量(seed越少肯定去重速度越快,但漏失率越大)。

    另外针对基于Scrapy+Redis框架的爬虫,我使用Bloomfilter作了一些优化,只需替换scrapy_redis模块即可使用Bloomfilter去重,并且去重队列和种子队列可以拆分到不同的机器上,

  详情见:《scrapy_redis去重优化(已有7亿条数据),附Demo福利》,代码见:Scrapy_Redis_Bloomfilter

基于Redis的Bloomfilter去重(转载)的更多相关文章

  1. [转载]基于Redis的Bloomfilter去重(附Python代码)

    前言: “去重”是日常工作中会经常用到的一项技能,在爬虫领域更是常用,并且规模一般都比较大.去重需要考虑两个点:去重的数据量.去重速度.为了保持较快的去重速度,一般选择在内存中进行去重. 数据量不大时 ...

  2. 基于Redis的BloomFilter算法去重

    BloomFilter算法及其适用场景 BloomFilter是利用类似位图或者位集合数据结构来存储数据,利用位数组来简洁的表示一个集合,并且能够快速的判断一个元素是不是已经存在于这个集合.因为基于H ...

  3. [转载] 基于Redis实现分布式消息队列

    转载自http://www.linuxidc.com/Linux/2015-05/117661.htm 1.为什么需要消息队列?当系统中出现“生产“和“消费“的速度或稳定性等因素不一致的时候,就需要消 ...

  4. 转载:基于Redis实现分布式锁

    转载:基于Redis实现分布式锁  ,出处: http://blog.csdn.net/ugg/article/details/41894947 背景在很多互联网产品应用中,有些场景需要加锁处理,比如 ...

  5. 在阿里云Centos7.6上面部署基于Redis的分布式爬虫Scrapy-Redis

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_83 Scrapy是一个比较好用的Python爬虫框架,你只需要编写几个组件就可以实现网页数据的爬取.但是当我们要爬取的页面非常多的 ...

  6. 物联网应用中实时定位与轨迹回放的解决方案 – Redis的典型运用(转载)

    物联网应用中实时定位与轨迹回放的解决方案 – Redis的典型运用(转载)   2015年11月14日|    by: nbboy|    Category: 系统设计, 缓存设计, 高性能系统 摘要 ...

  7. 基于redis排行榜的实战总结

    前言: 之前写过排行榜的设计和实现, 不同需求其背后的架构和设计模型也不一样. 平台差异, 有的立足于游戏平台, 为多个应用提供服务, 有的仅限于单个游戏.排名范围差异, 有的面向全局排名, 有的只做 ...

  8. 基于redis分布式缓存实现

    Redis的复制功能是完全建立在之前我们讨论过的基 于内存快照的持久化策略基础上的,也就是说无论你的持久化策略选择的是什么,只要用到了Redis的复制功能,就一定会有内存快照发生,那么首先要注意你 的 ...

  9. 记一次企业级爬虫系统升级改造(六):基于Redis实现免费的IP代理池

    前言: 首先表示抱歉,春节后一直较忙,未及时更新该系列文章. 近期,由于监控的站源越来越多,就偶有站源做了反爬机制,造成我们的SupportYun系统小爬虫服务时常被封IP,不能进行数据采集. 这时候 ...

随机推荐

  1. 雅礼集训 Day6 T2 Equation 解题报告

    Equation 题目描述 有一棵\(n\)个点的以\(1\)为根的树,以及\(n\)个整数变量\(x_i\).树上\(i\)的父亲是\(f_i\),每条边\((i,f_i)\)有一个权值\(w_i\ ...

  2. echarts异步加载

    echarts体积很大,在移动端使用异步加载是一种提高渲染速度的方法,结合webpack的做法如下: require.ensure([], function(require){ const echar ...

  3. Pty的字符串(string)

    题目描述 在神秘的东方有一棵奇葩的树,它有一个固定的根节点(编号为1).树的每条边上都是一个字符,字符为a,b,c中的一个,你可以从树上的任意一个点出发,然后沿着远离根的边往下行走,在任意一个节点停止 ...

  4. url为什么要编码及php中的中文字符urlencode基本原理

    首先了解以下中文字符在使用urlencode的时候运用的基本原理: urlencode()函数原理就是首先把中文字符转换为十六进制,然后在每个字符前面加一个标识符%. 此字符串中除了 -_. 之外的所 ...

  5. poj 3311 floyd+dfs或状态压缩dp 两种方法

    Hie with the Pie Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 6436   Accepted: 3470 ...

  6. font-family 定义的最后为什么要加一句sans-serif

    定义font-family时,最好在最后加一个sans-serif,这样如果所列出的字体都不能用,则默认的sans-serif字体能保证调用; W3C建议字体定义的时候,最后以一个类别的字体结束,例如 ...

  7. Java Nio注意事项

    Selector  : public abstract class Selector extends Object SelectableChannel 对象的多路复用器. 可通过调用此类的 open ...

  8. css 实现高斯模糊

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. system call hooking 系统调用增加或劫持

    1. 引言:这篇文章提供了一种增加自定义系统调用或劫持原有的系统调用的实现方法,只针对 linux 系统.主要思路是获取系统调用表 sys_call_table 地址,然后用新函数地址覆盖系统调用表某 ...

  10. 生成一个空白BMP的简单代码【转】

    转自:http://blog.chinaunix.net/uid-15063109-id-4275395.html 做图像处理时,有时需要临时生成图使用.以下是生成320x240 24位图的一个简单的 ...