http://blog.sina.com.cn/s/blog_5484ad0d01008gox.html
http://guoyunsky.iteye.com/category/82971
http://blog.csdn.net/gris0509/archive/2009/11/15/4812641.aspx
Heritrix的架构图如下:
在以上两个博客里,很详细的介绍了Heritrix的结构,可以帮助理解Heritrix的工作原理。个人觉得在理解中最重要的有如下几点:
1. Frontier链接制造工厂
首先,Frontier是用来向线程提供链接的,因此,在上面的代码中(在我所列出的第一个博客中的Heritrix的架构 -2 中,有代码),使用了两个ArrayList来保存链接。其中,第一个pendingURIs保存的是等待处理的链接,第二个 prerequisites中保存的也是链接,只不过它里面的每个链接的优先级都要高于pendingURIs里的链接。通常,在 prerequisites中保存的都是如DNS之类的链接,只有当这些链接被首先解析后,其后续的链接才能够被解析。
其次,除了这两个ArrayList外,在上面的 Frontier还有一个名称为alreadyIncluded的HashMap。它用于记录那些已经被处理过的链接。每当调用Frontier的 schedule()方法来加入一个新的链接时,Frontier总要先检查这个正要加入到队列中的链接是不是已经被处理过了。
很显然,在分析网页的时候,会出现大量相同的链 接,如果没有这种检查,很有可能造成抓取任务永远无法完成的情况。同时,在schedule()方法中还加入了一些逻辑,用于判断当前要进入队列的链接是 否属于需要优先处理的,如果是,则置入prerequisites队列中,否则,就简单的加入pendingURIs中即可。
注意:Frontier中还有两个关键的方法,next()和 finished(),这两个方法都是要交由抓取的线程来完成的。Next()方法的主要功能是:从等待队列中取出一个链接并返回,然后抓取线程会在它自 己的run()方法中完成对这个链接的处理。而finished()方法则是在线程完成对链接的抓取和后续的一切动作后(如将链接传递经过处理器链)要执 行的。它把整个处理过程中解析出的新的链接加入队列中,并且在处理完当前链接后,将之加入alreadyIncluded这个HashMap中去。
但是在Frontier实现的过程中,要面临很多问题,其中就有“大数据量,多并发”的存在。所以,在Frontier实现时,考虑了Berkeley Database,即bdb(缩写)。
简单的说,Berkeley DB就是一个HashTable,它能够按“key/value”方式来保存数据。它是由美国Sleepycat公司开发的一套开放源代码的嵌入式数据库,它为应用程序提供可伸缩的、高性能的、有事务保护功能的数据管理服务。
那么,为什么不使用一个传统的关系型数据库呢? 这是因为当使用BerkeleyDB时,数据库和应用程序在相同的地址空间中运行,所以数据库操作不需要进程间的通讯。然而,当使用传统关系型数据库时, 就需要在一台机器的不同进程间或在网络中不同机器间进行进程通讯,这样所花费的开销,要远远大于函数调用的开销。
另外,Berkeley DB中的所有操作都使用一组API接口。因此,不需要对某种查询语言(比如SQL)进行解析,也不用生成执行计划,这就大大提高了运行效率。
当然,做为一个数据库,最重要的功能就是事务的 支持,Berkeley DB中的事务子系统就是用来为其提供事务支持的。它允许把一组对数据库的修改看作一个原子单位,这组操作要么全做,要么全不做。在默认的情况下,系统将提 供严格的ACID事务属性,但是应用程序可以选择不使用系统所作的隔离保证。该子系统使用两段锁技术和先写日志策略来保证数据的正确性和一致性。这种事务 的支持就要比简单的HashTable中的Synchronize要更加强大。
注意:在Heritrix中,使用的是Berkeley DB的Java版本,这种版本专门为Java语言做了优化,提供了Java的API接口以供开发者使用。
为什么Heritrix中要用到Berkeley DB呢?这就需要再回过头来看一下Frontier了。
在上一小节中,当一个链接被处理后,也即经过处 理器链后,会生成很多新的链接,这些新的链接需要被Frontier的一个schedule方法加入到队列中继续处理。但是,在将这些新链接加入到队列之 前,要首先做一个检查,即在alreadyIncluded这个HashMap中,查看当前要加入到队列中的链接是否在先前已经被处理过了。
当使用HashMap来存储那些已经被处理过的链接时,HashMap中的key为url,而value则为一个对url封装后的对象。很显然的,这里有几个问题。
l 对这个HashMap的读取是多线程的,因为每个线程都需要访问这个HashMap,以决定当前要加入链接是否已经存在过了。
l 对这个HashMap的写入是多线程的,每个线程在处理完毕后,都会访问这个HashMap,以写入最新处理的链接。
l 这个HashMap的容量可能很大,可以试想,一次在广域网范围上的网页抓取,可能会涉及到上十亿个URL地址,这种地址包括网页、图片、文件、多媒体对象等,所以,不可能将这么大一张表完全的置放于内存中。
综合考虑以上3点,仅用一个HashMap来保 存所有的链接,显然已经不能满足“大数据量,多并发”这样的要求。因此,需要寻找一个替代的工具来解决问题。Heritrix中的BdbFrontier 就采用了Berkeley DB,来解决这种URL存放的问题。事实上,BdbFrontier就是Berkeley DB Frontier的简称。
为了在BdbFrontier中使用Berkeley DB,Heritrix本身构造了一系列的类来帮助实现这个功能。这些类如下:
l BdbFrontier
l BdbMultipleWorkQueues
l BdbWorkQueue
l BdbUriUniqFilter
上述的4个类,都以Bdb3个字母开头,这表明它们都是使用到了Berkeley DB的功能。其中:
(1)BdbMultipleWorkQueues代表了一组链接队列,这些队列有各自不同的key。这样,由Key和链接队列可以形成一个“Key/Value”对,也就成为了Berkeley DB里的一条记录(DatabaseEntry)
可以说,这就是一张Berkeley DB的数据库表。其中,数据库的一条记录包含两个部分,左边是一个由右边的所有URL链接计算出来的公共键值,右边则是一个URL的队列。
(2)BdbWorkQueue代表了一个基于 Berkeley DB的队列,与BdbMutipleWorkQueues所不同的是,该队列中的所有的链接都具有相同的键值。事实上,BdbWorkQueue只是对 BdbMultipleWorkQueues的封装,在构造一个BdbWorkQueue时,需传入一个健值,以此做为该Queue在数据库中的标识。事 实上,在工作线程从Frontier中取出链接时,Heritrix总是先取出整个BdbWorkQueue,再从中取出第一个链接,然后将当前这个 BdbWorkQueue置入一个线程安全的同步容器内,等待线程处理完毕后才将该Queue释放,以便该Queue内的其他URI可以继续被处理。
(3)BdbUriUniqFilter是一个 过滤器,从名称上就能知道,它是专门用来过滤当前要进入等待队列的链接对象是否已经被抓取过。很显然,在BdbUriUniqFilter内部嵌入了一个 Berkeley DB数据库用于存储所有的被抓取过的链接。它对外提供了
public void add(String key, CandidateURI value)
这样的接口,以供Frontier调用。当然,若是参数的CandidateURI已经存在于数据库中了,则该方法会禁止它加入到等待队列中去。
(4)BdbFrontier就是 Heritrix中使用了Berkeley DB的链接制造工厂。它主要使用BdbUriUniqFilter,做为其判断当前要进入等待队列的链接对象是否已经被抓取过。同时,它还使用了 BdbMultipleWorkQueues来做为所有等待处理的URI的容器。这些URI根据各自的内容会生成一个Hash值成为它们所在队列的键值。
在Heritrix1.10的版本中,可以说BdbFrontier是惟一一个具有实用意义的链接制造工厂了。虽然Heritrix还提供了另外两个Frontier:
org.archive.crawler.frontier.DomainSensitiveFrontier
org.archive.crawler.frontier.AdaptiveRevisitFrontier
但是, DomainSensitiveFrontier已经被废弃不再推荐使用了。而AdaptiveRevisitFrontier的算法是不管遇到什么新链 接,都义无反顾的再次抓取,这显然是一种很落后的算法。因此,了解BdbFrontier的实现原理,对于更好的了解Heritrix对链接的处理有实际 意义。
BdbFrontier的代码相对比较复杂,笔者在这里也只能简单将其轮廓进行介绍,读者仍须将代码仔细研读,方能把文中的点点知识串联起来,进而更好的理解Heritrix作者们的巧妙匠心。
2. Heritrix的多线程ToeThread和ToePool
想要更有效更快速的抓取网页内容,则必须采用多线程。Heritrix中提供了一个标准的线程池ToePool,它用于管理所有的抓取线程。
ToePool和ToeThread都位于 org.archive.crawler.framework包中。前面已经说过,ToePool的初始化,是在CrawlController的 initialize()方法中完成的。(本条内容参考我列出的博客中第一条中的Heritrix的架构 -3)
3. 处理链和Processor(非常重要)
处理器链包括以下几种:
l PreProcessor
l Fetcher
l Extractor
l Writer
l PostProcessor
为了很好的表示整个处理器链的逻辑结构,以及它们之间的链式调用关系,Heritrix设计了几个API来表示这种逻辑结构。
org.archive.crawler.framework.Processor
org.archive.crawler.framework.ProcessorChain
org.archive.crawler.framework.ProcessorChainList
下面进行详细讲解。
(1) Processor类
该类代表着单个的处理器,所有的处理器都是它的子类。在Processor类中有一个process()方法,它被标识为final类型的,也就是说,它不可以被它的子类所覆盖。代码如下。
代码10.8
public final void process(CrawlURI curi) throws InterruptedException
{
// 设置下一个处理器
curi.setNextProcessor(getDefaultNextProcessor(curi));
try
{
// 判断当前这个处理器是否为enabled
if (!((Boolean) getAttribute(ATTR_ENABLED, curi)).booleanValue()) {
return;
}
} catch (AttributeNotFoundException e) {
logger.severe(e.getMessage());
}
// 如果当前的链接能够通过过滤器调用innerProcess(curi)方法来进行处理
if(filtersAccept(curi)) {
innerProcess(curi);
}
// 如果不能通过过滤器检查,则调用innerRejectProcess(curi)来处理
else
{
innerRejectProcess(curi);
}
}
方法的含义很简单。即首先检查是否允许这个处理器处理该链接,如果允许,则检查当前处理器所自带的过滤器是否能够接受这个链接。当过滤器的检查也通过后,则调用innerProcess(curi)方 法来处理,如果过滤器的检查没有通过,就使用innerRejectProcess(curi)方法处理。
其中innerProcess(curi)和 innerRejectProcess(curi)方法都是protected类型的,且本身没有实现任何内容。很明显它们是留在子类中,实现具体的处理逻辑。不过大部分的子类都不会重写innerRejectProcess(curi)方法了,这是因为反正一个链接已经被当前处理器拒绝处理了,就不用再有什么逻辑了,直接跳到下一个处理器继续处理就行了。
(2) ProcessorChain类
该类表示一个队列,里面包括了同种类型的几个Processor。例如,可以将一组的Extractor加入到同一个ProcessorChain中去。
在一个ProcessorChain中,有3个private类型的类变量:
private final MapType processorMap;
private ProcessorChain nextChain;
private Processor firstProcessor;
其中,processorMap中存放的是当前 这个ProcessorChain中所有的Processor。nextChain的类型是ProcessorChain,它表示指向下一个处理器链的指 针。而firstProcessor则是指向当前队列中的第一个处理器的指针。
3.ProcessorChainList
从名称上看,它保存了Heritrix一次抓取 任务中所设定的所有处理器链,将之做为一个列表。正常情况下,一个ProcessorChainList中,应该包括有5个 ProcessorChain,分别为PreProcessor链、Fetcher链、Extractor链、Writer链和 PostProcessor链,而每个链中又包含有多个的Processor。这样,就将整个处理器结构合理的表示了出来。
那么,在ToeThread的processCrawlUri()方法中,又是如何来将一个链接循环经过这样一组结构的呢?请看下面的代码(在org.archive.crawler.framework.ToeThread.java中):
代码10.9
private void processCrawlUri() throws InterruptedException { // 设定当前线程的编号 currentCuri.setThreadNumber(this.serialNumber); // 为当前处理的URI设定下一个ProcessorChain currentCuri.setNextProcessorChain(controller.getFirstProcessorChain()); // 设定开始时间 lastStartTime = System.currentTimeMillis(); try { // 如果还有一个处理链没处理完 while (currentCuri.nextProcessorChain() != null) { setStep(STEP_ABOUT_TO_BEGIN_CHAIN); // 将下个处理链中的第一个处理器设定为 // 下一个处理当前链接的处理器 currentCuri.setNextProcessor(currentCuri .nextProcessorChain().getFirstProcessor()); // 将再下一个处理器链设定为当前链接的 // 下一个处理器链,因为此时已经相当于 // 把下一个处理器链置为当前处理器链了 currentCuri.setNextProcessorChain(currentCuri .nextProcessorChain().getNextProcessorChain()); // 开始循环处理当前处理器链中的每一个Processor while (currentCuri.nextProcessor() != null) { setStep(STEP_ABOUT_TO_BEGIN_PROCESSOR); Processor currentProcessor = getProcessor(currentCuri.nextProcessor()); currentProcessorName = currentProcessor.getName(); continueCheck(); // 调用Process方法 currentProcessor.process(currentCuri); } } setStep(STEP_DONE_WITH_PROCESSORS); currentProcessorName = ""; } catch (RuntimeExceptionWrapper e) { // 如果是Berkeley DB的异常 if(e.getCause() == null) { e.initCause(e.getDetail()); } recoverableProblem(e); } catch (AssertionError ae) { recoverableProblem(ae); } catch (RuntimeException e) { recoverableProblem(e); } catch (StackOverflowError err) { recoverableProblem(err); } catch (Error err) { seriousError(err); } }
代码使用了双重循环来遍历整个处理器链的结构,第一重循环首先遍历所有的处理器链,第二重循环则在链内部遍历每个Processor,然后调用它的process()方法来执行处理逻辑。
在controller中有5个chain,当处理的时候,按照顺序分别调用各个chain,在调用每个chain的时候,使用chain中的processor(如currentCuri.setNextProcessor(currentCuri.nextProcessorChain().getFirstProcessor());),这样一步步的使用controller中的processChain,使用processChain中的processor。
4.网络爬虫的整体结构可以参考: http://hanyuanbo.iteye.com/blog/779350