Nutch源码阅读进程3---fetch
走了一遍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 {
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<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;
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;
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抓取队列中除名。再往下执行比较核心的三行代码:
(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();//获得内容
ProtocolStatus status = output.getStatus();//获得状态
Content content = output.getContent();//获得内容
5.再下面主要是对响应的相应状态进行相应的处理:
(1):如果状态为WOULDBLOCK,执行:
case ProtocolStatus.WOULDBLOCK:
// retry ?
fetchQueues.addFetchItem(fit);
break;
// 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);
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进行了一系列判断及操作:
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);
}
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集合中,然后再对这个重定向的网页进行抓取;
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)
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时,采集结束。
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. 微信