2023年11月28日发(作者:)

10 无比强大的网络爬虫Heritrix

Lucene很强大,这点在前面的章节中,已经作了详细介绍。但是,无论多么强大的搜索引

擎工具,在其后台,都需要一样东西来支援它,那就是网络爬虫Spider

网络爬虫,又被称为蜘蛛Spider,或是网络机器人、BOT等,这些都无关紧要,最重要的

是要认识到,由于爬虫的存在,才使得搜索引擎有了丰富的资源。

Heritrix是一个纯由Java开发的、开源的Web网络爬虫,用户可以使用它从网络上抓取想要

的资源。它来自于Heritrix最出色之处在于它的可扩展性,开发者可以扩

展它的各个组件,来实现自己的抓取逻辑。本章就来详细介绍一下Heritrix和它的各个组件。

10.1 Heritrix的使用入门

要想学会使用Heritrix,当然首先得能把它运行起来。然而,运行Heritrix并非一件容易的

事,需要进行很多配置。在Heritrix的文档中对它的运行有详细的介绍,不过尽管如此,笔

者仍然花了大量时间,才将其配置好并运行成功。

10.1.1 下载和运行Heritrix

Heritrix/

SourceForge的下载页面。当前Heritrix的最新版本为1.10

1)在下载完Heritrix的完整开发包后,解压到本地的一个目录下,如图10-1所示。

10-1 Heritrix的目录结构

其中,Heritrix所用到的工具类库都存于lib下,HeritrixJar包。另外,

Heritrix目录下有一个conf目录,其中包含了一个很重要的文件:ties

2ties中配置了大量与Heritrix运行息息相关的参数,这些参数主要是配

置了Heritrix运行时的一些默认工具类、WebUI的启动参数,以及Heritrix的日志格式等。

当第一次运行Heritrix时,只需要修改该文件,为其加入WebUI的登录名和密码,如图10-2

所示。

10-2 修改HeritrixWebUI的登录名和密码

其中,用户名和密码是以一个冒号进行分隔,使用者可以指定任何的字符串做为用户名密码,

图中所示只不过延续了Heritrix以前版本中默认的用户名和密码而已。

3)在设置完登录名和密码后,就可以开始运行Heritrix了。Heritrix有多种方式启动,例

如,可以使用CrawlController,以后台方式加载一个抓取任务,即为编程式启动。不过最常

见的还是以WebUI的方式启动它。

4Heritrix的主类为ix,运行它,就可以启动Heritrix。当然,在

运行它的时候,需要为其加上lib目录下的所有jar包。以下是笔者在命令行中启动Heritrix

时所使用的批处理文件,此处列出,仅供读者参考(笔者的Heritrix目录是位于E盘的根目

下,即E:heritrix

代码10.1

java -Xmx512m -=e:heritrix -cp

"E:;E:heritrixlibcommons-collect

;E:;E:heritrixlibpoi-scr

;E:;E:

;E:heritrixlibcommons-cli

-;E:;E:heritrixlibjavaswf-CVS-SN

;E:;E:heritrixlibservlet-tom

;E:;E:heritrixlibjasper-

;E:;E:herit

;E:;E:he

;E:;E:h

;E:;E:heritrix

;E:heritrixlibje-3.0.12.j

ar;E:;E:heritrixlibjasper-runtime

-;E:"

ix

5)在上面的批处理文件中,将Heritrix所用到的所有的第三方Jar包都写进了classpath

中,同时执行了ix这个主类。图10-3Heritrix启动时的画面。

10-3 Heritrix的启动画面

6)在这时,Heritrix的后台已经对服务器的8080端口进行了监听,只需要通过浏览器访

localhost:8080,就可以打开HeritrixWebUI了。如图10-4所示。

10-4 HeritrixWebUI的登录界面

7)在这个登录界面,输入刚才在ties中预设的WebUI的用户名和密码,就

可以进入如图10-5所示的HeritrixWebUI的主界面。

10-5 登录后的界面

8)当看到这个页面的时候,就说明Heritrix已经成功的启动了。在页面的中央有一道状

态栏,用于标识当前正在运行的抓取任务。如图10-6所示:

10-6 抓取任务的状态栏

在这个WebUI的帮助下,用户就可以开始使用Heritrix来抓取网页了。

10.1.2 Eclipse里配置Heritrix的开发环境

讲完了通过命令行方式启动的Heritrix,当然要讲一下如何在Eclipse中配置Heritrix的开发

环境,因为可能需要对代码进行调试,甚至修改一些它的源代码,来达到所需要的效果。下

面来研究一下Heritrix的下载包。

1webapps文件夹是用来提供Servlet引擎的,也就是提供HeritrixWebUI的部分,因

此,在构建开发环境时必不可少。conf文件夹是用来提供配置文件的,因此也需要配置进入

工程。Lib目录下主要是Heritrix在运行时需要用到的第三方的软件,因此,需要将其设定

EclipseBuild Path下。最后就是Heritrixjar包了,将其解压,可以看到其内部的结

构如图10-7所示。

10-7 HeritrixJar包的结构

2)根据图10-7所示,应该从Heritrix的源代码包中把这些内容取出,然后放置到工程中

来。Heritrix的源代码包解压后,只有两个文件夹,如图10-8所示。

10-8 Heritrix的源代码包的结构

3)只需在src目录下,把图10-7中的内容配全,就可以将工程的结构完整了。如图10-9

所示。

10-9 src目录下的内容

4)图10-10和图10-11是笔者机器上的HeritrixEclipse中的工程配置好后的截图,以

workspace中文件夹的预览。

10-10 Eclipse工程视图下的包结构 10-11 文件夹中的工程

其中,org目录内是Heritrix的源代码,另外,笔者将conf目录去掉了,直接将ties

文件放在了工程目录下。在图10-10中,读者可能没有看到Heritrix所使用到的Jar包,这

是因为在工程视图中,它们被过滤器过滤掉了,实际上,所有lib目录下的jar包都已经被

加进了build path中。

5)不过,读者很有可能遇到这样的情况,那就是在将所有的jar包都导入后,工程编译

完成,却发现在左边的package explorer中出现了大量的编译错误。如图10-12所示。

10-12 出现的编辑错误

6)随便打开一个出错的文件,如图10-13所示,会发现大量的错误都来自于“assert”关

键字。这种写法似乎Eclipse不认识。

10-13 出错的程序

7)解决问题的关键在于,Eclipse的编译器不认识assert这个关键字。可以在“选项”菜

单中将编译器的语法样式改为5.0,也就是JDK1.5兼容的语法,然后重启编译整个工程就

可以了。如图10-14所示。

10-14 改变编译器的语法等级

8)在重新编译完整个工程后,笔者的Eclipse中仍然出现了一个编译错误,那就是在

eRecord类中,如图10-15所示。

10-15 一个仍然存在的错误

从代码看来,这是因为在使用条件表达式,对strippedFileName这个String类型的对象赋值

时,操作符的右则出现了一个char型的常量,因此影响了编译。暂且不论为什么在Heritrix

的源代码中会出现这样的错误,解决问题的办法就是将char变成String类型,即:

(strippedFileName != null? strippedFileName: "-");

9)当这样修改完后,整个工程的错误就被全部解决了,也就可以开始运行Heritrix了。

Eclipse下运行ix类,如图10-16所示。

10-16 Eclipse中运行Heritrix

10当看到图10-17所示的界面时,就说明Heritrix已经成功的在Eclipse中运行,也就意

味着可以使用Eclipse来对Heritrix进行断点调试和源码修改了。

10-17 Eclipse中成功的运行

10.1.3 创建一个新的抓取任务

10.1.1小节中,已经看到了Heritrix成功运行后的WebUI,接下来,就要带领读者来创建

一个新的抓取作务。

1)单击WebUI菜单栏上的“Jobs”标签,就可以进入任务创建页面。如图10-18所示。

10-18 菜单栏上的“Jobs”标签

2)在任务创建页面中,有4种创建任务的方式,如图10-19所示,具体含义如下。

z Based on existing job:以一个已经有的抓取任务为模板,创建所有抓取属性和抓取

起始URL的列表。

z Based on a recovery:在以前的某个任务中,可能设置过一些状态点,新的任务将从

