走了一遍Inject和Generate,基本了解了nutch在执行爬取前的一些前期预热工作,包括url的过滤、规则化、分值计算以及其与mapreduce的联系紧密性等,自我感觉nutch的整个流程是很缜密的,起码从前面两个过程看是这样的。
 
前期回顾:上一期主要是讲解了nutch的第二个环节Generate,该环节主要完成获取将要抓取的url列表,并写入到segments目录下,其中一些细节的处理包括每个job提交前的输入输出以及执行的map和reducer类具体做了那些工作都可以参考上一篇。接下来的fetch部分感觉应该是nutch的灵魂了,因为以前的nutch定位是搜索引擎,发展至今已演变为爬虫工具了。
 
这几天在弄一个项目的基础数据,都没有好好的用心看nutch,中间试图再次拜读fetch这块的代码,发现这是一块难啃的骨头,网上的一些材料讲的侧重点也有所不同,但是为了走完nutch,必须跨过这道坎。。。。。。下面开始吧~~~~
 
1.fetch的入口从Crawl类的fetcher.fetch(segs[0], threads);语句入手,其将segments和爬取的线程数作为参数传到fetch函数中,进入到fetch函数中,首先执行的是一个checkConfiguration函数,用于检查http.agent.name和http.robot.nam是否有值,如果为空则通过控制台返回一些报错信息等。后面就是一些变量的赋值和初始化,比如超时变量、抓取的最大深度、最多的链接个数等这些都是为了后面抓取工作做准备的。后面可是初始化一个mapreduce的job,设置输入为:Generate阶段生成的segments目录下的crawl_generate,输出为:segments,要操作的map的类是:job.setMapRunnerClass(Fetcher.class);,通过job.runJob(job)提交job;
 
