[原创]手把手教你写网络爬虫(7):URL去重
手把手教你写网络爬虫(7)
作者:拓海 (https://github.com/tuohai666)
摘要:从零开始写爬虫,初学者的速成指南!
封面:
本期我们来聊聊URL去重那些事儿。以前我们曾使用Python的字典来保存抓取过的URL,目的是将重复抓取的URL去除,避免多次抓取同一网页。爬虫会将待抓取的URL放在todo队列中,从抓取到的网页中提取到新的URL,在它们被放入队列之前,首先要确定这些新的URL是否被抓取过,如果之前已经抓取过了,就不再放入队列。
有别于单机系统,在分布式系统中,这些URL应该存放在公共缓存中,才能让多个爬虫实例共享,我们继续使用Redis缓存这些数据。URL既可以存储在Redis的Set数据结构中,也可以将URL作为Key存储为Redis的String类型。至于这两种方案各有什么优缺点,就留给读者自己去思考了。
直接存储URL
将URL以字符串的形式直接存储到内存中。保守估计一下URL的平均长度是100字节,那么1亿个URL所占的内存是: 100000000 * 0.0001MB = 10000MB,约等于10G。这也不是不能用,占用的空间再大都能通过扩容来解决。
问题是,如果一个服务器存不下这么多URL该怎么办呢?其实也简单,明确每台服务器的分工,也就是说得到一个URL就知道要交给哪台服务器存储,每台服务器只存储一类URL,比较简单的实现方式就是对URL先哈希再取模。虽然能用,但还是有很大优化空间的。
存储消息摘要
MD5是一个消息摘要算法,它的用途很广泛,我们这里用它来压缩URL。
消息摘要算法的特点:
- 无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。
- 只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出。
- 消息摘要是单向、不可逆的。只能进行正向的信息摘要,而无法从摘要中恢复出任何的原始消息。
以上特点说明我们可以通过存储URL的MD5来实现去重功能,因为不同的URL,MD5不同,相同的URL,MD5相同嘛。
对应的MD5值是这样的:d552b0b40e21d06d73a1a0938635eb1a
怎么样?省了不少空间吧?
有人说,拓海你不要骗我,这个算法的输入是个无穷集合,而输出是一个有限集合,必然会存在碰撞的,也就是存在不同的URL算出相同的MD5。这会导致去重时误判,少抓数据!
好吧,从理论上来说,必然会出现这种情况。可是出现这种情况的概率是多少呢?下面就算算两个不同URL产生相同消息摘要的概率。
以下是三种常见的消息摘要算法,分别是32、64、128字节,每个字节是十六进制数字的字符,它们的可能值数量分别是:
md5: 16^32 = 2^128 = 3.4 * 10^38
sha256: 16^64 = 2^256 = 1.2 × 10^77
sha512: 16^128 = 2^512 = 1.3 × 10^154
你可能会说,我是数字盲,我不知道这个数大不大。好吧,我为你找到了两个直观的参照物:
IPv6编码地址数:2^128(约3.4×10^38)
IPv6是IETF设计的用于替代现行版本IP协议(IPv4)的下一代IP协议,号称可以为全世界的每一粒沙子编上一个网址。
可观测宇宙中的原子总数:10^80
上图是哈勃望远镜对准天球上一个特定的区域(相当于整个天球面积的1/12700000)进行长时间的图像拍摄,最后在这个区域里面找到了约有10000个星系。这样可以合理的推测,目前我们能用天文望远镜观测到的宇宙范围内有1.27x10^11个星系。
一个星系的恒星数(行星忽略不计了)目前普遍接受的一个数量级是4x10^11个。
像太阳这样的恒星的质量是1.96x10^30kg。
这就可以算出宇宙的总质量为9.96x10^55kg。
一个氢原子的质量是1.66x10^-24g。
用宇宙质量除以一个氢原子的质量就得出了目前可观测宇宙中的近似原子个数是10^80个。
可见,不同URL产生相同消息摘要的可能性非常小,简直像大海捞针。。。不是,简直像宇宙中捞原子一样难,所以就放心使用吧。
消息摘要实现了对URL的压缩,但压缩后的大小还是和原来在一个数量级,空间效率并没有质的提升。有没有办法只用几个bit来唯一标识一个URL呢?有!布隆过滤器就是专门解决这类问题的。
布隆过滤器
Bloom Filter是一种空间效率很高的随机数据结构,它利用bit数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。
它的原理很简单,首先需要准bit数组(所有位初始化为0)和k个独立hash函数。将hash函数对应的值的位数组置1,查找时如果发现所有hash函数对应位都是1说明存在,否则不存在。很明显这个过程并不保证查找的结果是100%正确的。
如何根据输入元素个数n,确定bit数组m的大小及hash函数个数呢?当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倍。假设错误率为0.01,则此时m应是n的13倍。这样k大概是8个。
Google的Guava基础库里有布隆过滤器的实现,非常的简洁和有深度,我们一起来学习一下这段java代码。
public <T> boolean put(T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
long bitSize = bits.bitSize();
long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32); boolean bitsChanged = false;
for (int i = 1; i <= numHashFunctions; i++) {
int combinedHash = hash1 + (i * hash2);
// Flip all the bits if it's negative (guaranteed positive number)
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
bitsChanged |= bits.set(combinedHash % bitSize);
}
return bitsChanged;
}
01 函数的功能是把一条数据hash后保存到BitArray中,如果BitArray有变化则返回true,否则返回false。参数是数据、hash函数个数、BitArray地址。
03 使用murmur3 hash出一个long型的值。为什么是一个,不应该是numHashFunctions个吗?请往下看。
04 05 把hash64切成两半,变成hash1和hash2。
08 09 重点来了,numHashFunctions个hash函数原来是这么来的:hash1+(i*hash2)。Excuse me? 这种操作太随意了吧?不用担心,请看《Less Hashing, Same Performance: Building a Better Bloom Filter》,里面论述了这种操作不会影响布隆过滤器的性能:A standard technique from the hashing literature is to use two hash functions h1(x) and h2(x) to simulate additional hash functions of the form gi(x) = h1(x) + ih2(x). We demonstrate that this technique can be usefully applied to Bloom filters and related data structures. Specifically, only two hash functions are necessary to effectively implement a Bloom filter without any loss in the asymptotic false positive probability.这个优化非常有用,毕竟hash的代价还是很大的。
11 12 是负的就取反(这里的操作都很粗暴)。
14 设置BitArray里对应的bit,下面进入set()里看看。
boolean set(long index) {
if (!get(index)) {
data[(int) (index >>> 6)] |= (1L << index);
bitCount++;
return true;
}
return false;
} boolean get(long index) {
return (data[(int) (index >>> 6)] & (1L << index)) != 0;
}
02 先get()一下,看看是不是已经置为1。
03 index右移6位就是除以64,说明data是long型的数组,除以64就定位到了bit所在的数组下标。1L左移index位,定位到了bit在long中的位置。
下一步
以上就是URL去重的一点思路,希望对大家有帮助。下期打算为大家介绍下字符编解码,以及乱码的完美解决方案。再见!
[原创]手把手教你写网络爬虫(7):URL去重的更多相关文章
- [原创]手把手教你写网络爬虫(4):Scrapy入门
手把手教你写网络爬虫(4) 作者:拓海 摘要:从零开始写爬虫,初学者的速成指南! 封面: 上期我们理性的分析了为什么要学习Scrapy,理由只有一个,那就是免费,一分钱都不用花! 咦?怎么有人扔西红柿 ...
- [原创]手把手教你写网络爬虫(5):PhantomJS实战
手把手教你写网络爬虫(5) 作者:拓海 摘要:从零开始写爬虫,初学者的速成指南! 封面: 大家好!从今天开始,我要与大家一起打造一个属于我们自己的分布式爬虫平台,同时也会对涉及到的技术进行详细介绍.大 ...
- Python爬虫:手把手教你写迷你爬虫架构
前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:我爱学Python 语言&环境 语言:继续用Python开路 ...
- 手把手教你写基于C++ Winsock的图片下载的网络爬虫
手把手教你写基于C++ Winsock的图片下载的网络爬虫 先来说一下主要的技术点: 1. 输入起始网址,使用ssacnf函数解析出主机号和路径(仅处理http协议网址) 2. 使用socket套接字 ...
- 手把手教你写电商爬虫-第三课 实战尚妆网AJAX请求处理和内容提取
版权声明:本文为博主原创文章,未经博主允许不得转载. 系列教程: 手把手教你写电商爬虫-第一课 找个软柿子捏捏 手把手教你写电商爬虫-第二课 实战尚妆网分页商品采集爬虫 看完两篇,相信大家已经从开始的 ...
- 手把手教你写电商爬虫-第四课 淘宝网商品爬虫自动JS渲染
版权声明:本文为博主原创文章,未经博主允许不得转载. 系列教程: 手把手教你写电商爬虫-第一课 找个软柿子捏捏 手把手教你写电商爬虫-第二课 实战尚妆网分页商品采集爬虫 手把手教你写电商爬虫-第三课 ...
- 网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接
本文原作者:“水晶虾饺”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.引言 好多小白初次接触即时通讯(比如:IM或者消息推送应用)时,总是不 ...
- 手把手教你写Sublime中的Snippet
手把手教你写Sublime中的Snippet Sublime Text号称最性感的编辑器, 并且越来越多人使用, 美观, 高效 关于如何使用Sublime text可以参考我的另一篇文章, 相信你会喜 ...
- 手把手教你写LKM rookit! 之 第一个lkm程序及模块隐藏(一)
唉,一开始在纠结起个什么名字,感觉名字常常的很装逼,于是起了个这<手把手教你写LKM rookit> 我觉得: 你们觉得:...... 开始之前,我们先来理解一句话:一切的操作都是系统调用 ...
随机推荐
- vscode使用shell
https://stackoverflow.com/questions/42606837/how-to-use-bash-on-windows-from-visual-studio-code-inte ...
- python subprocess模块使用总结
一.subprocess以及常用的封装函数运行python的时候,我们都是在创建并运行一个进程.像Linux进程那样,一个进程可以fork一个子进程,并让这个子进程exec另外一个程序.在Python ...
- python 字符串的方法
capitalize() 把字符串的第一个字符改为大写 casefold() 把整个字符串的所有字符改为小写 center(width) 将字符串居中,并使用空格填充至长度 width 的新字符串 c ...
- [Luogu1801] 黑匣子 - Treap
Description Black Box是一种原始的数据库.它可以储存一个整数数组,还有一个特别的变量i.最开始的时候Black Box是空的.而i等于0.这个Black Box要处理一串命令. 命 ...
- Linux-centos-7.2-64bit 安装配置mysql
2018-04-12 安装在/usr/local/下,配置文件在/etc/my.ini 1.下载mysql安装包到 /usr/local/software cd /usr/local/software ...
- 如何用Math.max.apply()获取数组最大/小值
最近似乎对JavaScript有点兴趣了~~~打算好好钻研这个东西.可是,一开始就遇到问题了!!! Math.min.apply(obj,args);//这个obj对象将代替Function类里thi ...
- Selenium_java coding
1)public class HelloWorld { // class 是类的意思 // 类名指的是class后面这个词,这个词是我们起的名 public static void main(Stri ...
- Windows安装SVN服务器和客户端
我的操作系统版本是windows10 64位.接下来我会先介绍SVN服务器的安装,然后再介绍安装SVN客户端,并进行测试. 下载 首先我们需要到官网上去下载svn服务器程序. [svn官网地址] (h ...
- angularjs中的几种工具方法
1.比较两个字符串是否相等 2.对象形式转化成json和json转化成字符串形式 3.便利对象遍历数组 4.绑定数据 5.多个app功能模块的实现 <!doctype html><h ...
- [LeetCode] Find K-th Smallest Pair Distance 找第K小的数对儿距离
Given an integer array, return the k-th smallest distance among all the pairs. The distance of a pai ...