这个设置的状态点开始。

z Based on a profile专门为不同的任务设置了一些模板,新建的任务将按照模板来生

成。

z With defaults:这个最简单,表示按默认的配置来生成一个任务。

10-19 “任务”菜单中

Heritrix中,一个任务对应一个描述文件。这个描述文件的默认的名称为。每次

创建一个新任务时,都相当于生成了一个的文件。文件中详细记录了Heritrix在运

行时需要的所有信息。例如,它包括该用户所选择的Processor类、Frontier类、Fetcher类、

抓取时线程的最大数量、连接超时的最大等待时间等信息。上面所说的4种创建抓取任务的

方式,其实都是在生成一个文件。其中,第4With defaults,则是直接拷贝默认

文件。在所创建的Eclipse工程或是命令行启动的Heritrix下载包中,该默认的

文件均是放于profilesdefault目录下的。

关于的细节,在此还不必深究。因为它里面所有的内容,都会在WebUI上看到。

3)单击With defaults链接,创建一个新的抓取任务,如图10-20所示。

10-20 新的抓取任务

4)在新建任务的名称上,填入“Sohu_news,表示该抓取任务将抓取搜狐的新闻信息。

Description中随意填入字符,然后再在seeds框中,填入搜狐新闻的网址。

这里需要解释一下seeds的含义。所谓seeds,其实指的是抓取任务的起始点。每次的抓取,

总是需要从一个起始点开始,在得到这个起始点网页上的信息后,分析出新的地址加入抓取

队列中,然后循环抓取,重复这样的过程,直到所有链接都分析完毕。

5)在图10-20中,设置了搜狐新闻的首页为种子页面,以此做为起始点。用户在使用时,

也可以同时输入多个种子,每个URL地址单独写在一行上,如图10-21所示。

10-21 多个种子的情况

当然,凭着目前的设置,还没法开始抓取网页,还需要对这个任务进行详细的设置。

10.1.4 设置抓取时的处理链

在图10-21中,seeds文本框下有一排按钮,单击“Modules”按钮,就进入了配置抓取时的

处理链的页面,如图10-22所示。

10-22 配置处理链的页面

从上而下,可以看到,需要配置的内容共有7项,其中CrawlScopeFrontier是两个最重

要的组件。

CrawlScope用于配置当前应该在什么范围内抓取网页链接。比如,如果选择BroadScope

则表示当前抓取的范围不受限制,但如果选择了HostScope,则表示抓取的范围在当前的

Host内。

从笔者的经验看来,在抓取时,无论是HostScopePathScope都不能真正的限制到抓取的

内容。需要对Scope内的代码进行一定的修改才可以,因此,暂时选择BroadScope来充当

示例中的范围限定,其实也就是对范围不做任何的限定。即从开始,抓取任

何可以抓取到的信息。如图10-23所示。

10-23 设置Scope

Frontier则是一个URL的处理器,它将决定下一个被处理的URL是什么。同时,它还会将

经由处理器链所解析出来的URL加入到等待处理的队列中去。在例子中,使用BdbFrontier

类来做为处理器,全权掌管URL的分配。如图10-24所示。

10-24 设置Frontier

除了这两个组件外,还有5个队列要配。这五个队列根据先后的顺序,就依次组成了Heritrix

的整个处理器链。5个队列的含义分别如下:

1PreProcessor这个队列中,所有的处理器都是用来对抓取时的一些先决条件做判断的。

比如判断的信息等,它是整个处理器链的入口。如图10-25所示。

10-25 设置PreProcessor

2Fetcher:从名称上看,它用于解析网络传输协议,比如解析DNSHTTPFTP等。

在演示中,主要使用FetchDNSFetchHTTP两个类。如图10-26所示。

10-26 设置Fetcher

3Extractor:它的名字就很好的揭示了它的作用。它主要用是于解析当前获取到的服务

器返回内容,这些内容通常是以字符串形式缓存的。在这个队列中,包括了一系列的工具,

如解析HTMLCSS等。在解析完毕,取出页面中的URL后,将它们放入队列中,等待下

次继续抓取。在演示中,使用两种ExtractorExtractorHTTPExtractorHTML如图10-27

所示。

10-27 设置Extractor

4Writer:主要是用于将所抓取到的信息写入磁盘。通常写入磁盘时有两种形式,一种

是采用压缩的方式写入,在这里被称为Arc方式,另一种则采用镜象方式写入。当然处理起

来,镜象方式要更为容易一些,因此,在演示中命名用镜象Mirror方式。如图10-28所示。

10-28 设置Writer

5PostProcessor在整个抓取解析过程结束后,进行一些扫尾的工作,比如将前面Extractor

解析出来的URL有条件的加入到待处理队列中去。如图10-29所示。

10-29 设置PostProcessor

值得一提的是,在处理器链的设置过程中,每一个队列中的处理器都是要分先后顺序的,

息的处理流程实际上是不可逆的,因此,在设置时,可以看见在队列的右侧总是有“Up

Down”和“Remove”这样的操作,以帮助能够正确的设置其顺序。

在设置完Hertrix所需的处理链后,仍然还不能够马上开始抓取任务,还需对默认的运行时

参数做一些修改,以适应真正的需要。

10.1.5 设置运行时的参数

在设置完处理链后,在页面顶部或底部都可以找到如图10-30所示的菜单项,单击Settings

链接,就进入了属性设置的页面,如图10-30所示。

10-30 进入“Settings

在属性设置页面上有非常多的输入域,Heritrix在抓取网页时,这些域是用来对的各个组件

的值进行预设,如图10-31所示。

10-31 属性配置页面

由于页面上的内容非常多,使用者可能无法全部了解它们的作用。所以Heritrix提供了一个

辅助功能,来在最大程度上让使用者了解每个参数的含义。如图10-32所示。

10-32 属性提示

可以看到,在每个属性的右侧都有一个小问号,当单击问号时,就会弹出一个Javascript

Alert提示框,上面介绍了当前属性的作用。例如,在上图中单击“max-bytes-download”属

性,通过Alert的提示可以知道,它表示的是抓取器最大下载的字节数,当下载字节数超过

这个属性上所设定的值时,抓取就会自动停止。另外,如果将这个值设为0,则表示没有限

制。

事实上,当在第一次使用Heritrix时,所需要设置的参数并不多,以默认设置为主。以下就

来介绍一些必须要在第一次使用时就要配置好的参数。

1max-toe-threads

该参数的含义很容易了解,它表示Heritrix在运行该抓取任务时,为任务分配多少个线程进

行同步抓取。该参数的默认值为100,而事实上根据笔者的经验,在机器配置和网络均很好

的情况下,设置50个线程数就已经足够使用了。

2HTTP-Header

HTTP-Header这个属性域下面,包括两个属性值“user-agent”和“from。默认情况下,

这两个属性的值如图10-33所示。

10-33 默认的情况

很明显,这样的值是无法完成真实的HTTP协议的模拟的,所以,必须要将值改掉。10-34

是笔者机器上的一种配置,读者可以借鉴。

10-34 一种正确的配置

z @VERSION@”字符串需要被替换成Heritrix的版本信息。

z PROJECT_URL_HERE”可以被替换成任何一个完整的URL地址。

z from”属性中不需要设置真实的E-mail地址,只需是格式正确的邮件地址就可以

了。

当正确设置了上述的两个属性后,Heritrix就具备了运行的条件。单击“Submit”链接,提

交这个抓取任务,如图10-35所示。

10-35 提交任务“Submit job

10.1.6 运行抓取任务

1)当单击“Submit job”链接后,会看到图10-36所示的页面。图中最上方很清楚的显示

Job created这表示刚才所设置的抓取任务已经被成功的建立。同时,在下面的Pending

Jobs”一栏,可以清楚的看到刚刚被创建的Job,它的状态目前为“Pending

10-36 Job提交后的页面

2)下面启动这个任务。回到“Console”界面上,可以看到,如图10-37所示,刚刚创建

的任务已经显示了出来,等待我们开始它。

10-37 Job提交后的Console界面

3)在面版的右测,它显示了当前Java虚拟机的一些状态,如图10-38所示,可以看到当

前的堆大小为4184KB而已经被使用了3806KB,另外,最大的堆内容可以达到65088KB