2.提交完job后开始执行Fetch的run方法:
public void run(RecordReader<Text, CrawlDatum> input,
OutputCollector<Text, NutchWritable> output,
Reporter reporter) throws IOException {
……
}
从run函数的参数就可以看出输入是text和crawlDatum封装的类RecordReader,输出为text和nutchWritable封装的类OutputCollector,当然了,进来还是一通的设置各种参数、阈值等等。这里值得一提的是对于爬取网页这块用的一个以前学操作系统中关于任务调度的经典案例——生产者与消费者案例。首先通过一行代码:feeder = new QueueFeeder(input, fetchQueues, threadCount * queueDepthMuliplier);定义生产者队列,其中的input就是输入参数,fetchQueues是通过this.fetchQueues = new FetchItemQueues(getConf());得到(默认是采取byHost模式,另外还有两种byIP和byDomain),第三个参数也是读取配置文件的默认值来的。这里定义好生产者后,主要负责从Generate出来的crawldatum的信息,并把它们加入到共享队列中去。(补充一下:关于FetchItemQueues、FetchItemQueue以及FetchItem之间的相互关系可以通过查找源码发现:
FetchItemQueues中包含的字段有:
public static final String DEFAULT_ID = "default";
Map<String, FetchItemQueue> queues = new HashMap<String, FetchItemQueue>();
AtomicInteger totalSize = new AtomicInteger(0);
int maxThreads;
long crawlDelay;
long minCrawlDelay;
long timelimit = -1;
int maxExceptionsPerQueue = -1;
Configuration conf;
public static final String QUEUE_MODE_HOST = "byHost";
public static final String QUEUE_MODE_DOMAIN = "byDomain";
public static final String QUEUE_MODE_IP = "byIP";
String queueMode;
由此可见,这里的map集合queues是将一个字符串与FetchItemQueue封装后得到的,那么FetchItemQueue主要包括的字段有:
List<FetchItem> queue = Collections.synchronizedList(new LinkedList<FetchItem>());
Set<FetchItem> inProgress = Collections.synchronizedSet(new HashSet<FetchItem>());
AtomicLong nextFetchTime = new AtomicLong();
AtomicInteger exceptionCounter = new AtomicInteger();
long crawlDelay;
long minCrawlDelay;
int maxThreads;
Configuration conf;
同理,从这里可以看出,queue是有对象FetchItem封装而来,而这里的FetchItem主要包括以下字段:
int outlinkDepth = 0;
String queueID;
Text url;
URL u;
CrawlDatum datum;
至此,我们大概清楚了从fetchitem->fetchitemqueue->fetchitemqueues的封装关系了。
既然有了生产者生产产品了,那就应该有消费者来消费了(有需求就有市场,有市场也就有消费者)
 
3.消费者的产生源自代码:
for (int i = 0; i < threadCount; i++) {       // spawn threads  
     new FetcherThread(getConf()).start();  
   }  
这样就根据用户设置的需求,生成指定个数threadCount个消费者。在这之前还有一些参数的设置比如超时、blocking等,该方法后面就是关于等待每个线程(消费者)的结束以及每个线程抓取了多少网页是否成功抓取网页的信息,后面再判断生产者的抓取队列是否已经被抓取完,如果是则输出抓取队列中的信息,另外还有个一判断机制,判断抓取的线程是否超时,如果超时则进入等待状态。
 
4.这是整个生产者消费者的模型,形象并有效的反映与解决了抓取的队列和线程之间的关系,下面还要着重看看消费者是如何取到抓取队列中的url并进行抓取的,这时主要是通过new FetcherThread(getConf()).start();  代码进入到FetchThread的run方法。进入后首先就是执行:fit = fetchQueues.getFetchItem();主要是从之前存入抓取队列中取出数据,紧随其后就是判断,取出的数据是否为空,如果为空则进一步判断生产者是否存活或者抓取队列中是否还有数据,如果有则等待,如果没有则任务fetchItem已经处理完了,结束该线程(消费者)的爬取。当然,如果取得了fit不为空,则通过代码: Text reprUrlWritable =
(Text) fit.datum.getMetaData().get(Nutch.WRITABLE_REPR_URL_KEY); if (reprUrlWritable == null) {
reprUrl = fit.url.toString();
} else {
reprUrl = reprUrlWritable.toString();
}得到其url,然后还要从该url的数据中分析出协议protocal(注意:该功能的实现是利用nutch的必杀技插件机制实现的,用到的是protocolFactory这个类,具体怎么回事,有待研究^_^),稍后是判断该url是否遵从RobotRules,如果不遵从则利用代码:fetchQueues.finishFetchItem(fit, true);或者如其delayTime大于我们配置的maxDelayTime,那就不抓取这个网页将其从fetchQueues抓取队列中除名。再往下执行比较核心的三行代码:
ProtocolOutput output = protocol.getProtocolOutput(fit.url, fit.datum);//利用协议获得响应的内容
ProtocolStatus status = output.getStatus();//获得状态
Content content = output.getContent();//获得内容
 
5.再下面主要是对响应的相应状态进行相应的处理:
(1):如果状态为WOULDBLOCK,执行:
             case ProtocolStatus.WOULDBLOCK:
                // retry ?
                fetchQueues.addFetchItem(fit);
                    break;
即进行retry,把当前url添加到FetchItemQueues队列中,进行重试
(2)如果状态时SUCCESS,表示抓取到了页面,紧接着就是执行: pstatus = output(fit.url, fit.datum, content, status, CrawlDatum.STATUS_FETCH_SUCCESS, fit.outlinkDepth);进入到output这个方法后,我们可以看到首先是对于元数据的赋值,包括 datum.setStatus(status);
datum.setFetchTime(System.currentTimeMillis());datum.getMetaData().put(Nutch.WRITABLE_PROTO_STATUS_KEY, pstatus);等,后面就是判断如果fetch_success标记存在的话即表示抓取成功,则将执行对抓取到的页面源码进行解析parseResult = this.parseUtil.parse(content);
再后面就是表示写文件output.collect(key, new NutchWritable(datum));
output.collect(key, new NutchWritable(content));
output.collect(url, new NutchWritable(new ParseImpl(new ParseText(parse.getText()),parseData, parse.isCanonical())));
以上执行完output方法后我们可以通过代码pstatus = output(fit.url, fit.datum, content, status, CrawlDatum.STATUS_FETCH_SUCCESS, fit.outlinkDepth);发现会返回pstatus状态,该状态表示从页面中是否解析出来了url。如果解析出来了则标记为STATUS_DB_UNFETCHED并初始化分值,代码如下:
CrawlDatum newDatum = new CrawlDatum(CrawlDatum.STATUS_DB_UNFETCHED,
fit.datum.getFetchInterval(), fit.datum.getScore());
// transfer existing metadata to the redir
newDatum.getMetaData().putAll(fit.datum.getMetaData());
scfilters.initialScore(redirUrl, newDatum);随即还对该redirUrl进行了一系列判断及操作:
if (reprUrl != null) {
newDatum.getMetaData().put(Nutch.WRITABLE_REPR_URL_KEY,
new Text(reprUrl));
}
fit = FetchItem.create(redirUrl, newDatum, queueMode);
if (fit != null) {
FetchItemQueue fiq =
fetchQueues.getFetchItemQueue(fit.queueID);
fiq.addInProgressFetchItem(fit);
} else {
// stop redirecting
redirecting = false;
reporter.incrCounter("FetcherStatus", "FetchItem.notCreated.redirect", 1);
}
以上就是对于返回状态为success的url的一系列解决方式;
 
(3)如果是MOVED或者TEMP_MOVED,表示这个网页被重定向了。然后对其重定向的内容进行解析并生成相应的文件,执行output(fit.url, fit.datum, content, status, code);以及 Text redirUrl =handleRedirect(fit.url, fit.datum,
urlString, newUrl, temp,Fetcher.PROTOCOL_REDIR);得到重定向的网址并生成一个新的FetchItem,根据其QueueID放到相应的队列的inProgress集合中,然后再对这个重定向的网页进行抓取;
 
(4)如果状态是EXCEPTION,对当前url所属的FetchItemQueue进行检测,看其异常的网页数有没有超过最大异常网页数,如果大于,那就清空这个队列,认为这个队列中的所有网页都有问题;
 
(5)如果状态是RETRY或者是BLOCKED,那就输出CrawlDatum,将其状态设置成STATUS_FETCH_RETRY,在下一轮进行重新抓取;
 
(6)如果状态是GONE,NOTFOUND,ACCESS_DENIED,ROBOTS_DENIED,那就输出CrawlDatum,设置其状态为STATUS_FETCH_GONE,可能在下一轮中就不进行抓取了;
 
(7)如果状态是NOTMODIFIED,那就认为这个网页没有改变过,那就输出其CrawlDatum,将其状态设成成STATUS_FETCH_NOTMODIFIED;
 
(8)如果所有状态都没有找到,那默认输出其CrawlDatum,将其状态设置成STATUS_FETCH_RETRY,在下一轮抓取中再重试
最后判断网页重定向的次数,如果超过最大重定向次数,就输出其CrawlDatum,将其状态设置成STATUS_FETCH_GONE
 
6.每个消费者“消费”的过程走完后,还要执行从这个消费队列中除名,毕竟你来过了,走了之后就要签个到什么的,所以在FetchThread的run方法最后执行了finally代码:
finally {
if (fit != null) fetchQueues.finishFetchItem(fit);
activeThreads.decrementAndGet(); // count threads
LOG.info("-finishing thread " + getName() + ", activeThreads=" + activeThreads);
}表示当前线程结束,整个线程队列中减少医院,其中activeThreads.decrementAndGet(); 这类的用法在nutch的fetch过程中出现的很频繁,activeThreads的定义为:private AtomicInteger activeThreads = new AtomicInteger(0);(补充一下:这里主要的作用表示不管是decrementAndGet()还是incrementAndGet()方法都是线程安全的,一个表示减1,一个表示加1)
后面就是其他的消费中一次重复3、4、5、6的过程,我们跳出来回到Crawl.java类中的fetcher.fetch(segs[0], threads);方法可以看出它也是在整个循环:
for (i = 0; i < depth; i++) { // generate new segment
Path[] segs = generator.generate(crawlDb, segments, -1, topN, System
.currentTimeMillis());
if (segs == null) {
LOG.info("Stopping at depth=" + i + " - no more URLs to fetch.");
break;
}
fetcher.fetch(segs[0], threads); // fetch it segs[0]===[crawl20140727/segments/20140727195735]
if (!Fetcher.isParsing(job)) {
parseSegment.parse(segs[0]); // parse it, if needed
}
crawlDbTool.update(crawlDb, segs, true, true); // update crawldb
}中,也就是说Generate、fetch、parse以及update是在循环执行,当达到用户设置的采集depth或者系统默认的depth时,采集结束。
看到这里,我们大致明白了nutch的采集爬虫的过程了。
 
自己感觉最难啃的一根骨头应该是啃完了,尽管不是啃得很干净……
整个fetch的脉络大致如下,首先是进入从Fetch类的fetch函数入口,然后进行了一系列的赋值初始化等过程提交一个job,从代码job.setMapRunnerClass(Fetcher.class);可以看出在提交job时,执行到fetch的run函数:public void run(RecordReader<Text, CrawlDatum> input,OutputCollector<Text, NutchWritable> output,Reporter reporter) throws IOException 进入该run函数后,就是铺垫好要解决的工作并通过生产者-消费者模型来解决这个问题,真正的爬取部分由消费者来解决,通过代码:new FetcherThread(getConf()).start();看出应该进入到FetcherThread的run函数里面执行一系列的页面抓取、解析等操作。
 
(补充一点,从调试过程可以看到property即配置文件的信息为:{job.end.retry.interval=30000, ftp.keep.connection=false, io.bytes.per.checksum=512, mapred.job.tracker.retiredjobs.cache.size=1000, db.fetch.schedule.adaptive.dec_rate=0.2, mapred.task.profile.reduces=0-2, mapreduce.jobtracker.staging.root.dir=${hadoop.tmp.dir}/mapred/staging, mapred.job.reuse.jvm.num.tasks=1, mapred.reduce.tasks.speculative.execution=true, moreIndexingFilter.indexMimeTypeParts=true, db.ignore.external.links=false, io.seqfile.sorter.recordlimit=1000000, generate.min.score=0, db.update.additions.allowed=true, mapred.task.tracker.http.address=0.0.0.0:50060, fetcher.queue.depth.multiplier=50, fs.ramfs.impl=org.apache.hadoop.fs.InMemoryFileSystem, mapred.system.dir=${hadoop.tmp.dir}/mapred/system, mapred.task.tracker.report.address=127.0.0.1:0, mapreduce.reduce.shuffle.connect.timeout=180000, db.fetch.schedule.adaptive.inc_rate=0.4, db.fetch.schedule.adaptive.sync_delta_rate=0.3, mapred.healthChecker.interval=60000, mapreduce.job.complete.cancel.delegation.tokens=true, generate.max.per.host=-1, fetcher.max.exceptions.per.queue=-1, fs.trash.interval=0, mapred.skip.map.auto.incr.proc.count=true, parser.fix.embeddedparams=true,
……
urlnormalizer.order=org.apache.nutch.net.urlnormalizer.basic.BasicURLNormalizer org.apache.nutch.net.urlnormalizer.regex.RegexURLNormalizer, io.compression.codecs=org.apache.hadoop.io.compress.DefaultCodec,org.apache.hadoop.io.compress.GzipCodec,org.apache.hadoop.io.compress.BZip2Codec, link.score.updater.clear.score=0.0f, parser.html.impl=neko, io.file.buffer.size=4096, parser.character.encoding.default=windows-1252, ftp.timeout=60000, mapred.map.tasks.speculative.execution=true, fetcher.timelimit.mins=-1, mapreduce.job.split.metainfo.maxsize=10000000, http.agent.name=jack, mapred.map.max.attempts=4, mapred.job.shuffle.merge.percent=0.66, fs.har.impl=org.apache.hadoop.fs.HarFileSystem, hadoop.security.authentication=simple, fs.s3.buffer.dir=${hadoop.tmp.dir}/s3, lang.analyze.max.length=2048, mapred.skip.reduce.auto.incr.proc.count=true, mapred.job.tracker.jobhistory.lru.cache.size=5, fetcher.threads.timeout.divisor=2, db.fetch.schedule.class=org.apache.nutch.crawl.DefaultFetchSchedule, mapred.jobtracker.blacklist.fault-bucket-width=15, mapreduce.job.acl-view-job= , mapred.job.queue.name=default, fetcher.queue.mode=byHost, link.analyze.initial.score=1.0f, mapred.job.tracker.persist.jobstatus.hours=0, db.max.outlinks.per.page=100, fs.file.impl=org.apache.hadoop.fs.LocalFileSystem, db.fetch.schedule.adaptive.sync_delta=true, urlnormalizer.loop.count=1, ipc.client.kill.max=10, mapred.healthChecker.script.timeout=600000, mapred.tasktracker.map.tasks.maximum=2, http.max.delays=100, fetcher.follow.outlinks.depth.divisor=2, mapred.job.tracker.persist.jobstatus.dir=/jobtracker/jobsInfo, lang.identification.only.certain=false, http.useHttp11=false, lang.extraction.policy=detect,identify, mapred.reduce.slowstart.completed.maps=0.05, io.sort.mb=100, ipc.server.listen.queue.size=128, db.fetch.interval.default=2592000, ftp.password=anonymous@example.com, solr.auth=false, io.mapfile.bloom.size=1048576, ftp.follow.talk=false, fs.hsftp.impl=org.apache.hadoop.hdfs.HsftpFileSystem, fetcher.verbose=false, fetcher.throughput.threshold.check.after=5, hadoop.rpc.socket.factory.class.default=org.apache.hadoop.net.StandardSocketFactory, fs.hftp.impl=org.apache.hadoop.hdfs.HftpFileSystem, db.fetch.interval.max=7776000, fs.kfs.impl=org.apache.hadoop.fs.kfs.KosmosFileSystem, mapred.map.tasks=2, mapred.local.dir.minspacekill=0, fs.hdfs.impl=org.apache.hadoop.hdfs.DistributedFileSystem, urlfilter.domain.file=domain-urlfilter.txt, mapred.job.map.memory.mb=-1, mapred.jobtracker.completeuserjobs.maximum=100, plugin.folders=./plugins, indexer.max.content.length=-1, fetcher.throughput.threshold.retries=5, link.analyze.damping.factor=0.85f, urlfilter.regex.file=regex-urlfilter.txt, mapred.min.split.size=0, http.robots.403.allow=true……这样的信息)
 
参考博文:http://blog.csdn.net/amuseme_lu/article/details/6725561

友情赞助

如果你觉得博主的文章对你那么一点小帮助,恰巧你又有想打赏博主的小冲动,那么事不宜迟,赶紧扫一扫,小额地赞助下,攒个奶粉钱,也是让博主有动力继续努力,写出更好的文章^^。

    1. 支付宝                          2. 微信

                      

Nutch源码阅读进程3---fetch的更多相关文章

  1. Nutch源码阅读进程5---updatedb

    看nutch的源码仿佛就是一场谍战片,而构成这精彩绝伦的谍战剧情的就是nutch的每一个从inject->generate->fetch->parse->update的环节,首 ...

  2. Nutch源码阅读进程2---Generate

    继之前仓促走完nutch的第一个流程Inject后,再次起航,Debug模式走起,进入第二个预热阶段Generate~~~   上期回顾:Inject主要是将爬取列表中的url转换为指定格式<T ...

  3. Nutch源码阅读进程1---inject

    最近在Ubuntu下配置好了nutch和solr的环境,也用nutch爬取了一些网页,通过solr界面呈现,也过了一把自己建立小搜索引擎的瘾,现在该静下心来好好看看nutch的源码了,先从Inject ...

  4. Nutch源码阅读进程3

    走了一遍Inject和Generate,基本了解了nutch在执行爬取前的一些前期预热工作,包括url的过滤.规则化.分值计算以及其与mapreduce的联系紧密性等,自我感觉nutch的整个流程是很 ...

  5. Nutch源码阅读进程5

    看nutch的源码仿佛就是一场谍战片,而构成这精彩绝伦的谍战剧情的就是nutch的每一个从inject->generate->fetch->parse->update的环节,首 ...

  6. Nutch源码阅读进程4---parseSegment

    前面依次看了nutch的准备工作inject和generate部分,抓取的fetch部分的代码,趁热打铁,我们下面来一睹parse即页面解析部分的代码,这块代码主要是集中在ParseSegment类里 ...

  7. Nutch源码阅读进程4

    前面依次看了nutch的准备工作inject和generate部分,抓取的fetch部分的代码,趁热打铁,我们下面来一睹parse即页面解析部分的代码,这块代码主要是集中在ParseSegment类里 ...

  8. Linux 源码阅读 进程管理

    Linux 源码阅读 进程管理 版本:2.6.24 1.准备知识 1.1 Linux系统中,进程是最小的调度单位: 1.2 PCB数据结构:task_struct (Location:linux-2. ...

  9. chromium源码阅读--进程的Message Loop

    上一篇总结了chromium进程的启动,接下来就看线程的消息处理,这里的线程包含进程的主进程. 消息处理是由base::MessageLoop中实现,消息中的任务和定时器都是异步事件的. 主要如下几点 ...

随机推荐

  1. cobbler 配置(转载)

    Cobbler介绍 Cobbler 是一个系统启动服务(boot server),可以通过网络启动(PXE)的方式用来快速安装.重装物理服务器和虚拟机,支持安装不同的 Linux 发行版和 Windo ...

  2. fallacies of distributed computing

    The network is reliable. Latency is zero. Bandwidth is infinite. The network is secure. Topology doe ...

  3. cocoapods的时候出现的问题 _OBJC_CLASS_$_XXX

    最新的cocoapod导入xmpp的时候,会出现循环依赖,所以撸主选择了手动导入. 一开始还用的挺开心的,后来,使用cocoapods导入其他的框架,发现调用的时候总是报错. Undefined sy ...

  4. Linux计划任务crontab运行脚本不正确的问题

    问题的由来 写好的程序希望在崩溃之后能够自启动,于是利用linux的crontab功能,添加一个计划任务,每分钟执行一个脚本查看需要监控的进程是否还在,如果不在则启动之,否则不做任何事情.这么一个简单 ...

  5. EF(Linq)框架使用过程中的小技巧汇总

    这篇博客总结本人在实际项目中遇到的一些关于EF或者Linq的问题,作为以后复习的笔记或者供后来人参考(遇到问题便更新). 目录 技巧1: DbFunctions.TruncateTime()的使用 技 ...

  6. 算法:poj1066 宝藏猎人问题。

    package practice; import java.util.Scanner; public class TreasureHunt { public static void main(Stri ...

  7. npm穿墙

    GWF 很给力,很多东西都能墙掉,但是把 npm 也纳入黑名单,不知道 GWFer 是怎么想的.FQ翻了好多年了,原理其实也挺简单的,proxy 嘛! » 方法一 A) 国内源,http://cnpm ...

  8. Guava - EventBus(事件总线)

    Guava在guava-libraries中为我们提供了事件总线EventBus库,它是事件发布订阅模式的实现,让我们能在领域驱动设计(DDD)中以事件的弱引用本质对我们的模块和领域边界很好的解耦设计 ...

  9. SQLServer 随机生成指定范围的日期

    一个分页的问题,DTCms3.0中,分页是根据时间分页的,如果当添加时间(add_time)都是同一个数值时,不管点击第几页,显示的数据都是同一个的内容,于是就有了需要把同一个时间改指定随机日期的功能 ...

  10. 我心中的核心组件(可插拔的AOP)~调度组件quartz.net续~任务管理器的开发

    回到目录 对于任务调度来说,越来越多的团队选择了quartz,它在java和.net环境下表现都十分优秀,配置简单,功能强大,时间表达式配置灵活,但在使用时,还是感觉缺点什么,怎么说,你在服务器上安装 ...