1. Generate的作業
在inject 之後就是Generate,這個方法主要是從CrawlDb中產生一個Fetch可以抓取的url集合(fetchlist). 這Nutch 1.3 版本中,支持在一次Generate爲多個segment產生相應的fetchlists,而IP地址的解析只針對那些準備被抓取的url,在一個segment中,所有url都以IP,domain或者host來分類。它的命令行表示爲:
bin/nutch generate Usage: Generator <crawldb> <segments_dir> [-force] [-topN N] [-numFetchers numFetchers] [-adddays numDays] [-noFilter] [-noNorm][-maxNumSegments num]
參數說明:
*crawldb:crawldb的相對路徑。
*segments:segments的相對路徑。
*force:這個主要是對目錄進行加鎖用的配置。如果爲true,當目標鎖文件存在的,會認爲是有效的。如果爲false.當目標文件存在時。就會拋出IOException
*topN:表示產生TonN個url。
*numFetchers:表示Generate的MR任務要幾個Reducer節點,也就是要幾個輸出文件,這個配置會影響到Fetch的Map個數。
*numDays:表示當前日期,在對url過濾中要用到。
*noFilter:表示是否對url進行過濾。
*noNorm:表示對否對url進行規格化。
*maxNumSegments:表示segments的最大個數。
2. Generate 源碼分析
它主要分成三個部分:
1. +第一部分是產生要抓取的url子集,進行相應的過濾和規格化處理。
2. +第二部分是讀取上面產生的url子集,生成多個segment
3. +第三部分是更新crawldb數據庫,以保證下一次Generate不會包含相同的url
2.1 第一部分,產生url子集分析
主要分析其中的MR任務。用於產生相應的url抓取集合。主要代碼:
// map to inverted subset due for fetch, sort by score
JobConf job = new NutchJob(getConf());
job.setJobName("generate: select from " + dbDir);
//如果沒有設置numFetchers值,默認爲Map的個數
if (numLists == -1) { // for politeness make
numLists = job.getNumMapTasks(); // a partition per fetch task
}
//如果MapReduce的設置爲local,那麼就產生一個輸出文件
//Note:這裏partition也是Hadoop的一個概念,就是在Map後,它會對每一個key進行partition操作,看這個key會映射到那一個reduce上。
//所有相同的key的value就會聚合到這個reduce節點上
if ("local".equals(job.get("mapred.job.tracker")) && numLists != 1) {
// override
LOG.info("Generator: jobtracker is 'local', generating exactly one partition.");
numLists = 1;
}
job.setLong(GENERATOR_CUR_TIME, curTime);
// record real generation time
long generateTime = System.currentTimeMillis();
job.setLong(Nutch.GENERATE_TIME_KEY, generateTime);
job.setLong(GENERATOR_TOP_N, topN);
job.setBoolean(GENERATOR_FILTER, filter);
job.setBoolean(GENERATOR_NORMALISE, norm);
job.setInt(GENERATOR_MAX_NUM_SEGMENTS, maxNumSegments);
//配置輸入路徑
FileInputFormat.addInputPath(job, new Path(dbDir, CrawlDb.CURRENT_NAME));
job.setInputFormat(SequenceFileInputFormat.class);
//配置mapper,partitioner和Reducer.這裏都是selector,因爲它繼承了這三個抽象接口
job.setMapperClass(Selector.class);
job.setPartitionerClass(Selector.class);
job.setReducerClass(Selector.class);
//配置輸出格式
FileOutputFormat.setOutputPath(job, tempDir);
//配置輸出的<key,value>的類型<FloatWritable,SelectorEntry>
job.setOutputFormat(SequenceFileOutputFormat.class);
job.setOutputKeyClass(FloatWritable.class);
//因爲Map的輸出會安裝哦key的值來進行排序,所以這裏擴展了一個排序比較方法
job.setOutputKeyComparatorClass(DecreasingFloatComparator.class);
job.setOutputValueClass(SelectorEntry.class);
//設置輸出格式,這個類繼承自OutputFormat,如果需要擴展自己的outputFormat,那必須繼承自這個抽象接口
job.setOutputFormat(GeneratorOutputFormat.class);
try {
JobClient.runJob(job);
} catch (IOException e) {
throw e;
}
下面分析一下Selector 這個類。它使用了多重繼承,同時實現了三個接口,Selector類中Mapper分析
其中Map的工作如下:
* 如果有filter設置,先對url進行過濾
* 通過FetchSchedule查看當前url是不是達到了抓取的時間,沒有達到的就過濾掉
* 計算新的排序分數,根據url的當前分數,這裏調用了ScoringFilters的generatorSortValue方法
*對上一步產生的分數進行過濾,當這個分數小於一定的閥值時,對url進行過濾
*蒐集所有沒有被過濾的url信息,輸出爲<FloatWritable,SelectorEntry>類型,這裏的key就是第三步
計算出來的分數,在Map的輸出會調用DecreasingFloatComparator方法來對這個key進行排序
其中Partition方法主要是調用URLPartition來進行相應的分塊操作
這裏會首先根據url的hashCode來進行partition,如果用戶設置了根據domain或則ip來進行partition,那麼
這裏會根據用戶的配置類進行相應的partition操作,最後調用如下的方法來得到一個映射的reduceID號
(hashCode&Integer.MAX_VALUE)%numReduceTasks;
其中Reducer操作主要是收集沒有被過濾的url,每個reducer的url個數不會超過limit的個數,這個Limit通過入下公式計算
limit = job.getLong(GENERATOR_TOP_N, Long.MAX_VALUE) / job.getNumReduceTasks();
GENERATOR_TOP_N是用戶定義的,reducer的個數也是用戶定義的。在一個reducer任務中,如果收集的url個數超過了這個limit,那就新開一個segment,這裏的segment也有一個上限,就是用戶設置的maxNumSegments, 當新開的segment個數大小這個maxNumSegment時,url就會被過濾掉。
這裏url在segment中的分佈有兩個情況,一種是當沒有設置GENERATOR_MAX_COUNT這個參數時,每一個segment中所包含的url個數不超過limit上限,segmetn中對url的host個數沒有限制,而segment個數的上限爲maxNumSegments這個變量的值,這個變量是通過設置GENERATOR_MAX_NUM_SEGMENTS這個參數得到的,默認爲1,所以說默認只產生一個segment; 而當設置了GENERATOR_MAX_COUNT的時候,每一個segment中所包含的url的host的個數的上限就是這個maxCount的值,也就是說每一個segment所包含的同一個host的url的個數不能超過maxCount這個值,當超過這個值後,就把這個url放到下一個segment中去。
舉個簡單的例子,如果Reducer中收到10個url,而現在maxNumSegments爲2,limit爲5,也就是說一個segment最多放5個url,那這時如果用第一種設置的話,那0-4個url會放在第一個segment中,5-9個url會放在第二個segment中,這樣的話,兩個segment都放了5個url;但如果用第二種方法,這裏設置的maxCount爲4,但我們這裏的10個url按host分成2類,也就是說0-4個url屬於同一個host1, 5-9個url屬於host2,那這裏會把0-4箇中的前4個url放在segment1中,host1的第5個url放在segmetn2中,而host2中的5-8個url會放在segment1中,而第9個網頁會放在segment2中,因爲這裏的maxCount設置爲4,也就是說在每一個segment中,一個host所對應的url不能超過4,所以這裏的segment1放了8個url,而segment2放了2個url,這裏會現出不均勻的情況。* 有沒有注意到這裏的OutputFormat使用了GenerateOutputFormat,它擴展了MultipleSequenceFileOutputFormat,重寫了generateFileNameForKeyValue這個方法,就是對不同的segment生成不同的目錄名,生成規則如下
"fetchlist-" + value.segnum.toString() + "/" + name;
2.2 第二部分是讀取上面產生的url子集,生成多個segment,源代碼分析如下
// read the subdirectories generated in the temp
// output and turn them into segments
List<Path> generatedSegments = new ArrayList<Path>();
FileStatus[] status = fs.listStatus(tempDir);//這裏讀取上面生成的多個fetchlist的segment
try {
for (FileStatus stat : status) {
Path subfetchlist = stat.getPath();
if (!subfetchlist.getName().startsWith("fetchlist-")) continue;//過濾不是以fetchlist-開頭的文件
// start a new partition job for this segment
Path newSeg = partitionSegment(fs, segments, subfetchlist, numLists);對segment進行Partition操作,產生一個新的目錄
generatedSegments.add(newSeg);
}
} catch (Exception e) {
LOG.warn("Generator: exception while partitioning segments, exiting ...");
fs.delete(tempDir, true);
return null;
}
if (generatedSegments.size() == 0) {
LOG.warn("Generator: 0 records selected for fetching, exiting ...");
LockUtil.removeLockFile(fs, lock);
fs.delete(tempDir, true);
return null;
}
接下來主要對partitionSegment函數進行分析,看看到底做了些什麼
private Path partitionSegment(FileSystem fs, Path segmentsDir, Path inputDir,
int numLists) throws IOException {
// invert again, partition by host/domain/IP, sort by url hash
//這裏主要是對url按照host/domain/IP進行分類
//Note:這裏的分類就是partition的意思,就是相同的host或則domain或則IP的url發到同一臺機器上
//這裏主要是通過URLPartitioner來做的。具體是按那一個類來分類。是通過參數來進行配置,這裏有PARTITION_MODE_DOMAIN,
PARTITION_MODE_IP來配置的,默認是按URL的hashCode來分。
if (LOG.isInfoEnabled()) {
LOG.info("Generator: Partitioning selected urls for politeness.");
}
Path segment = new Path(segmentsDir, generateSegmentName());//也是在segmentDir目錄產生一個新的目錄,以當前時間命名
Path output = new Path(segment, CrawlDatum.GENERATE_DIR_NAME);//在上面的目錄下,再生成一個特定的crawl_generate目錄
LOG.info("Generator: segment: " + segment);
//又是用一個MR任務
NutchJob job = new NutchJob(getConf());
job.setJobName("generate: partition " + segment);
job.setInt("partition.url.seed", new Random().nextInt()); //這裏產生一個partition的隨機數
FileInputFormat.addInputPath(job, inputDir); //輸入目錄名
job.setInputFormat(SequenceFileInputFormat.class);//輸出文件格式
job.setMapperClass(SelectorInverseMapper.class);//輸入的Mapper,主要是過濾原來的key,使用url來作爲新的key
job.setMapOutputKeyClass(Text.class);Mapper的key輸出類型,這裏就是url的類型
job.setMapOutputValueClass(SelectorEntry.class);//Mapper的value的輸出類型,這裏還是原來的SelectorEntry類型
job.setPartitionerClass(URLPartitioner.class);//這裏的key(url)的Partition使用這個類來做,這個類前面有說明
job.setReducerClass(PartitionReducer.class);//Reduce類
job.setNumReduceTasks(numLists);//設置Reducer的個數,也就是生成幾個相應的輸出文件
FileOutputFormat.setOutputPath(job, output);//配置輸出路徑
job.setOutputFormat(SequenceFileOutputFormat.class);//配置輸出格式
job.setOutputKeyClass(Text.class);//配置輸出的key與value的類型
job.setOutputValueClass(CrawlDatum.class);//注意這裏的返回類型爲<Text,CrawlDatum>
job.setOutputKeyComparatorClass(HashComparator.class);//這裏定義控制key排序的比較方法
JobClient.runJob(job);//提交任務
return segment;
}
2.3 第三部分是更新crawlDb數據庫,以保證下一次的Generate不包含相同的url,這個是可以配置的。源代碼分析如下:
if (getConf().getBoolean(GENERATE_UPDATE_CRAWLDB, false)) {//判斷是否要把狀態更新到原來的數據庫中
// update the db from tempDir
Path tempDir2 = new Path(getConf().get("mapred.temp.dir", ".") + "/generate-temp-"
+ System.currentTimeMillis());
job = new NutchJob(getConf());//生成MR任務的配置
job.setJobName("generate: updatedb " + dbDir);
job.setLong(Nutch.GENERATE_TIME_KEY, generateTime);
// 將上面生成的所有segment的路徑作爲輸入
for (Path segmpaths : generatedSegments) {
Path subGenDir = new Path(segmpaths, CrawlDatum.GENERATE_DIR_NAME);
FileInputFormat.addInputPath(job, subGenDir);
}
//add current crawldb to input path
//把數據庫的路徑也作爲輸入
FileInputFormat.addInputPath(job, new Path(dbDir, CrawlDb.CURRENT_NAME));
job.setInputFormat(SequenceFileInputFormat.class);//定義了輸入格式
job.setMapperClass(CrawlDbUpdater.class);//定義了Mapper與Reducer方法
job.setReducerClass(CrawlDbUpdater.class);
job.setOutputFormat(MapFileOutputFormat.class);//定義了輸出格式
job.setOutputKeyClass(Text.class);//定義了輸出的key與value的類型
job.setOutputValueClass(CrawlDatum.class);
FileOutputFormat.setOutputPath(job, tempDir2);//定義了臨時輸出目錄
try {
JobClient.runJob(job);
CrawlDb.install(job, dbDir); //刪除原來的數據庫,把上面的臨時輸出目錄重命名爲真正的數據目錄名
} catch (IOException e) {
LockUtil.removeLockFile(fs, lock);
fs.delete(tempDir, true);
fs.delete(tempDir2, true);
throw e;
}
fs.delete(tempDir2, true);
}
接下來看一下CrawlDbUpdater類的功能,它實現了Mapper與Reducer的接口,接口說明如下:
它是用來更新CrawlDb數據庫,以保證下一次Generate不會包含相同的url
它的map函數很簡單。只是收集相應的<key,value>操作,沒有做其他操作。下面我們來看一下它的reduce方法
public void reduce(Text key, Iterator<CrawlDatum> values,
OutputCollector<Text,CrawlDatum> output, Reporter reporter) throws IOException {
genTime.set(0L);
while (values.hasNext()) {//這裏遍歷所有相同的url的CrawlDatum值
CrawlDatum val = values.next();
//判定當前url是否已經被generate過
if (val.getMetaData().containsKey(Nutch.WRITABLE_GENERATE_TIME_KEY)) {
LongWritable gt = (LongWritable) val.getMetaData().get(
Nutch.WRITABLE_GENERATE_TIME_KEY);
genTime.set(gt.get());//得到Generate的時間
if (genTime.get() != generateTime) {
orig.set(val);
genTime.set(0L);
continue;
}
} else {
orig.set(val);
}
}
if (genTime.get() != 0L) {//Note: 想想這裏什麼時候genTime爲0,當這個url被過濾掉,或則沒有
符合Generate要求,或則分數小於相應的閥值時
orig.getMetaData().put(Nutch.WRITABLE_GENERATE_TIME_KEY, genTime);//設置新的Generate時間
}
output.collect(key, orig);
}
}
3.總結
簡略的介紹了下Generate的流程,其中大量用到了MR任務。要有大量的配置,要深入理解還需要自己多多實踐從而加深理解。。參考Lemo的專欄