也就是在64M左右。

10-38 内存状态显示

4)此时,单击面版中的“Start”链接,就会将此时处于“Pending”状态的抓取任务激活,

令其开始抓取

5)在图10-39中,刚才还处于“Start”状态的链接已经变为了Hold状态。这表明,抓取

任务已经被激活。

10-39 抓取开始

6)此时,面版中出现了一条抓取状态栏,它清楚的显示了当前已经被抓取的链接数量,

另外还有在队列中等待被抓取的链接数量,然后用一个百分比显示出来。

7)在绿红相间的长条左侧,是几个实时的运行状态,其中包括抓取的平均速度(KB/s

和每秒钟抓取的链接数(URIs/sec,另外的统计还包括抓取任务所消耗的时间和剩余的时

间,不过这种剩余时间一般都不准,因为URI的数量总是在不断变化,每当分析一个网页,

就会有新的URI加入队列中。如图10-40所示。

10-40 抓取的速度和时间

8)在绿红相间的长条右侧,是当前的负载,它显示了当前活跃的线程数量,同时,还统

计了Heritrix内部的所有队列的平均长度。如图10-41所示。

10-41 线程和队列负载

9)从图10-40和图10-41中看到,真正的抓取任务还没有开始,队列中的总URI数量,

以及下载的速率都还基本为0。这应该还处于接收种子URL的网页信息的阶段。让我们再

来看一下当Heritrix运行一段时间后,整个系统的资源消耗和进度情况。

10在图10-42中,清楚的看到系统的资源消耗。其中,每秒下载的速率已经达到了23KB

另外,平均每秒有19.3URI被抓取。在负载方面,初设的50个线程均处于工作状态,最

长的队列长度已经达到了415URI,平均长度为5。从进度条上看,总共有3771URI

等待抓取,已经完成了718URI的抓取,另外,下载的字节总数也已经达到了1390KB

再观察一下左边,仅用时32s。可见,多线程抓取的速度还是很快的。

10-42 系统运行一段时间后的情况

11)不过,当抓取继续进行时,观察Java虚拟机的内存使用,发现其已达饱合状态。64M

的最大Heap显然不够用。如图10-43所示。

10-43 Java虚拟机的内存使用

12)由于这仅是一次演示,可以忽略内存的影响。但在真正的开发过程中,使用Heritrix

时,至少应为其分配512M的最大HeapSize,也就是在启动它时,应该设置-Xmx512m这个

属性。在使用命令行方式启动Heritrix的脚本中,笔者已经为其加入了该参数,而如果要在

使用Eclipse启动Heritrix时也设置该参数,具体的设置方法如图10-44所示。

10-44 Eclipse中加入启动参数

13)按图10-44所示,输入Java虚拟机的参数,就可以增大Heritrix的最大可用内存。如

10-45是使用了-Xmx512m参数后的Console界面。

10-45 使用了512mHeapSize

在运行的过程中,值得注意的一点是,进度条的百分比数量并不是准确的。因为这个百分比

实际上是已经处理的链接数和总共分析出的链接数的比值。当页面在不断被抓取分析时,

接的数量也会不断的增加,因此,这个百分比的数字也在不断的变化。例如如图10-46所示,

此时总共抓取到的链接数已经达到了12280个,处理了799个,它的百分比数量为6%,这

显然比图10-42或图10-39中的要小。

10-46 抓取了799的链接

读者可能已经发现,在Heritrix中,大量的链接被称为URI。从理论上说,URL应该是一个

完整的地址,而URI应该是去除协议、主机和端口后剩余的部分。Heritrix中可能有一定程

度的混淆,希望读者不要对此感到奇怪。

至此,已经把Heritrix成功的运行起来,并且抓取了一定的内容。接下来,看一下它是如何

存储抓取下来的信息的。

10.1.7 Heritrix的镜象存储结构

由于在前面设置了Writer的类型为MirrorWriter。因此,磁盘上应该留有了所抓取到的网页

的各种镜象。那么,究竟Heritrix是如何存储下镜象信息的呢?

打开Eclipseworkspace目录,进入heritrixProject的工程,里面有一个jobs目录。进入后,

找到以刚才job的名称打头的文件夹,这里面的内容,就是Heritrix在运行时实时生成的。

其中,有一个mirror目录,进入后,如图10-47所示。

10-47 mirror目录下的内容

其实所谓镜象方式存储,就是将URL地址按“/”进行切分,进而按切分出来的层次存储,

比如一个URL地址为:

/

那么它在mirror目录中的保存位置就该是目录下的文件。为了验

证这一说法的准确性,打开目录,可以看到图10-48

10-48 镜象示例

果然,文件就在这个目录下。另外,Heritrix也同样将各种图片或脚本信息按路径

进行了保存,例如,在目录下有一个images目录,其中就保存了URL地址

/images/这样的图片信息。如图10-49所示。

10-49 抓取下来的图片文件

10.1.8 终止抓取或终止Heritrix的运行

当用户进行某个抓取任务时,有两种方法会让任务停止下来。

1.正常终止

第一种方法当然就是任务的自然结束,其条件为所有队列中的URI都已经被处理过了。此

时,任务将自然终止。在“Jobs”面版上会看到任务已经完成,被加入到“Completed jobs

列表中。

2.强行终止

当然,任务不可能总是运行完,这可能是因为对任务的控制不够,结果抓取了太多不相关的

信息,进而造成URL队列无限制膨胀,无法终止。在这种情况下,就需要强行将任务终止。

Console面版上有如图10-50所示的一排链接,最后一个“Terminate”链接,就是用来终

止当前运行的任务。

10-50 终止任务的运行

单击Terminate链接后,当前在运行的抓取任务就会立即终止,并同样将任务放置到Jobs

面版上的“Completed jobs”列表中,只不过在“status”上,它会显示“Finished - Ended by

operator”这样的提示。

当然,如果用户希望关闭Heritrix,并终止所有正在运行的任务,也可以单击Console面版

上的“Shutdown Heritrix software”的链接,此时,Heritrix会弹出一个警告,告诉你如果关

Heritrix,则当前一切正在运行的任务都将被终止。如图10-51所示。

10-51 关闭前的提示

如果选择“I’m sure, shut it down,则HeritrixWebUI将会终止,虚拟机进程结束。

10.2 Heritrix的架构

在上一节中,详细介绍了Heritrix的使用入门。读者通过上一节的介绍,应该已经能够使用

Heritrix来进行简单的网页抓取了。那么,Heritrix的内容究竟是如何工作的呢?它的设计方

面有什么突出之处?

本节就将介绍Heritrix的几个主要组件,以此让读者了解其主要架构和工作方式。为后续的

扩展Heritrix做一些铺垫。

10.2.1 抓取任务CrawlOrder

之所以选择从CrawlOrder这个类说起,是因为它是整个抓取工作的起点。在上一节中已经

说过,一次抓取任务包括许多的属性,建立一个任务的方式有很多种,最简单的一种就是根

据默认的来配置。在内存中,order使用CrawlOrder这个类来进行表示。看一下

API文档中CrawlOrder的继承关系图,如图10-52所示。

10-52 CrawlOrder类的继承关系图

从继承关系图中可以看到,CrawlOrder继承自一系列的与属性设置相关的基类。另外,它的

最顶层基类是ute这是一个JMX中的类,它可以动态的反映出Java

容器内某个MBean的属性变化。关于这一部分的内容不是我们所要讨论的重点,只需知道,

CrawlOrder中的属性,是需要被随时读取和监测的。

那么究竟使用什么工具来读取文件中的各种属性呢。另外,一个CrawlOrder的对

Heritrix

gsXMLSettingsHandler

public XMLSettingsHandler(File orderFile) throws

InvalidAttributeValueException

XMLSettingsHandler的构造函数中,其所传入的参数orderFile正是一个经过对象封装的

File。这样,就可以直接调用其构造函数,来创建一个XMLSettingsHandler

实例,以此做为一个读取的工具。

当一个XMLSettingsHandler的实例被创建后,可以通过getOrder()方法来获取CrawlOrder

的实例,这样也就可以进行下一步的工作了。

10.2.2 中央控制器CrawlController

中央控制器是一次抓取任务中的核心组件。它将决定整个抓取任务的开始和结束。

