一、isUrlVisited
这主要是在Frontier里实现的,当一个链接要进入等待队列时需要先判断是否已经被抓取过,如果已经抓取过则不进入,否则进入。
其中最重要的部分就是存储已抓取url的结构。为了提高效率,Heritrix在内部使用了Berkeley DB,BdbFrontier是唯一个具有实际意义的链接工厂。
Heritrix中涉及存储url的主要的类有UriUniqFilter、FPMergeUriUniqFilte、SetBasedUriUniqFilter、BdbUriUniqFilter、 BloomUriUniqFilter、 emFPMergeUriUniqFilter和DiskFPMergeUriUniqFilter。
用户可以在创建一个爬取任务时选择BdbUriUniqFilter, BloomUriUniqFilter, emFPMergeUriUniqFilter和DiskFPMergeUriUniqFilter中的一种。默认是BdbUriUniqFilter。
下面将分别介绍这四种UriUniqFilter。
(1)BdbUriUniqFilter
数据结构
这里存储已经处理过的url的数据结构是Berkeley Database,叫做alreadySeen。声明代码如下:
protected transient Database alreadySeen = null;
Berkeley DB,它是一套开放源代码的嵌入式数据库。简单的说,Berkeley DB就是一个Hash Table,它能够按“key/value”方式来保存数据。使用Berkeley DB时,数据库和应用程序在相同的地址空间中运行,所以数据库操作不需要进程间的通讯。另外,Berkeley DB中的所有操作都使用一组API接口。因此,不需要对某种查询语言(比如SQL)进行解析,也不用生成执行计划,这就大大提高了运行效率。
算法:
为了节省存储空间,alreadySeenUrl中存储的并不是url,而是url的fingerprint。为了不破坏url的局部性,分别对url的主机名和整个url计算fingerprint,然后把24位的主机名fingerprint和40位的url的fingerprint连接起来得到最后的64位的fingerprint。
计算fingerprint是在createKey函数中实现。关键代码如下如下:
CharSequence hostPlusScheme = (index == -1)? url: url.subSequence(0, index);
long tmp = FPGenerator.std24.fp(hostPlusScheme);
return tmp | (FPGenerator.std40.fp(url) >>> 24);
setAdd函数把uri加入到数据库中,如果已经存在,则返回false,否则返回true。关键代码如下:
status = alreadySeen.putNoOverwrite(null, key, ZERO_LENGTH_ENTRY);
setRemove函数把uri从数据库中删除,如果成功则返回true,否则返回false。关键代码如下:
status = alreadySeen.delete(null, key);
(2)BloomUriUniqFilter
数据结构
这里采用的数据结构是BloomFilter,实现版本有很多种,默认采用BloomFilter32bitSplit。Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。
算法
初始状态时,Bloom Filter是一个包含m位的位数组,每一位都置为0。为了表达S={x1, x2,…,xn}这样一个n个元素的集合,Bloom Filter使用d个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素x,第i个哈希函数映射的位置hi(x)就会被置为1(1≤i≤k)。如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。
add函数实现了把url加入到Bloom Filter中的功能,关键代码如下:
boolean result = false; //插入是否成功
int i = d, l = s.length();//i表示第几个哈希函数,l是url的长度
long h; //存放哈希值
while( i-- != 0 ) {
h = hash( s, l, i );//用第i个哈希函数计算s的哈希值
if ( ! setGetBit( h ) ) result = true;//把h插入位数组中,并返回插入之前的值
}
return result; //返回插入是否成功
在判断y是否属于这个集合时,我们对y应用k次哈希函数,如果所有hi(y)的位置都是1(1≤i≤k),那么我们就认为y是集合中的元素,否则就认为y不是集合中的元素。这在contains函数中实现,关键代码如下:
int i = d, l = s.length();
while( i-- != 0 )
if ( ! getBit( hash( s, l, i ) ) ) return false;
//只要有一个哈希函数的值对应的位数组值是0,就返回false。
return true;
这种方式的缺陷是不能删除元素,这是由filters的工作方式决定的。
(3)emFPMergeUriUniqFilter和DiskFPMergeUriUniqFilter
二者都继承自FPMergeUriUniqFilter,区别就在于fingerprint存放在内存中还是磁盘上。下面先来介绍一下FPMergeUriUniqFilter,再分析emFPMergeUriUniqFilter和DiskFPMergeUriUniqFilter的区别。
数据结构:
数据主要有url和url的fingerprint,分别存储在不同文件中。为了便于之后的说明,采用下列符号。
U和U’:存储URL的磁盘文件,一行是一个URL。
T和T’:存储URL的fingerprint和该URL在U中的顺序。
F:存储URL的fingerprint。
算法
首先,把url的fingerprint与缓存中的流行url和哈希表T进行比较,如果已经在其中任何一个里面,不需要进行任何操作。否则,把url存在磁盘文件U中,把fingerprint和对应的url在U中的顺序存在T中。
一旦T的大小超过了预先设置的值,把T的内容复制到T’中,重命名U为U’。在其他爬虫继续工作时,T’和U’的内容被分别添加到F和frontier中,也就是merge的过程。先把T’ 根据fingerprint排序,线性合并T’和F,并标记加入到F的那些行。然后把T’根据顺序值排序,把U’里所有T’标记了的url加到frontier中。
FPMergeUriUniqFilter有两个重要的属性pendingSet:TreeSet<PendingItem>和quickCache:ArrayLongFPCache。pendingSet就相当于上面的T’,存放着等待merge的fingerprint和相应的url。quickCache缓存着最近见过的FP。
FPMergeUriUniqFilter在merge的时候调用了三个抽象函数:beginFpMerge、addNewFp、finishFpMerge。emFPMergeUriUniqFilter和DiskFPMergeUriUniqFilter的区别就在于用不同的方法实现了这三个函数,其实质就是fingerprint存在内存中还是硬盘上。拿最简单的addNewFp函数来看一下:
emFPMergeUriUniqFilter
protected void addNewFp(long currFp) {
newFps.add(currFp); // newFps是 LongArrayList类型
}
DiskFPMergeUriUniqFilter:
protected void addNewFp(long fp) {
try {
newFps.writeLong(fp);// newFps是DataOutputStream类型
newCount++;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
二、politeness
1. one connection at a time
Heritrix的礼貌性主要在Frontier中实现:一次对一个服务器只开一个链接,并且保证uri按一定速率处理,从而不会给被爬取的服务器造成负担。
数据结构:
爬虫采用宽度优先遍历,使用FIFO的队列来存储待爬取的URL。因为网页的局部性,队列中相邻的URL很可能是相同主机名的,这样爬取会给服务器造成很大负担。如果用很多队列来存放URL,每个队列中URL的主机名相同,同一时间里,只允许队列中一个URL被爬取,就能避免上述问题了。
heritrix中主机名相同的URL队列是用WorkQueue来实现的,一个WorkQueue就是一个具有相同主机名的队列。frontier中用Map类型的allQueues存储着主机名和相应的队列;snoozedClassQueues存储着所有休眠的url队列的key,它们都按唤醒时间排序;readyClassQueues存储着已经准备好被爬取的队列的key;inactiveQueues存储着所有非活动状态的url队列的key;retiredQueues存储着不再激活的url队列的key。
算法:
线程返回readyClassQueues和snoozedClassQueues中已经到唤醒时间的队列中第一个url,下载相应的文档,完成之后从队列中移除该url。
每爬取到一个url都需要判断应该加入哪个队列中。 首先根据url的主机名判断是否存在该主机名的队列,如果不存在就新建一个队列。然后判断该队列是否在生命周期内,如果不在就设置为在生命周期内。如果队列需要保持不激活状态或者活动队列的数量超过设定的阈值,就把该队列放入inactiveQueues中,否则放在readyClassQueues中。
另外,heritrix还设定了很多参数来限制对服务器的访问频率。如最长等待时间max-delay-ms,默认30秒;重连同一服务器至少等待时间min-delay-ms,默认是3秒,重连同一服务器要等待上次连接至今时间间隔的几倍delay-factor,默认是5。当然这些参数用户也可以在配置爬虫的时候自己设定。
2. robots.txt
robots.txt称为机器人协议,放在网站的根目录下。在这个文件中声明该网站中不想被robot 访问的部分,或者指定搜索引擎只收录指定的内容。这是一个君子协定,爬虫可以不遵守,但是出于礼貌最好遵守。
heritrix在预处理阶段处理robots.txt。它把针对每个user-agent的allow和disallow封装为一个RobotsDirectives类,整个robots.txt用一个Robotstxt对象来存储。
heritrix处理robots.txt有五种方法,都封装在RobotsHonoringPolicy中。这五种方法分别是:
Classic:遵守robots.txt对当前user-agent的第一部分指令。
Ignore:忽略robots.txt。
Custom:遵守robots.txt中特定操作的指令。
Most-favored:遵守最宽松的指令。
Most-favored-set:给定一些user-agent格式的集合,遵守最宽松的限制。
当策略是Most-favored或Most-favored-set时,可以选择是否伪装成另一个user agent。
RobotsExlusionPolicy类中包含heritrix最终处理robots.txt的方法,disallows用来判断userAgent能否访问某个url。它完全依据用户在新建一个爬虫任务时设置的处理robots.txt的策略来实现。