1. Inject 功能介紹
在Nutch中Inject是用來把文本格式的url列表注入到抓取數據庫中,一般是用來引導系統的初始化。其中文本格式的URL每一列包含一個url。
同時inject裏面保留了兩個元數據。
nutch.score : 允許設置特定url的分數
nutch.fetchInterval : 表示特定url的抓取間隔,單位爲毫秒。
e.g. http://www.nutch.org/ \t nutch.score=10 \t nutch.fetchInterval=2592000 \t userType=open_source
2. Inject 命令行執行
bin/nutch inject <url_dir> <crawl_db>
可以用如下命令來查看數據庫內容
bin/nutch readdb <crawl_db> -stats -sort
輸出如下
rawlDb statistics start: db/crawldb
Statistics for CrawlDb: db/crawldb
TOTAL urls: 1
retry 0: 1
min score: 1.0
avg score: 1.0
max score: 1.0
status 1 (db_unfetched): 1
www.baidu.com : 1
CrawlDb statistics: done
3. Inject 源代碼分析
org.apache.nutch.crawl.Injector ,使用hadoop提供的ToolRunner來運行其實例。最終的入口函數是void inject(Path crawlDb, Path urlDir).
共涉及到了兩個MapReduce。
1).主要功能是吧文件格式的輸入轉化成<url,CrawlDatum>格式的輸出,這裏的CrawlDatum是Nutch對於單個抓取url對象的一個抽象,其中有很多url相關信息。
2).主要功能是把上面新生成的輸出與舊的CrawlDB數據進行合併,生成一個新的CrawlDb.
3.1 第一個MR Job任務源碼分析
JobConf sortJob = new NutchJob(getConf()); //獲得一個Nutch Job配置
sortJob.setJobName("inject " + urlDir); //設置Job名稱
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);//配置了<key,value>類型,這裏爲<Text,CrawlDatum>
sortJob.setOutputValueClass(CrawlDatum.class);
sortJob.setLong("injector.current.time", System.currentTimeMillis());
JobClient.runJob(sortJob);//提交任務到JobTracker,讓其運行任務。
3.1.1 對InjectMapper源碼分析
這個類主要用於對url進行解析,過濾和規格化
public void map(WritableComparable key, Text value,
OutputCollector<Text, CrawlDatum> output, Reporter reporter)
throws IOException {
String url = value.toString(); // 將value轉化成string型
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>();//設置一個保存metabata的Map容器
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進行規格化,這裏調用的是plugings中的插件
url = filters.filter(url); // filter the 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 {
scfilters.injectedScore(value, datum);//這裏對url的分數進行初始化
} 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 第二個MR任務源碼分析
第二個MR任務主要是對CrawlDatum進行合併,源代碼如下:
//合併已經存在的crawldb
JobConf mergeJob = CrawlDb.createJob(getConf(), crawlDb); //配置相應的Job
FileInputFormat.addInputPath(mergeJob, tempDir); 配置輸入文本數據,就是上面的第一個MR任務的輸出
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); //把第一個MR任務的輸出目錄刪除
下面是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
job.setOutputKeyClass(Text.class);//這裏設置了輸出類型<Text,CrawlDatum>
job.setOutputValueClass(CrawlDatum.class);
return job;
}
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); //得到第二個MR任務的輸出目錄
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);//把第二個MR任務的輸出目錄重命名爲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臨時目錄重命名爲DrawlDb目錄名
具體流程如下:
url_dir-->MapReduce1(Inject new urls)-->MapReduce2(merge new urls with old crawlDb)-->install new CrwalDb-->cleanup
主要參考 lemo專欄