CrawlController位于ork中,在它的Field声明中,看到如下代码

片段。

代码10.2

// key subcomponents which define and implement a crawl in progress

private transient CrawlOrder order;

private transient CrawlScope scope;

private transient ProcessorChainList processorChains;

private transient Frontier frontier;

private transient ToePool toePool;

private transient ServerCache serverCache;

// This gets passed into the initialize method.

private transient SettingsHandler settingsHandler;

可以看到,在CrawlController类中,定义了以下几个组件:

z CrawlOrder:这就不用说了,因为一个抓取工作必须要有一个Order对象,它保存了

对该次抓取任务中,的属性配置。

z CrawlScope:在10.1.4节中已经介绍过了,这是决定当前的抓取范围的一个组件。

z ProcessorChainList:从名称上很明显就能看出,它表示了处理器链,在这个列表中

的每一项都可以和10.1.4节中所介绍的处理器链对应上。

z Frontier:很明显,一次抓取任务需要设定一个Frontier,以此来不断为其每个线程

提供URI

z ToePool:这是一个线程池,它管理了所有该抓取任务所创建的子线程。

z ServerCache:这是一个缓存,它保存了所有在当前任务中,抓取过的Host名称和

Server名称。

以上组件应该是一次正常的抓取过程中所必需的几项,它们各自的任务很独立,分工明确,

但在后台中,它们之间却有着千丝万缕的联系,彼此互相做为构造函数或初始化的参数传入。

那么,究竟该如何获得CrawlController的实例,并且通过自主的编程来使用Heritrix提供的

API进行一次抓任务呢?

事实上CrawlController有一个不带参数的构造函数,开发者可以直接通过它的构造函数来

构造一个CrawlController的实例。但是值得注意的一点,在构造一个实例并进行抓取任务

时,有几个步骤需要完成:

1)首先构造一个XMLSettingsHandler对象,将内的属性信息装入。

2)调用CrawlController的构造函数,构造一个CrawlController的实例。

3)调用CrawlControllerintialize(SettingsHandler)方法,初始化CrawlController实例。

其中,传入的参数是在第一步是构造的XMLSettingsHandler实例。

4)当上述3步完成后,CrawlController就已经具备运行的条件,可以开始运行了。此时,

只需调用它的requestCrawlStart()方法,就可以启运线程池和Frontier,然后就可以开始不断

的抓取网页了。

上述过程可以用图10-53所示。

10-53 使用CrawlController启运抓取任务

CrawlControllerinitialize()方法中,Heritrix主要做了以下几件事:

1)从XMLSettingsHandler中取出Order

2)检查了用户设定的UserAgent等信息,看是否符合格式。

3)设定了开始抓取后保存文件信息的目录结构。

4)初始化了日志信息的记录工具。

5)初始化了使用Berkley DB的一些工具。

6)初始化了ScopeFrontier以及ProcessorChain

7)最后实例化了线程池。

在正常情况下,以上顺序不能够被随意变动,因为后一项功能的初始化很有可能需要前几项

功能初始化的结果。例如线程池的初始化,必须要在先有了Frontier的实例的基础上来进行。

读者可能对其中的Berkeley DB感到费解,在后面的小节将详细说明。

从图10-53中看到,最终启动抓取工作的是requestCrawlStart()方法。其代码如下。

代码10.3

public void requestCrawlStart() {

// 初始化处理器链

runProcessorInitialTasks();

// 设置一下抓取状态的改变,以便能够激发一些Listeners

// 来处理相应的事件

sendCrawlStateChangeEvent(STARTED, _PENDING);

String jobState;

state = RUNNING;

jobState = _RUNNING;

sendCrawlStateChangeEvent(, jobState);

// A proper exit will change this value.

= _FINISHED_ABNORMAL;

// 开始日志线程

Thread statLogger = new Thread(statistics);

e("StatLogger");

();

// 启运Frontier,抓取工作开始

();

}

可以看到,启动抓取工作的核心就是要启动Frontier(通过调用其start()方法),以便能够开

始向线程池中的工作线程提供URI,供它们抓取。

下面的代码就是BdbFrontier的父类AbstractFrontier中的start()方法和unpause()方法:

代码10.4

public void start() {

if (((Boolean)getUncheckedAttribute(null, ATTR_PAUSE_AT_START))

.booleanValue()) {

// 若配置文件中不允许该次抓取开始

// 则停止

tCrawlPause();

} else {

// 若允许开始,则开始

unpause();

}

}

synchronized public void unpause() {

// 去除当前阻塞变量

shouldPause = false;

// 唤醒所有阻塞线程,开始抓取任务

notifyAll();

}

start()方法中,首先判断配置中的属性是否允许当前线程开始。若不允许,则令controller

停止抓取。若允许开始,则简单的调用unpause()方法。unpause()方法更为简单,它首先将

阻塞线程的信号量设为false,即允许线程开始活动,然后通过notifyAll()方法,唤醒线程池

中所有被阻塞的线程,开始抓取。

10.2.3 Frontier链接制造工厂

Frontier在英文中的意思是“前线,领域”,在Heritrix中,它表示一种为线程提供链接的工

具。它通过一些特定的算法来决定哪个链接将接下来被送入处理器链中,同时,它本身也负

责一定的日志和状态报告功能。

事实上,要写出一个合格并且真正能够使用的Frontier绝非一件简单的事情,尽管有了

Frontier接口,其中的方法约束了Frontier的行为,也给编码带来了一定的指示。但是其中

还存在着很多问题,需要很好的设计和处理才可以解决。

Heritrix的官方文档上,有一个Frontier的例子,在此拿出来进行一下讲解,以此来向读

者说明一个最简单的Frontier都能够做什么事。以下就是这个Frontier的代码。

代码10.5

public class MyFrontier extends ModuleType implements Frontier,

FetchStatusCodes {

// 列表中保存了还未被抓取的链接

List pendingURIs = new ArrayList();

// 这个列表中保存了一系列的链接,它们的优先级

// 要高于pendingURIs那个List中的任何一个链接

// 表中的链接表示一些需要被满足的先决条件

List prerequisites = new ArrayList();

// 一个HashMap,用于存储那些已经抓取过的链接

Map alreadyIncluded = new HashMap();

// CrawlController对象

CrawlController controller;

// 用于标识是否一个链接正在被处理

boolean uriInProcess = false;

// 成功下载的数量

long successCount = 0;

// 失败的数量

long failedCount = 0;

// 抛弃掉链接的数量

long disregardedCount = 0;

// 总共下载的字节数

long totalProcessedBytes = 0;

// 构造函数

public MyFrontier(String name) {

super(_NAME, "A simple frontier.");

}

// 初始化,参数为一个CrawlController

public void initialize(CrawlController controller)

throws FatalConfigurationException, IOException {

// 注入

ller = controller;

// 把种子文件中的链接加入到pendingURIs中去

pe().refreshSeeds();

List seeds = pe().getSeedlist();

synchronized(seeds) {

for (Iterator i = or(); t();) {

UURI u = (UURI) ();

CandidateURI caUri = new CandidateURI(u);

d();

schedule(caUri);

}

}

public synchronized void schedule(CandidateURI caURI) {

/*

* 首先判断要加入的链接是否已经被抓取过

* 如果已经包含在alreadyIncluded这个HashMap

* 则说明处理过了,即可以放弃处理

*/

if (!nsKey(String())) {

if(mmediateScheduling()) {

(caURI);

} else {

(caURI);

}

// HashMap中使用url的字符串来做为key

// 而将实际的CadidateURI对象做为value

(String(), caURI);

}

}

public void batchSchedule(CandidateURI caURI) {

schedule(caURI);

}

public void batchFlush() {

}

// 一次抓取结束后所执行的操作,该操作由线程池

// 中的线程来进行调用

public synchronized void finished(CrawlURI cURI) {

awledURIFailureEvent(cURI);

failedCount++;

oMinimal();

}

singCleanup();

}

// 返回所有已经处理过的链接数量

public long discoveredUriCount() {

return ();

}

}

public long deleteURIs(String match) {

return 0;

}

}

Frontier中,根据笔者给出的中文注释,相信读者已经能够了解这个Frontier中的大部分

