为了彻底弄明白Nutch中的Html页面解析流程,所以接下来研究下HtmlParser类。
路径:$nutch-1.7/src/plugin/parse-html/src/java/org/apache/nutch/parse/html
类:HtmlParser.java
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 setConf函数
代码如下:
public void setConf(Configuration conf) { this.conf = conf; this.htmlParseFilters = new HtmlParseFilters(getConf()); this.parserImpl = getConf().get("parser.html.impl", "neko"); this.defaultCharEncoding = getConf().get( "parser.character.encoding.default", "windows-1252"); this.utils = new DOMContentUtils(conf); this.cachingPolicy = getConf().get("parser.caching.forbidden.policy", Nutch.CACHING_FORBIDDEN_CONTENT); }
这个函数应该是首先被调用的,我们来看看这个函数做了什么!
添加打印代码,打印的结果如下所示:
2014-06-26 10:22:25,809 INFO parse.html - save conf now!!! 2014-06-26 10:22:25,811 INFO parse.html - parserImpl---neko 2014-06-26 10:22:25,811 INFO parse.html - defaultCharEncoding---windows-1252 2014-06-26 10:22:25,814 INFO parse.html - get DOMContentUtils Object ok! 2014-06-26 10:22:25,814 INFO parse.html - cachingPolicy---content
结论:
代码的作用是:
1保存配置conf.
2从配置里获取所有的htmlParseFilters插件供后续使用
3 真正的解析类是neko.
4 默认的编码方式是-windows-1252.
5构造了一个DOMContentUtils对象。
6cachingPolicy的值为content.
从打印信息中知道目前系统内部还没有HtmlParseFilter插件。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
接下来进入到最关键的函数
public ParseResult getParse(Content content) {
代码如下:
public ParseResult getParse(Content content) { HTMLMetaTags metaTags = new HTMLMetaTags(); URL base; try { base = new URL(content.getBaseUrl()); } catch (MalformedURLException e) { return new ParseStatus(e).getEmptyParseResult(content.getUrl(), getConf()); } String text = ""; String title = ""; Outlink[] outlinks = new Outlink[0]; Metadata metadata = new Metadata(); // parse the content DocumentFragment root; try { byte[] contentInOctets = content.getContent(); InputSource input = new InputSource(new ByteArrayInputStream(contentInOctets)); EncodingDetector detector = new EncodingDetector(conf); detector.autoDetectClues(content, true); detector.addClue(sniffCharacterEncoding(contentInOctets), "sniffed"); String encoding = detector.guessEncoding(content, defaultCharEncoding); metadata.set(Metadata.ORIGINAL_CHAR_ENCODING, encoding); metadata.set(Metadata.CHAR_ENCODING_FOR_CONVERSION, encoding); input.setEncoding(encoding); if (LOG.isTraceEnabled()) { LOG.trace("Parsing..."); } root = parse(input); } catch (IOException e) { return new ParseStatus(e).getEmptyParseResult(content.getUrl(), getConf()); } catch (DOMException e) { return new ParseStatus(e).getEmptyParseResult(content.getUrl(), getConf()); } catch (SAXException e) { return new ParseStatus(e).getEmptyParseResult(content.getUrl(), getConf()); } catch (Exception e) { LOG.error("Error: ", e); return new ParseStatus(e).getEmptyParseResult(content.getUrl(), getConf()); } // get meta directives HTMLMetaProcessor.getMetaTags(metaTags, root, base); if (LOG.isTraceEnabled()) { LOG.trace("Meta tags for " + base + ": " + metaTags.toString()); } // check meta directives if (!metaTags.getNoIndex()) { // okay to index StringBuffer sb = new StringBuffer(); if (LOG.isTraceEnabled()) { LOG.trace("Getting text..."); } utils.getText(sb, root); // extract text text = sb.toString(); sb.setLength(0); if (LOG.isTraceEnabled()) { LOG.trace("Getting title..."); } utils.getTitle(sb, root); // extract title title = sb.toString().trim(); } if (!metaTags.getNoFollow()) { // okay to follow links ArrayList<Outlink> l = new ArrayList<Outlink>(); // extract outlinks URL baseTag = utils.getBase(root); if (LOG.isTraceEnabled()) { LOG.trace("Getting links..."); } utils.getOutlinks(baseTag!=null?baseTag:base, l, root); outlinks = l.toArray(new Outlink[l.size()]); if (LOG.isTraceEnabled()) { LOG.trace("found "+outlinks.length+" outlinks in "+content.getUrl()); } } ParseStatus status = new ParseStatus(ParseStatus.SUCCESS); if (metaTags.getRefresh()) { status.setMinorCode(ParseStatus.SUCCESS_REDIRECT); status.setArgs(new String[] {metaTags.getRefreshHref().toString(), Integer.toString(metaTags.getRefreshTime())}); } ParseData parseData = new ParseData(status, title, outlinks, content.getMetadata(), metadata); ParseResult parseResult = ParseResult.createParseResult(content.getUrl(), new ParseImpl(text, parseData)); // run filters on parse ParseResult filteredParse = this.htmlParseFilters.filter(content, parseResult, metaTags, root); if (metaTags.getNoCache()) { // not okay to cache for (Map.Entry<org.apache.hadoop.io.Text, Parse> entry : filteredParse) entry.getValue().getData().getParseMeta().set(Nutch.CACHING_FORBIDDEN_KEY, cachingPolicy); } return filteredParse; }
下面来看看这个函数到底是怎么解析的!
根据打印信息,知道:
content.getContent---内容是原汁原味的html内容,不经过任何加工。
通过日志发现,Nutch的content.getContent().length的长度总是为65536.这就很奇怪了,看来Nutch在配置中做了限制。
查看conf/nutch-default.xml发现
<property> <name>http.content.limit</name> <value>65536</value> <description>The length limit for downloaded content using the http:// protocol, in bytes. If this value is nonnegative (>=0), content longer than it will be truncated; otherwise, no truncation at all. Do not confuse this setting with the file.content.limit setting. </description> </property>
这就好办了,将这部分复制到conf/nutch-site.xml,然后修改为
<property> <name>http.content.limit</name> <value>1048576</value> <description>The length limit for downloaded content using the http:// protocol, in bytes. If this value is nonnegative (>=0), content longer than it will be truncated; otherwise, no truncation at all. Do not confuse this setting with the file.content.limit setting. </description> </property>
然后看
root = parse(input);
知道默认解析方式是:
Neko
~~~~~~~继续往下分析下面的代码。
HTMLMetaProcessor.getMetaTags(metaTags, root, base);
获取meta信息
其中一个可能的形式如下:
metaTags information---base=null, noCache=false, noFollow=false, noIndex=false, refresh=false, refreshHref=null * general tags: - title = 优酷音乐-无音乐,不生活! - keywords = 优酷音乐 优酷音乐频道 优酷 优酷视频 优酷电影 优酷电视剧,优酷视频 - msapplication-task = name=上传视频;action-uri=http://www.youku.com/v/upload/;icon-uri=http://www.youku.com/favicon.ico - application-name = 优酷网 - description = 优酷音乐-无音乐,不生活! - 快速找到你所想要的,超棒视听空间! - 优酷视频 - msapplication-starturl = http://www.youku.com/ * http-equiv tags: - content-type = text/html; charset=UTF-8 - content-language = zh-CN
这样的话,后续想要什么东西,可以考虑从这里获取。
然后下面解析是提取若干内容
text----对应着网页中去掉标签后的文本。
title----网页的标题
outlinks---当前网页中的外链数组。
这个都比较简单。后续如果想过滤外链的话就可以考虑从这里过滤。
~~~~~~~~~~~~
接下来是构造parseResult
相关的一些字段的值如下:
content.getMetadata()------ETag="1951694047" Date=Thu, 26 Jun 2014 05:33:38 GMT Vary=Accept-Encoding Content-Length=20774 nutch.crawl.score=1.0 Content-Encoding=deflate Last-Modified=Thu, 26 Jun 2014 05:31:29 GMT _fst_=33 nutch.segment.name=20140626133401 Connection=close Content-Type=text/html Server=b28www1 content.getUrl()------http://music.youku.com/
剩下的就是过滤器的作用了。
分析完毕。
到这里只是解析完毕。
有兴趣的读者请再回头查看org.apache.nutch.crawl.Crawl.java的 public int run(String[] args) throws Exception {
函数。
这里简单解释一下流程。
1 获取参数,比如线程个数,topN的个数。
2 生成各个对象,比如
Injector injector = new Injector(getConf()); Generator generator = new Generator(getConf()); Fetcher fetcher = new Fetcher(getConf()); ParseSegment parseSegment = new ParseSegment(getConf()); CrawlDb crawlDbTool = new CrawlDb(getConf()); LinkDb linkDbTool = new LinkDb(getConf());
3 只执行一次注入操作
// initialize crawlDb injector.inject(crawlDb, rootUrlDir);
4 根据depth决定循环次数
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 if (!Fetcher.isParsing(job)) { parseSegment.parse(segs[0]); // parse it, if needed } crawlDbTool.update(crawlDb, segs, true, true); // update crawldb }
5 执行
linkDbTool.invert(linkDb, segments, true, true, false); // invert links
6 如果设置了solr的url
执行针对solr的索引操作
if (solrUrl != null) { // index, dedup & merge FileStatus[] fstats = fs.listStatus(segments, HadoopFSUtil.getPassDirectoriesFilter(fs)); IndexingJob indexer = new IndexingJob(getConf()); indexer.index(crawlDb, linkDb, Arrays.asList(HadoopFSUtil.getPaths(fstats))); SolrDeleteDuplicates dedup = new SolrDeleteDuplicates(); dedup.setConf(getConf()); dedup.dedup(solrUrl); }
到此,所有的爬取网页-解析-非索引的操作就完成了。后续讲解索引操作。
目标是ElasticSearch.