Nutch 1.3 學習筆記 - Inject
----------------------------
1. Inject是幹嘛的?
在Nutch中Inject是用來把文字格式設定的url列表注入到抓取資料庫中,一般是用來引導系統的初始化。
這裡的文字格式設定如下:
http://www.nutch.org/ \t nutch.score=10 \t nutch.fetchInterval=2592000 \t userType=open_source
這裡的url與其中繼資料之間用Tab隔開,這裡有兩個保留的中繼資料,如下
nutch.score : 表示特定url的分數
nutch.fetchInterval : 表示特定url的抓取間隔,單位為毫秒
Inject注入後產生的資料庫為二進位結構,是Hadoop的MapSequenceFileOutputFormat格式
2. Inject運行命令
bin/nutch inject <url_dir> <crawl_db>
在本地運行後的輸出結果如下:
Injector: starting at 2011-08-23 10:14:10Injector: crawlDb: db/crawldbInjector: urlDir: urlsInjector: Converting injected urls to crawl db entries.Injector: Merging injected urls into crawl db.Injector: finished at 2011-08-23 10:14:12, elapsed: 00:00:02
你可以用如下命令來查看其資料庫內容
bin/nutch readdb <crawl_db> -stats -sort
在原生輸出如下:
rawlDb statistics start: db/crawldbStatistics for CrawlDb: db/crawldbTOTAL urls:1retry 0:1min score:1.0avg score:1.0max score:1.0status 1 (db_unfetched):1 www.baidu.com :1CrawlDb statistics: done
3. Inject原始碼分析
我們知道Injector.java在Nutch原始碼中的位置為org.apache.nutch.crawl.Injector.java
其中有一個main入口函數,使用Hadoop的工具類ToolRunner來運行其執行個體
但其是終入口函數還是void inject(Path crawlDb, Path urlDir)
其中有兩個MP任務,第一個主要是把檔案格式的輸入轉換成<url,CrawlDatum>格式的輸出,這裡的
CrawlDatum是Nutch對於單個抓取url對象的一個抽象,其中有很多url的相關資訊
第二個MP主要是把上面新產生的輸出與舊的CrawlDb資料進行合并,產生一個新的CrawlDb
3.1 對於Inject中第一個MP任務的分析
第一個MP任務主要代碼如下:
JobConf sortJob = new NutchJob(getConf()); // 產生一個Nutch的配置抽象 sortJob.setJobName("inject " + urlDir); FileInputFormat.addInputPath(sortJob, urlDir); // 設定InputFormat,這裡為FileInputFormat,這裡要注意的是可以調用多次addInputPath這個方法,效果是會有多個輸入源 sortJob.setMapperClass(InjectMapper.class); // 這裡設定了Mapper方法,主要是用於解析、過濾和規格化url文本,把其轉換成<url,CrawlDatum>格式 FileOutputFormat.setOutputPath(sortJob, tempDir); // 這裡定義了一個輸出路徑,這裡的tempDir=mapred.temp.dir/inject-temp-Random() sortJob.setOutputFormat(SequenceFileOutputFormat.class); // 這裡配置了輸出格式,這裡為SequenceFileOutputFormat,這是MP的一種二進位輸出結構 sortJob.setOutputKeyClass(Text.class); // 這裡配置了MP的輸出<key,value>的類型,這裡為<Text,CrawlDatum> sortJob.setOutputValueClass(CrawlDatum.class); sortJob.setLong("injector.current.time", System.currentTimeMillis()); JobClient.runJob(sortJob); // 這裡用於提交任務到JobTracker,讓其運行任務
這裡對InjectMapper中的主要代碼進行分析:
這個類主要用於對url進行解析、過濾和規格化
public void map(WritableComparable key, Text value, OutputCollector<Text, CrawlDatum> output, Reporter reporter) throws IOException { String url = value.toString(); // value is line of text if (url != null && url.trim().startsWith("#")) { // 這裡以#號開頭的文本就過濾 /* Ignore line that start with # */ return; } // if tabs : metadata that could be stored // must be name=value and separated by \t float customScore = -1f; int customInterval = interval; Map<String,String> metadata = new TreeMap<String,String>(); // 設定屬性的一個容器 if (url.indexOf("\t")!=-1){ String[] splits = url.split("\t"); // 對一行文本進行切分 url = splits[0]; for (int s=1;s<splits.length;s++){ // find separation between name and value int indexEquals = splits[s].indexOf("="); if (indexEquals==-1) { // skip anything without a = continue; } String metaname = splits[s].substring(0, indexEquals); // 得到中繼資料的名字 String metavalue = splits[s].substring(indexEquals+1); // 得到中繼資料的值 if (metaname.equals(nutchScoreMDName)) { // 看是不是保留的中繼資料 try { customScore = Float.parseFloat(metavalue);} catch (NumberFormatException nfe){} } else if (metaname.equals(nutchFetchIntervalMDName)) { try { customInterval = Integer.parseInt(metavalue);} catch (NumberFormatException nfe){} } else metadata.put(metaname,metavalue); // 如果這個中繼資料不是保留的中繼資料,就放到容器中 } } try { url = urlNormalizers.normalize(url, URLNormalizers.SCOPE_INJECT); // 對url進行規格化,這裡調用的是plugins中的外掛程式 url = filters.filter(url); // filter the url // 以url進行過濾 } catch (Exception e) { if (LOG.isWarnEnabled()) { LOG.warn("Skipping " +url+":"+e); } url = null; } if (url != null) { // if it passes value.set(url); // collect it// 這裡產生一個CrawlDatum對象,設定一些url的初始化資料 CrawlDatum datum = new CrawlDatum(CrawlDatum.STATUS_INJECTED, customInterval); datum.setFetchTime(curTime); // 設定當前url的抓取時間 // now add the metadata Iterator<String> keysIter = metadata.keySet().iterator(); while (keysIter.hasNext()){ // 配置其中繼資料 String keymd = keysIter.next(); String valuemd = metadata.get(keymd); datum.getMetaData().put(new Text(keymd), new Text(valuemd)); }// 設定初始化分數 if (customScore != -1) datum.setScore(customScore); else datum.setScore(scoreInjected); try {// 這裡對url的分數進行初始化 scfilters.injectedScore(value, datum); } catch (ScoringFilterException e) { if (LOG.isWarnEnabled()) { LOG.warn("Cannot filter injected score for url " + url + ", using default (" + e.getMessage() + ")"); } }// Map 收集相應的資料,類型為<Text,CrawlDatum> output.collect(value, datum); } } }
3.2 第二個MP任務的分析
第二個MP任務主要是對crawlDb進行合并,原始碼如下:
// merge with existing crawl db JobConf mergeJob = CrawlDb.createJob(getConf(), crawlDb); // 這裡對Job進行相應的配置 FileInputFormat.addInputPath(mergeJob, tempDir); // 這裡配置了輸入的文本資料,就是上面第一個MP任務的輸出 mergeJob.setReducerClass(InjectReducer.class); // 這裡配置了Reduce的抽象類別,這裡會覆蓋上面createJob設定的Reduce類 JobClient.runJob(mergeJob); // 提交運行任務 CrawlDb.install(mergeJob, crawlDb); // 把上面新產生的目錄重新命名為crawlDb的標準檔案夾名,然後再刪除老的目錄 // clean up FileSystem fs = FileSystem.get(getConf()); fs.delete(tempDir, true); // 把第一個MP任務的輸出目錄刪除
下面是createJob的原始碼說明:
public static JobConf createJob(Configuration config, Path crawlDb) throws IOException {// 產生新的CrawlDb檔案名稱 Path newCrawlDb = new Path(crawlDb,Integer.toString(new Random().nextInt(Integer.MAX_VALUE))); JobConf job = new NutchJob(config); // 產生相應的Job配置抽象 job.setJobName("crawldb " + crawlDb); Path current = new Path(crawlDb, CURRENT_NAME); if (FileSystem.get(job).exists(current)) { // 如果存在老的CrawlDb目錄,將其加入InputPath路徑中,和上面的tempDir一起進行合并 FileInputFormat.addInputPath(job, current); }// NOTE:有沒有注意到這裡如果有老的CrawlDb目錄的話,那它的檔案格式是MapFileOutputFormat,而下面對其讀取用了SequenceFileInputFormat來讀,因為這兩個類底層都是調用了SequenceFile的Reader與Writer來讀寫的,所以可以通用。 job.setInputFormat(SequenceFileInputFormat.class); // 設定CrawlDb目錄檔案的格式為SequenceFileInputFormat job.setMapperClass(CrawlDbFilter.class); // 設定相應的Map操作,主要是過濾和規格化url job.setReducerClass(CrawlDbReducer.class); // 設定相應的Reduce操作,主要是對url進行彙總 FileOutputFormat.setOutputPath(job, newCrawlDb); // 設定新的輸出路徑 job.setOutputFormat(MapFileOutputFormat.class); // 設定輸出的格式,這裡是MapFileOutputFormat// 這裡設定了輸出的類型<Text,CrawlDatum> job.setOutputKeyClass(Text.class); job.setOutputValueClass(CrawlDatum.class); return job; }
下面來看看覆蓋的InjectReducer都幹了些什麼,部分原始碼如下:
public void reduce(Text key, Iterator<CrawlDatum> values,OutputCollector<Text, CrawlDatum> output, Reporter reporter) throws IOException { boolean oldSet = false;// 把相同url彙總後的結果進行處理,這個迴圈主要是新注入的url與老的url有沒有相同的,// 如果有相同的話就不設定其狀態,支援collect出去了 while (values.hasNext()) { CrawlDatum val = values.next(); if (val.getStatus() == CrawlDatum.STATUS_INJECTED) { injected.set(val); injected.setStatus(CrawlDatum.STATUS_DB_UNFETCHED); } else { old.set(val); oldSet = true; } } CrawlDatum res = null; if (oldSet) res = old; // don't overwrite existing value else res = injected; output.collect(key, res); } }
最後來看一下CrawlDb.install方法都幹了些什麼,其原始碼如下:
public static void install(JobConf job, Path crawlDb) throws IOException { Path newCrawlDb = FileOutputFormat.getOutputPath(job); // 得到第二個MP任務的輸出目錄 FileSystem fs = new JobClient(job).getFs(); Path old = new Path(crawlDb, "old"); Path current = new Path(crawlDb, CURRENT_NAME); // 得到CrawlDb的正規目錄名,也就是有沒有老的CrawlDb if (fs.exists(current)) {// 如果有老的CrawlDb目錄,把老的目錄名生命名為old這個名字 if (fs.exists(old)) fs.delete(old, true); // 這裡判斷old這個目錄是不是已經存在,如果存在就刪除之 fs.rename(current, old); } fs.mkdirs(crawlDb); fs.rename(newCrawlDb, current); // 這裡是把第二個MP任務的輸出目錄重新命名為current目錄,也就是正規目錄名 if (fs.exists(old)) fs.delete(old, true); // 刪除重使名後的老的CrawlDb目錄 Path lock = new Path(crawlDb, LOCK_NAME); LockUtil.removeLockFile(fs, lock); // 目錄解鎖 }
4. 總結
Inject主要是從文字檔中注入新的url,使其與老的crawlDb中的url進行合并,然後把老的CrawlDb目錄刪除,現把新產生的CrawlDb臨時目錄重新命名為CrawlDb目錄名。
流程如下:
url_dir -> MapReduce1(inject new urls) -> MapReduece2(merge new urls with old crawlDb) -> install new CrawlDb -> clean up