玄机。以下给出详细的解释。

首先,Frontier是用来向线程提供链接的,因此,在上面的代码中,使用了两个ArrayList

来保存链接。其中,第一个pendingURIs保存的是等待处理的链接,第二个prerequisites

保存的也是链接,只不过它里面的每个链接的优先级都要高于pendingURIs里的链接。通常,

prerequisites中保存的都是如DNS之类的链接,只有当这些链接被首先解析后,其后续

的链接才能够被解析。

除了这两个ArrayList外,在上面的Frontier还有一个名称为alreadyIncludedHashMap

它用于记录那些已经被处理过的链接。每当调用Frontierschedule()方法来加入一个新的

链接时,Frontier总要先检查这个正要加入到队列中的链接是不是已经被处理过了。

很显然,在分析网页的时候,会出现大量相同的链接,如果没有这种检查,很有可能造成抓

取任务永远无法完成的情况。同时,在schedule()方法中还加入了一些逻辑,用于判断当前

要进入队列的链接是否属于需要优先处理的,如果是,则置入prerequisites队列中,否则,

就简单的加入pendingURIs中即可。

注意:Frontier中还有两个关键的方法,next()finished(),这两个方法都是要交由抓取的线程来完成的。

Next()方法的主要功能是:从等待队列中取出一个链接并返回,然后抓取线程会在它自己的run()方法中完

成对这个链接的处理。而finished()方法则是在线程完成对链接的抓取和后续的一切动作后(如将链接传递

经过处理器链)要执行的。它把整个处理过程中解析出的新的链接加入队列中,并且在处理完当前链接后,

将之加入alreadyIncluded这个HashMap中去。

需要读者记住的是,这仅仅是一个最基础的代码,它有很多的功能缺失和性能问题,甚至可

能出现重大的同步问题。不过尽管如此,它应当也起到了抛砖引玉的作用,能够从结构上揭

示了一个Frontier的作用。

10.2.4 Berkeley DB实现的BdbFrontier

简单的说,Berkeley DB就是一个HashTable,它能够按“key/value”方式来保存数据。它是

由美国Sleepycat公司开发的一套开放源代码的嵌入式数据库,它为应用程序提供可伸缩的、

高性能的、有事务保护功能的数据管理服务。

那么,为什么不使用一个传统的关系型数据库呢?这是因为当使用BerkeleyDB时,数据库

和应用程序在相同的地址空间中运行,所以数据库操作不需要进程间的通讯。然而,当使用

传统关系型数据库时,就需要在一台机器的不同进程间或在网络中不同机器间进行进程通

讯,这样所花费的开销,要远远大于函数调用的开销。

另外,Berkeley DB中的所有操作都使用一组API接口。因此,不需要对某种查询语言(比

SQL)进行解析,也不用生成执行计划,这就大大提高了运行效率。

当然,做为一个数据库,最重要的功能就是事务的支持,Berkeley DB中的事务子系统就是

用来为其提供事务支持的。它允许把一组对数据库的修改看作一个原子单位,这组操作要么

全做,要么全不做。在默认的情况下,系统将提供严格的ACID事务属性,但是应用程序可

以选择不使用系统所作的隔离保证。该子系统使用两段锁技术和先写日志策略来保证数据的

正确性和一致性。这种事务的支持就要比简单的HashTable中的Synchronize要更加强大。

注意:Heritrix中,使用的是Berkeley DBJava版本,这种版本专门为Java语言做了优化,提供了Java

API接口以供开发者使用。

为什么Heritrix中要用到Berkeley DB呢?这就需要再回过头来看一下Frontier了。

在上一小节中,当一个链接被处理后,也即经过处理器链后,会生成很多新的链接,这些新

的链接需要被Frontier的一个schedule方法加入到队列中继续处理。但是,在将这些新链接

加入到队列之前,要首先做一个检查,即在alreadyIncluded这个HashMap中,查看当前要

加入到队列中的链接是否在先前已经被处理过了。

当使用HashMap来存储那些已经被处理过的链接时,HashMap中的keyurl,而value

为一个对url封装后的对象。很显然的,这里有几个问题。

z 对这个HashMap的读取是多线程的,因为每个线程都需要访问这个HashMap,以决

定当前要加入链接是否已经存在过了。

z 对这个HashMap的写入是多线程的,每个线程在处理完毕后,都会访问这个

HashMap,以写入最新处理的链接。

z 这个HashMap的容量可能很大,可以试想,一次在广域网范围上的网页抓取,可能

会涉及到上十亿个URL地址,这种地址包括网页、图片、文件、多媒体对象等,所

以,不可能将这么大一张表完全的置放于内存中。

综合考虑以上3点,仅用一个HashMap来保存所有的链接,显然已经不能满足“大数据量,

多并发”这样的要求。因此,需要寻找一个替代的工具来解决问题。Heritrix中的BdbFrontier

就采用了Berkeley DB来解决这种URL存放的问题。事实上,BdbFrontier就是Berkeley DB

Frontier的简称。

为了在BdbFrontier中使用Berkeley DBHeritrix本身构造了一系列的类来帮助实现这个功

能。这些类如下:

z BdbFrontier

z BdbMultipleWorkQueues

z BdbWorkQueue

z BdbUriUniqFilter

上述的4个类,都以Bdb3个字母开头,这表明它们都是使用到了Berkeley DB的功能。其

中:

1BdbMultipleWorkQueues代表了一组链接队列,这些队列有各自不同的key。这样,由

Key和链接队列可以形成一个“Key/Value”对,也就成为了Berkeley DB里的一条记录

DatabaseEntry)如图10-54所示。

10-54 BdbMultipleWorkQueues示意

10-54清楚的显示了Berkeley DB中的key/value形式。可以说,这就是一张Berkeley DB

的数据库表。其中,数据库的一条记录包含两个部分,左边是一个由右边的所有URL链接

计算出来的公共键值,右边则是一个URL的队列。

2BdbWorkQueue代表了一个基于Berkeley DB的队列,与BdbMutipleWorkQueues所不

同的是,该队列中的所有的链接都具有相同的键值。事实上,BdbWorkQueue只是对

BdbMultipleWorkQueues的封装,在构造一个BdbWorkQueue时,需传入一个健值,以此做

为该Queue在数据库中的标识。事实上,在工作线程从Frontier中取出链接时,Heritrix

是先取出整个BdbWorkQueue,再从中取出第一个链接,然后将当前这个BdbWorkQueue

入一个线程安全的同步容器内,等待线程处理完毕后才将该Queue释放,以便该Queue

的其他URI可以继续被处理。

3BdbUriUniqFilter是一个过滤器,从名称上就能知道,它是专门用来过滤当前要进入等

待队列的链接对象是否已经被抓取过。很显然,BdbUriUniqFilter内部嵌入了一个Berkeley

DB数据库用于存储所有的被抓取过的链接。它对外提供了

public void add(String key, CandidateURI value)

这样的接口,以供Frontier调用。当然,若是参数的CandidateURI已经存在于数据库中了,

则该方法会禁止它加入到等待队列中去。

4BdbFrontier就是Heritrix中使用了Berkeley DB的链接制造工厂。它主要使用

BdbUriUniqFilter,做为其判断当前要进入等待队列的链接对象是否已经被抓取过。同时,

它还使用了BdbMultipleWorkQueues来做为所有等待处理的URI的容器。这些URI根据各

自的内容会生成一个Hash值成为它们所在队列的键值。

Heritrix1.10的版本中,可以说BdbFrontier是惟一一个具有实用意义的链接制造工厂了。

虽然Heritrix还提供了另外两个Frontier

SensitiveFrontier

veRevisitFrontier

但是,DomainSensitiveFrontier已经被废弃不再推荐使用了。而AdaptiveRevisitFrontier的算

法是不管遇到什么新链接,都义无反顾的再次抓取,这显然是一种很落后的算法。因此,了

BdbFrontier的实现原理,对于更好的了解Heritrix对链接的处理有实际意义。

BdbFrontier的代码相对比较复杂,笔者在这里也只能简单将其轮廓进行介绍,读者仍须将

代码仔细研读,方能把文中的点点知识串联起来,进而更好的理解Heritrix作者们的巧妙匠

心。

