1.1 inject
1.1.1 职责
将文本文件中的种子url初始化到系统存储所有url列表的库(crawldb)中。爬虫系统本质上是图遍历,最终目的是发现整个网络图(只不过nutch的图遍历与传统《数据结构》中的图遍历算法不同之处在于——后者一般是在图拓扑结构已知的情况下,以某种策略访问每个节点,而前者是访问之前图拓扑未知,是在访问的过程中一边发现新的节点,一边遍历的过程)。图遍历需要指定从哪个(些)起点开始遍历。inject相当于为nutch遍历整个网络图指定了入口,由于考虑了并行化,nutch可以指定多个起始位置开始并行遍历整个网络图,所以可以指定多个seed url[2]
1.1.2 Usage
injector <crawldb> <url_dir>,参数解释如下:
1) crawldb:是injector的输出路径,用来存储所有爬取url数据库(即crawldb)文件夹路径。
crawldb存储了<url:CrawlDatum>之之间的映射关系,物理上以HadoopSequenceFile文件的形式存在于HDFS上(当然该HDFS不一定是分布式的,单机的话也可以是本能地文件系统)。
2) url_dir:是injector的输入目录,可以含有子目录,各级目录中的文本文件都会按照如图5格式进行解析:
图5 种子url文本文件的格式
每个文本文件每行定义一个url和若干可选的元数据,之间以制表符切分,元数据格式为key=value形式,元数据的key除了以下3个保留的key值之外,可以任意取值(但是中间不能有\t):
1) nutch.score
该key定义了url的得分(即重要程度),nutch给url计算重要性的具体做法可参见[6],实际是pageRank算法,该算法将网页和网页之间的链接视为源网页对目标网页的一次投票,该算法对一般会对每个网页设置初始重要程度为相等的值,nutch采用的默认值是1.0f。当然,用户为了使得某个网页在inject之后,能够有更大的可能被选中加入到fetchlist,加入到fetchlist就能在接下来的fetch阶段开始该网页的爬取。可以将该值设置的大一些。但是需要注意的时,pageRank算法视链接为投票,一个url的初始重要程度高,并不代表在pageRank收敛时,该url的最终重要程度高,其重要程度主要由指向该url的其他url的重要程度决定的,即inlinks的重要程度决定的。
2) nutch.fetchInterval
注入的url爬取周期(fetchInterval),即在多少天之后能够被再次选取加入到fetchlist,无论该url之前是什么状态(如是否被爬取,由于出错重试了一定次数,具体参见[4]),默认是30天。
3) nutch.fetchInterval.fixed
为注入的url设置固定爬取周期(fetchInterval),该配置项的优先级比nutch.fetchInterval高,如果注入了该值,爬取周期固定为指定值,nutch.fetchInterval即使指定了也无效,并且nutch执行过程中不会修改该值。
以上3个metadata对该url决定了该url何时能够被爬取,具体参见5.3。
1.1.3 Configurable Item
表1 inject的可配置项解释
参数 | 含义 | 默认值 |
db.fetch.interval.default | 注入url时,默认的爬取周期(fetch.interval) | 2592000,单位:秒,=30天,该值的实际含义是:不管该url之前是什么状态,在30天之后,nutch都将重新尝试爬取该网页。 注意:该值只是一个"软期限",实际该网页到了30天之后,是否能被加入到fetchlist还要受fetch阶段的参数控制,具体参见5.3 |
db.score.injected | 注入时url的默认网页得分(重要程度) | 1.0f,网页默认的重要程度,默认网页的重要程度都相同,关于网页的Scoring可以参照[6] |
db.injector.overwrite | 这2者是控制新注入的url在原来的crawldb中已经存在的时候(当该条件不成立的时候,二者都不起作用),nutch如何处理新注入的metadata和旧的metadata之间的关系的,2者共同配合完成如下图6所示逻辑:
| 2者的默认值都为false,表明:在出现待注入的url已经在crawldb中存在的情况下,完全忽略新注入的url以及其附带的metadata |
db.injector.update |
图6 injector的configurable item中update和override参数的作用
1.1.4 相关数据结构分析
1.1.4.1 CrawlDatum
存储了url在整个爬取过程中的状态信息,该状态信息跨越多个阶段,该状态信息的不同取值影响着nutch运行时行为的如下内容:
1) url是否会被爬取。
2) 何时能被爬取。
3) 在爬取成功后什么时候再开始下次爬取。
具体的,状态信息包含:
1) byte类型的status:存储了从inject、fetch、parse等阶段的状态,具体如图 7所示:
图7 crawlDatum中url的status取值范围(为简化图形省略了前缀STATUS_)
通过图 7可以看出,nutch为了将不同的status状态取值划分成了3个不同的范围,从上到下依次为“crawldb中的状态”、“fetch阶段的状态”和“其他阶段的状态”,为了便于进行逻辑运算(例如:CrawlDatum.hasDbStatus(CrawlDatum)和hasFetchStatus(CrawlDatum)方法),nutch在各个阶段中间设置了各个阶段的上限取值DB_MAX和FETCH_MAX。
不同的status的取值会在不同的阶段之间进行变迁,可以如下图所示的状态机[4]来表示:
图 8 nutch的CrawlDatum状态机[4]
2) long类型的(实际是Date,存储是微秒)fetchtime:上次(或者下次)爬取时间,初始化的crawlDatum的时候,nutch将其初始化为系统当前时间,该时间会被nutch的FetchSchedule(爬取调度器)来使用以决定该crawlDatum对应的url是否会加入到fetchlist。
3) byte类型的retries,在fetch阶段,由于各种原因(最常见的是源网站连接不上),url无法访问,对该url的fetch会在一定的时间内超时,此时会retries次数会自增,retries超过一定的次数,该url会被认为GONE状态。retries与status的关系见图 8。
4) fetchInterval:爬取周期(间隔),表示每隔多长时间该url具有被选取加入fetchlist的资格。
5) score:该url的重要程度,影响该url爬取的优先级,越高越会被优先爬取。
6) signature:url对应的html内容的md5摘要签名。用于验证2个网页的内容是否相同。
7) moidfiedTime:网页内容的变化时间,当该url再次被爬取的时候,通过计算器内容的signatrue,与上次进行比较,如果不相同,则认为网页内容发生了变化,记录moidfiedTime为当前时间。
8) metaData,一个持久化的map结构存储key:value对。用于存储爬取过程中该url其他的元信息,例如:图片分辨率,url所在的segment文件夹名称等。
1.1.4.2 Crawldb
Crawldb是逻辑概念,存储<url:CrawlDatum>之间的映射关系,物理上是HDFS上的一个SequenceFile格式的文件。该文件中典型的一条记录如图 9所示:
图9 CrawlDb的记录示例
1.1.5 MapReduce Job分析
1.1.5.1 sortJob
图10 injector的程序流程图
injector的input有两种文件类型,一种是待注入的文本文件,二是旧的sequenceFile类型的crawldb文件。由于2种文件类型不同,无法发送给同一个Reducer进行处理,nutch采用了2个MapReduce Job来处理,这2个job就是图 10中的2个“子流程”。其中sortJob主要完成从种子文件文本转换成与现有crawldb同样格式的<url:CrawlDatum>的预处理工作,以方便mergeJob采用统一的的InjectReducer进行处理,sortJob的解析如图 11所示。
图 11 injector sortJobMapReduce过程解析
sortJob的map阶段由InjectMapper进行处理,主要的程序流程图如图 12所示。
图 12 InjectMapper的程序流程图
需要注意的是,sortJob在mapper阶段执行完毕之后,会判断crawldb是否已经存在,如果已经存在,sortJob不会执行reducer阶段,而是等到mergeJob来一并处理。如果存在crawldb不存在,则sortJob会执行InjectReducer来进行处理。
从实际nutch对sortJob的实现来看,sortJob应该命名为filterJob更合适一些,因为其大部分都是在进行url的“过滤(filter)”和“规范化(normalization)”工作,真正“sort”的是依赖于mapReduce框架的shuffle过程完成的。
1.1.5.1 mergeJob
mergeJob,顾名思义,主要完成相同url的CrawlDatum的“合并”工作。它将sortJob的输出和现有的crawldb二者作为输入,如果存在相同的key(为url),由于key为url,在shuffle阶段,相同url的CrawlDatum会被发送到同一个InjectReducer,所以无论是新注入的url的CrawlDatum还是原来crawldb中已经存在的CrawlDatum(url与新注入的url相同)都会被同一个InjectReducer处理,所以InjectReducer可以判断是否新注入的url与现有的crawldb中的url存在重复的情况。具体重复之后如何处理,要受到injector的update和overwrite参数的控制(参见图 6)。 mergeJob的MapReduce过程分析如图 13。
图 13 injector mergeJob MapReduce过程解析
1.1.5.1 相关Design Pattern
1.1.5.1.1 Composite模式
图 14 URLNormalizer及其子类和URLNormalizer采用Composite模式
图 14中角色在nutch中的对应关系如下:
1) 抽象“组件”:URLNormalizer。
2) 具体“组件”:BasicURLNormalizer、PassURLNormalizer、RegexURLNormalizer,HostURLNormalizer和QueryStringURLNormalizer,其中前三者是nutch默认启用的。
3) 组合“组件”:URLNormalizers。
4) Client: nutch中充当该Composite模式的Client的类有:Injector、Generator、LinkDb、LinkDbFliter、URLParitioner。
虽然,nutch在实现时,URLNormalizers并没有implements URLNormalizer接口,但是它实现了与接口同名的方法,实际上是符合了Composite模式的特征。
其实不仅仅是URLNormalizer一个插件实现了Composite模式,nutch以下插件也都实现了Composite模式:
1) URLFilter及其子类和URLFilters。
2) ScoringFilter及其子类和ScoringFilters。
3) IndexWriter及其子类和IndexWriters。
4) IndexingFilter及其子类和IndexingFilters。
5) HtmlParseFilter及其子类和HtmlParseFilters。
实际上,各种Filter都实现了共同的接口pluggable,pluggable是一个空接口,起标示作用(Markable Interface)。pluggable及其子类的类图如下:
图 15 pluggable接口和子类以及其相关类形成的Design Pattern
从图 15可以看出,pluggable子类下半部分实现了Composite模式,上半部分实现了简单工厂模式。
1.1.6 FAQ
1.1.6.1 inject阶段只能被执行一次吗?
不是,inject可以被执行多次。但是需要注意避免对crawldb的写冲突,例如updatedb阶段也会写crawldb,或者是多实例爬取环境下的执行多个inject。
1.1.6.2 同一个url多次inject会在crawldb中生成重复记录吗?
不会,因为crawldb的key为url,相同的url在crawldb中只有一条记录,无论重复注入多少次。
1.1.6.3 如果在inject时指定了nutch.fetchInterval.fixed值,即为某url设置了固定爬取周期,是否能更改?
可以,两种方法,一是再次执行inject,为该url指定不同的nutch.fetchInterval.fixed值;二是二次开发,直接修改crawldb中对应的crawldatum的metadata的key为nutch.fetchInterval.fixed的值。
1.1.6.4 如果限制了只爬取与种子url文件在相同domian下的url,如何让nutch在运行时爬取新的domain下的url?
该问题不是一个FAQ,应该算作一个trick,可以使用inject注入新的domain下的某个url到crawldb中(注意:要避免对crawldb的写冲突,即在inject时有其他阶段正在写crawldb,例如updatedb)。
1.1.6.5 在种子文件中含有重复的url(即一个url出现2次以上),injector如何处理?
InjectReducer处理了这种情况,对于相同url的iterator<CrawlDatum>,最后的一个CrawlDatum生效。
参考文献
[1]. http://wiki.apache.org/nutch/CrawlDatumStates