10.2.5 Heritrix的多线程ToeThreadToePool

想要更有效更快速的抓取网页内容,则必须采用多线程。Heritrix中提供了一个标准的线程

ToePool,它用于管理所有的抓取线程。

ToePoolToeThread都位于ork包中。前面已经说过,ToePool

初始化,是在CrawlControllerinitialize()方法中完成的。来看一下ToePool以及ToeThread

是如何被初始化的。以下代码是在CrawlController中用于对ToePool进行初始化的。

// 构造函数

toePool = new ToePool(this);

// 中的配置,实例化并启动线程

e(Toes());

ToePool的构造函数很简单,如下所示:

public ToePool(CrawlController c) {

super("ToeThreads");

ller = c;

}

它仅仅是调用了父类Group的构造函数,同时,将注入的CrawlController

给类变量。这样,便建立起了一个线程池的实例了。但是,那些真正的工作线程又是如何建

立的呢?

下面来看一下线程池中的setSize(int)方法。从名称上看,这个方法很像是一个普通的赋值方

法,但实际上,它并不是那么简单。

代码10.6

public void setSize(int newsize)

{

targetSize = newsize;

int difference = newsize - getToeCount();

// 如果发现线程池中的实际线程数量小于应有的数量

// 则启动新的线程

if (difference > 0) {

for(int i = 1; i <= difference; i++) {

// 启动新线程

startNewThread();

}

}

// 如果线程池中的线程数量已经达到需要

else

{

int retainedToes = targetSize;

// 将线程池中的线程管理起来放入数组中

Thread[] toes = s();

// 循环去除多余的线程

for (int i = 0; i < ; i++) {

if(!(toes[i] instanceof ToeThread)) {

continue;

}

retainedToes--;

if (retainedToes>=0) {

continue;

}

ToeThread tt = (ToeThread)toes[i];

();

}

}

}

// 用于取得所有属于当前线程池的线程

private Thread[] getToes()

{

Thread[] toes = new Thread[activeCount()+10];

// 由于ToePool继承自Group

// 因此当调用enumerate(Thread[] toes)方法时,

// 实际上是将所有该ThreadGroup中开辟的线程放入

// toes这个数组中,以备后面的管理

ate(toes);

return toes;

}

// 开启一个新线程

private synchronized void startNewThread()

{

ToeThread newThread = new ToeThread(this, nextSerialNumber++);

ority(DEFAULT_TOE_PRIORITY);

();

}

通过上面的代码可以得出这样的结论:线程池本身在创建的时候,并没有任何活动的线程实

例,只有当它的setSize方法被调用时,才有可能创建新线程;如果当setSize方法被调用多

次而传入不同的参数时,线程池会根据参数里所设定的值的大小,来决定池中所管理线程数

量的增减。

当线程被启动后,所执行的是其run()方法中的片段。接下来,看一个ToeThread到底是如

何处理从Frontier中获得的链接的。

代码10.7

public void run()

{

String name = er().getCrawlOrderName();

(getName()+" started for order '"+name+"'");

try {

while ( true )

{

// 检查是否应该继续处理

continueCheck();

setStep(STEP_ABOUT_TO_GET_URI);

// 使用Frontiernext方法从Frontier

// 取出下一个要处理的链接

CrawlURI curi = ntier().next();

// 同步当前线程

synchronized(this) {

continueCheck();

setCurrentCuri(curi);

}

/*

* 处理取出的链接

*/

processCrawlUri();

setStep(STEP_ABOUT_TO_RETURN_URI);

// 检查是否应该继续处理

continueCheck();

// 使用Frontierfinished()方法

// 来对刚才处理的链接做收尾工作

// 比如将分析得到的新的链接加入

// 到等待队列中去

synchronized(this) {

ntier().finished(currentCuri);

setCurrentCuri(null);

}

// 后续的处理

setStep(STEP_FINISHING_PROCESS);

lastFinishTime = tTimeMillis();

// 释放链接

eContinuePermission();

if(shouldRetire) {

break; // from while(true)

}

}

} catch (EndedException e) {

} catch (Exception e) {

(,"Fatal exception in "+getName(),e);

} catch (OutOfMemoryError err) {

seriousError(err);

} finally {

eContinuePermission();

}

setCurrentCuri(null);

// 清理缓存数据

ecorders();

corder = null;

localProcessors = null;

(getName()+" finished for order '"+name+"'");

setStep(STEP_FINISHED);

ed();

controller = null;

}

在上面的方法中,很清楚的显示了工作线程是如何从Frontier中取得下一个待处理的链接,

然后对链接进行处理,并调用Frontierfinished方法来收尾、释放链接,最后清理缓存、

终止单步工作等。另外,其中还有一些日志操作,主要是为了记录每次抓取的各种状态。

很显然,以上代码中,最重要的一行语句是processCrawlUri(),它是真正调用处理链来对链

接进行处理的代码。其中的内容,放在下一个小节中介绍。

10.2.6 处理链和Processor

1Processor

该类代表着单个的处理器,所有的处理器都是它的子类。在Processor类中有一个process()

方法,它被标识为final类型的,也就是说,它不可以被它的子类所覆盖。代码如下。

代码10.8

public final void process(CrawlURI curi) throws InterruptedException

{

// 设置下一个处理器

tProcessor(getDefaultNextProcessor(curi));

try

{

// 判断当前这个处理器是否为enabled

if (!((Boolean) getAttribute(ATTR_ENABLED, curi)).booleanValue()) {

return;

}

} catch (AttributeNotFoundException e) {

(sage());

}

// 如果当前的链接能够通过过滤器

// 则调用innerProcess(curi)方法

// 来进行处理

if(filtersAccept(curi)) {

innerProcess(curi);

}

// 如果不能通过过滤器检查,则调

// innerRejectProcess(curi)来处理

else

{

innerRejectProcess(curi);

}

}

方法的含义很简单。即首先检查是否允许这个处理器处理该链接,如果允许,则检查当前处

innerProcess(curi)方法来处理,如果过滤器的检查没有通过,就使用innerRejectProcess(curi)

方法处理。

其中innerProcess(curi)innerRejectProcess(curi)方法都是protected类型的,且本身没有实现

任何内容。很明显它们是留在子类中,实现具体的处理逻辑。不过大部分的子类都不会重写

innerRejectProcess(curi)方法了,这是因为反正一个链接已经被当前处理器拒绝处理了,就不

用再有什么逻辑了,直接跳到下一个处理器继续处理就行了。

2ProcessorChain

该类表示一个队列,里面包括了同种类型的几个Processor。例如,可以将一组的Extractor

加入到同一个ProcessorChain中去。

在一个ProcessorChain中,有3private类型的类变量:

private final MapType processorMap;

private ProcessorChain nextChain;

private Processor firstProcessor;

其中,processorMap中存放的是当前这个ProcessorChain中所有的ProcessornextChain

类型是ProcessorChain,它表示指向下一个处理器链的指针。而firstProcessor则是指向当前

队列中的第一个处理器的指针。

3ProcessorChainList

从名称上看,它保存了Heritrix一次抓取任务中所设定的所有处理器链,将之做为一个列表。

正常情况下,一个ProcessorChainList中,应该包括有5ProcessorChain分别为PreProcessor

链、Fetcher链、Extractor链、Writer链和PostProcessor链,而每个链中又包含有多个的

Processor。这样,就将整个处理器结构合理的表示了出来。

那么,在ToeThreadprocessCrawlUri()方法中,又是如何来将一个链接循环经过这样一组

结构的呢?请看下面的代码:

代码10.9

private void processCrawlUri() throws InterruptedException {

// 设定当前线程的编号

eadNumber(Number);

// 为当前处理的URI设定下一个ProcessorChain

tProcessorChain(stProcessorChain());

// 设定开始时间

lastStartTime = tTimeMillis();

try {

// 如果还有一个处理链没处理完

while (ocessorChain() != null)

{

setStep(STEP_ABOUT_TO_BEGIN_CHAIN);

// 将下个处理链中的第一个处理器设定为

// 下一个处理当前链接的处理器

tProcessor(currentCuri

.nextProcessorChain().getFirstProcessor()

);

// 将再下一个处理器链设定为当前链接的

// 下一个处理器链,因为此时已经相当于

// 把下一个处理器链置为当前处理器链了

tProcessorChain(currentCuri

.nextProcessorChain().getNextProcessorCha

in());

// 开始循环处理当前处理器链中的每一个Processor

while (ocessor() != null)

{

setStep(STEP_ABOUT_TO_BEGIN_PROCESSOR);

Processor currentProcessor =

getProcessor(ocessor());

currentProcessorName = e();

continueCheck();

// 调用Process方法

s(currentCuri);

}

}

setStep(STEP_DONE_WITH_PROCESSORS);

currentProcessorName = "";

}

catch (RuntimeExceptionWrapper e) {

// 如果是Berkeley DB的异常

if(se() == null) {

use(ail());

}

recoverableProblem(e);

} catch (AssertionError ae) {

recoverableProblem(ae);

} catch (RuntimeException e) {

recoverableProblem(e);

} catch (StackOverflowError err) {

recoverableProblem(err);

} catch (Error err) {

seriousError(err);

}

}

代码使用了双重循环来遍历整个处理器链的结构,第一重循环首先遍历所有的处理器链,

二重循环则在链内部遍历每个Processor,然后调用它的process()方法来执行处理逻辑。

10.3 扩展和定制Heritrix

在前面两节中,向读者介绍了Heritrix的启动、创建任务、抓取网页、组件结构。但是,读

者应该也可以明显的看出,如果不用Heritrix抓取和分析网页的行为进行一定的控制,它是

无法达到要求的。

Heritrix的行为进行控制,是要建立在对其架构充分了解的基础之上的,因此,本节的内

容完全是基于上一节中所讨论的基础。

10.3.1 Heritrix中添加自己的Extractor

很明显,Heritrix内嵌的Extractor并不能够很好的完成所需要的工作,这不是说它不够强大,

而是因为在解析一个网页时,常常有特定的需要。比如,可能只想抓取某种格式的链接,或

是抓取某一特定格式中的文本片断。Heritrix所提供的大众化的Extractor只能够将所有信息

全部抓取下来。在这种情况下,就无法控制Heritrix到底该抓哪些内容,不该抓哪些内容,

进而造成镜象信息太复杂,不好建立索引。

以下就使用一个实例,来讲解该如何定制和使用Extractor这个实例其实很简单,主要功能

就是抓取所有在Sohu的新闻主页上出现的新闻,并且URL格式如下所示。

/20061122/

1)分析一下这个URL可以知道,其中的主机部分是,这是搜狐新闻

的域名,20061122”应该表示的是新闻的日期,而最后的“”应该是一个

新闻的编号,该编号全部以“n”打头。

2)有了这样的分析,就可以根据URL的特点,来定出一个正则表达式,凡是当链接符合

该正则表达式,就认为它是一个潜在的值得抓取的链接,将其收藏,以待抓取。正则表达式

如下:

/[d]+/n[d]+.shtml

3)事实上所有的Extractor均继承自tor这个抽象基类,

在它的内部实现了innerProcess方法,以下便是innerProcess的实现:

代码10.10

public void innerProcess(CrawlURI curi) {

try {

/*

* 处理链接

*/

extract(curi);

} catch (NullPointerException npe) {

otation("err=" + ss().getName());

alizedError(getName(), npe, "");

(G, getName() + ": NullPointerException",

npe);

} catch (StackOverflowError soe) {

otation("err=" + ss().getName());

alizedError(getName(), soe, "");

(G, getName() + ": StackOverflowError", soe);

} catch (alfunctionError cme) {

otation("err=" + ss().getName());

alizedError(getName(), cme, "");

(G, getName() + ": CoderMalfunctionError",

cme);

}

}

这个方法中,大部分代码都用于处理在解析过程中发生的各种异常和日志写入,不过,它为

所有的Extractor定义了新的一个接口extract(CrawlURI),也就是说,所有的Extractor继承

自它后,只需实现extract方法就可以了。以下是扩展Extractor时要做的几件事:

1)写一个类,继承Extractor的基类。

2)在构造函数中,调用父类的构造函数,以形成完整的家族对象。

3)继承extract(curi)方法。

为了实现抓取首页上所有新闻的链接,所开发的Extractor的完整源代码如下

所示。

代码10.11

package my;

import ption;

import ;

import ;

import r;

import n;

import eption;

import RI;

import tor;

import ;

import CharSequence;

this(name, "Sohu News Extractor");

}

// 构造函数

public SohuNewsExtractor(String name, String description) {

super(name, description);

}

// 第一个正则式,用于匹配SOHU新闻的格式

public static final String PATTERN_SOHU_NEWS =

"/[d]+/n[d]+.shtml";

// 第二个正则式,用于匹配所有的

public static final String PATTERN_A_HREF =

"])s*>";

// 继承的方法

protected void extract(CrawlURI curi) {

// 将链接对象转为字符串

String url = ng();

/*

* 下面一段代码主要用于取得当前链接的返回 字符串,以便对内容进行分析时使用

*/

ReplayCharSequence cs = null;

try {

HttpRecorder hr = pRecorder();

if (hr == null) {

throw new IOException("Why is recorder null here?");

}

cs = layCharSequence();

} catch (IOException e) {

alizedError(e(), e,

"Failed get of replay char sequence " + ng()

+ " " + sage());

(, "Failed get of replay char sequence in

"

+ tThread().getName(), e);

// 若找到了一个链接

while (()) {

String newUrl = (2);

// 查看其是否为SOHU新闻的格式

if (s(PATTERN_SOHU_NEWS)) {

// 若是,则将链接加入到队列中

// 以备后续处理

addLinkFromString(curi, newUrl, "", K_HOP);

}

}

} catch (Exception e) {

tackTrace();

}

}

// 将链接保存记录下来,以备后续处理

private void addLinkFromString(CrawlURI curi, String uri,

CharSequence context, char hopType) {

try {

AndAddLinkRelativeToBase(uri, ng(),

hopType);

} catch (URIException e) {

if (getController() != null) {

getController().logUriError(e, I(), uri);

} else {

("Failed createAndAddLinkRelativeToBase "

+ curi + ", " + uri + ", " + context + ", "

+ hopType + ": " + e);

}

}

}

}

在上面代码的extract()方法中:

1)首先是将Fetcher所获得的链接的HTML响应取得,并转成字符串,这样,才有可能

在后面对页面中的链接做处理。

2)从页面内容中,使用正则式取出所有链接的内容。判断链接是否符合Sohu的新闻格

式,倘若符合,则调用addLinkFromString()方法,来将这个链接加入到某个队列缓存中,以

备后续的处理。

Extractor类开发完毕后,如果使用WebUI的方式启动Heritrix并让它出现在下拉选项中,

则需要修改Eclipse工程中的modules目录下的s文件,如图10-55所示。

10-55 修改s文件

打开s文件可以看到,所有在WebUI中设置处理器链时,页面上的下拉列表

中的数据都保存在了其中,为了加入我们开发的SohuNewsExtractor,只需在其中合适的位

置上加入一行,内容如下所示:

wsExtractor|SohuNewsExtractor

接下来,再次启动Heritrix,创建一个任务,进入处理器链设置的页面,就可以看到自己开

发的Extractor了,如图10-56所示。

10-56 新加入的Extractor已经在下拉菜单中显示出来

选择后,单击“Add”按钮,就可以将其加入到队列中,如图10-57所示。

10-57 已经加入到处理器队列中

需要注意的是,一定要将其置于ExtractorHTTP的后面,以保证Heritrix能够先行处理HTTP

协议中的相关内容。与加入自己定制的Extractor的过程类似,开发者们也可以定制其他几

种处理器。同样,只需要在modules目录下找到相应的.options文件,然后将类全名加入即

可。

10.3.2 定制Queue-assignment-policy两个问题

首先提出两个问题:

z 什么是Queue-assignment-policy

z 为什么要改变Queue-assignment-policy

10.2节中,向读者介绍过了Heritrix的架构。其中,讲解了Heritrix使用了Berkeley DB

来构建链接队列。这些队列被置放于BdbMultipleWorkQueues中时,总是先给予一个Key

然后将那些Key值相同的链接放在一起,成为一个队列,也就是一个Queue

这里就出现了一个问题,这个Key值到底该如何计算呢?事实上,这里也说的Key值,应

该是做为一种标识符的形式存在。也就是说,它要与URL之间有一种内在的联系。

Heritrix中,为每个队列赋上Key值的策略,也就是它的queue-assignment-policy。这就

解答了第一个问题。

在默认的情况下,Heritrix使用HostnameQueueAssignmentPolicy来解决Key值生成的问题。

仔细看一下这个策略的名称就知道,这种策略其实是以链接的Host名称为Key值来解决这

个问题的。换句话也就是说,相同Host名称的所有URL都会被置放于同一个队列中间。

这种方式在很大程度上可以解决广域网中信息抓取时队列的键值问题。但是,它对于某个单

独网站的网页抓取,就出现了很大的问题。以Sohu的新闻网页为例,其中大部分的URL

都来自于sohu网站的内部,因此,如果使用了HostnameQueueAssignmentPolicy,则会造成

有一个队列的长度非常长的情况。

Heritrix中,一个线程从一个队列中取URL链接时,总是会先从队列的头部取出第一个

链接,在这之后,这个被取出链接的队列会进入阻塞状态,直到待该链接处理完,它才会从

阻塞状态中恢复。

假如使用HostnameQueueAssignmentPolicy策略来应对抓取一个网站中内容的情况,很有可

能造成仅有一个线程在工作,而其他所有线程都在等待。这是因为那个装有绝大多数URL

链接的队列几乎会永远处于阻塞状态,因此,别的线程根本获取不到其中的URI在这种情

况下,抓取工作会进入一种类似于休眠的状态。因此,需要改变queue-assignment-policy

避免发生这种情况,这也就回答了第二个问题。

10.3.3 Queue-assignment-policy

QueueAssignmentPolicy

那么,被改变的Key值的生成方式,应该具有什么样的要求呢?从上面的叙述中可以知道,

这个Key值最重要的一点就是应该能够有效的将所有的URL散列到不同的队列中,最终能

使所有的队列的长度的方差较小,在这种情况下,才能保证工作线程的最大效率。

任何扩展queue-assignment-policy的默认实现的类,均继承自QueueAssignmentPolicy并覆写

了其getClassKey()方法,getClassKey方法的参数为一个链接对象,而我们的散列算法,正

是要根据这个链接对象来返回一个值。

具体的算法就不说了,有许多种方法可以实现的。比如使用字符串的长度等,在百度上搜索

URL散列算法,最为出名的就要算是ELFHash法了。关于它的实现,有兴趣的读者可以自

行研究。

10.3.4 扩展FrontierScheduler来抓取特定的内容

FrontierScheduler是一个PostProcessor,它的作用是将在Extractor中所分析得出的链接加入

Frontier中,以待继续处理。先来看一下FrontierSchedulerinnerProcess()方法,代码如

下。

代码10.12

protected void innerProcess(final CrawlURI curi) {

if (able()) {

(getName() + " processing " + curi);

}

// 如果当前链接的处理结果中,有一些高优

// 先级的链接要被处理

if (requisiteUri() && chStatus() == S_DEFERRED) {

handlePrerequisites(curi);

return;

}

// 对当前这个Processor进行同步

synchronized(this) {

// 从处理结果中,取出所有链接进行循环

for (final Iterator iter = Links().iterator();

t();) {

Object obj = ();

CandidateURI cauri = null;

// 转型为CandidateURI

if (obj instanceof CandidateURI) {

cauri = (CandidateURI)obj;

} else {

("Unexpected type: " + obj);

}

// 调用schedule()方法

if (cauri != null) {

schedule(cauri);

}

}

}

}

protected void schedule(CandidateURI caUri) {

// 调用Frontier中的schedule()方法

// 将传入的链接加入到等待队列中

getController().getFrontier().schedule(caUri);

}

上面的代码中,首先检查当前链接处理后的结果集中是否有一些属于高优先级的链接,如果

是,则立刻转走进行处理。如果没有,则对所有的结果集进行遍历,然后调用Frontier中的

schedule方法加入队列进行处理。

在这里,innerProcess()中并未立刻使用Frontier中的schedule()方法,而是增加了一层封装,

先调用了一个类内部的protected类型的schedule()方法,进而在这个方法中再调用Frontier

schedule方法。这种方式对FrontierScheduler进行扩展留出了很好的接口。

例如,当有某个任务在抓取时,可能希望人为的去除符合某种条件的URL链接,使得其内

容不会保存到本地。比如,要去除所有的扩展名为.zip.exe.rar.pdf.doc的链接(其

实也就是不想下载这类文件)可以通过继承FrontierScheduler并重写内部的schedule方法

来达到我们的需要。以下是一个示例。

protected void schedule(CandidateURI caUri) {

String url = ng();

if (th(".zip")

|| th(".rar")

|| th(".exe")

|| th(".pdf")

|| th(".doc")

|| th(".xls")) {

return;

}

getController().getFrontier().schedule(caUri);

}

这样,每当Heritrix在执行任务时,遇到这样的文件,就会跳过抓取,从而达到了对URL

链接进行筛选的目的。

10.3.5 Prefetcher中取消的限制

是一种专门用于搜索引擎网络爬虫的文件,当构造一个网站时,如果作者希望该

网站的内容被搜索引擎收录,就可以在网站中创建一个纯文本文件,在这个文件

中,声明该网站不想被robot访问的部分。这样,该网站的部分或全部内容就可以不被搜索

引擎收录了,或者指定搜索引擎只收录指定的内容。

Heritrix在其说明文档中,表明它是一个完全遵守协议的网络爬虫。这一点固然在

宣传上起到了一定的作用。但是,在实际的网页采集过程中,这并不是一种最好的作法。因

为大部分的网站并不会放置一个文件以供搜索引擎读取,在互联网信息以几何级

数增长的今天,网站总是在希望自己的内容不被人所利用的同时,又希望自己能够被更多的

用户从搜索引擎上检索到。

不过幸好,协议本身只是一种附加的协议,网站本身并不能了解究竟哪些Socket

联接属于爬虫哪些属于正常的浏览器连接。所以,不遵守协议成为了更多搜索引擎

的首选。

使用过Heritrix的朋友就会发现这样一个问题,如果当一个网站没有放置文件时,

Heritrix总是要花上大量的时间试图去访问这样一个文件,甚至可能retry很多次。这无疑很

大的降低了抓取效率。因此,为了提高抓取的效率,可以试着将对的访问部分去

除。

Heritrix中,对文件的处理是处于PreconditionEnforcer这个Processor中的。

PreconditionEnforcer是一个Prefetcher,当处理时,总是需要考虑一下当前这个链接是否有

访

PreconditionEnforcer中,有一个private类型的方法,它的方法签名为:

private boolean considerRobotsPreconditions(CrawlURI curi)

该方法的含义为:在进行对参数所表示的链接的抓取前,看一下是否存在一个由

所决定的先决条件。很显然,如果对每个链接都有这样的处理。那么,很有可能导致整个抓

取任务的失败。因此,需要对它进行调整。

这个方法返回true时的含义为需要考虑文件,返回false时则表示不需要考虑

文件,可以继续将链接传递给后面的处理器。所以,最简单的修改办法就是将这

个方法整个注释掉,只留下一个false的返回值。经过笔者的试验,这种方法完全可行,抓

取的速度提高了至少一半以上!

10.4 小结

本章对一款使用纯Java语言开发的、功能强大的网络爬虫进行了介绍。从它的使用入门至

系统结构,以至最后的扩展和定制,旨在使读者用最快的速度了解一款优秀的开源爬虫。

而,由于篇幅所限,本章中的内容只能算是一个简单的入门。Heritrix本身的功能极其强大,

且扩展性良好。但它的缺点是配置较为复杂,且源码不好理解。希望有能力的读者可以下载

它的源码并且阅读,相信通过这样的努力,一定可以令自己阅读代码的能力有很大的增强。

在一个搜索引擎的开发过程中,使用一个合适的爬虫来获得所需要的网页信息是第一步,

一步是整个系统成功的基础。因为搜索引擎事实上是一个巨大的资源库,如果从资源角度无

法解决用户的需要。那么它也一定不会成功。相信Heritrix在今后的版本中会更加完善,功

能更为丰富。