以下主要介绍了下generator的第一个MapReduce
首先generate可能主要分成三部分,
- + 第一部分是产生要抓取的url子集,进行相应的过滤和规格化操作
- + 第二部分是读取上面产生的url子集,生成多个segment
- + 第三部分是更新crawldb数据库,以保证下一次Generate不会包含相同的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); // 配置CrawlDb的输入格式
// 配置Mapper,Partitioner和Reducer,这里都是Selector,因为它继承了这三个抽象接口
job.setMapperClass(Selector.class);
job.setPartitionerClass(Selector.class);
job.setReducerClass(Selector.class);
FileOutputFormat.setOutputPath(job, tempDir);
// 配置输出格式
job.setOutputFormat(SequenceFileOutputFormat.class);
// 配置输出的<key,value>的类型<FloatWritable,SelectorEntry>
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;
}
看一下generator的第一部分的Map做了些什么工作。
public void map(Text key, CrawlDatum value,
OutputCollector<FloatWritable,SelectorEntry> output, Reporter reporter)
throws IOException {
Text url = key;
if (filter) {
// If filtering is on don't generate URLs that don't pass
// URLFilters
try {
//lter the bad urls 符不符合设定的过滤器的要求,可以根据扩展点要求进行过滤。不满足要求就过滤掉
if (filters.filter(url.toString()) == null) return;
} catch (URLFilterException e) {
if (LOG.isWarnEnabled()) {
LOG.warn("Couldn't filter url: " + url + " (" + e.getMessage() + ")");
}
}
}
CrawlDatum crawlDatum = value;
// check fetch schedule 检查该url添加到抓取列表中,根据抓取时间,但是,不一定在列表中就一定抓取
if (!schedule.shouldFetch(url, crawlDatum, curTime)) {
LOG.debug("-shouldFetch rejected '" + url + "', fetchTime="
+ crawlDatum.getFetchTime() + ", curTime=" + curTime);
return;
}
LongWritable oldGenTime = (LongWritable) crawlDatum.getMetaData().get(
Nutch.WRITABLE_GENERATE_TIME_KEY);
if (oldGenTime != null) { // awaiting fetch & update
if (oldGenTime.get() + genDelay > curTime) // still wait for
// update
return;
}
float sort = 1.0f;
try {
sort = scfilters.generatorSortValue(key, crawlDatum, sort);// Calculate a sort value for Generate
} catch (ScoringFilterException sfe) {
if (LOG.isWarnEnabled()) {
LOG.warn("Couldn't filter generatorSortValue for " + key + ": " + sfe);
}
}
// 一种限制如果是一种限制的状态,就不进行抓取。过滤
if (restrictStatus != null
&& !restrictStatus.equalsIgnoreCase(CrawlDatum.getStatusName(crawlDatum.getStatus()))) return;
// consider only entries with a score superior to the threshold
//根据排分阈值来过滤
if (scoreThreshold != Float.NaN && sort < scoreThreshold) return;
// consider only entries with a retry (or fetch) interval lower than threshold
// 根据抓去间隔进行过滤,貌似都是在inject步骤产生的
if (intervalThreshold != -1 && crawlDatum.getFetchInterval() > intervalThreshold) return;
// sort by decreasing score, using DecreasingFloatComparator
sortValue.set(sort);
// record generation time
crawlDatum.getMetaData().put(Nutch.WRITABLE_GENERATE_TIME_KEY, genTime);//MapWritable是对writable的键值对实现
entry.datum = crawlDatum;
entry.url = key;
output.collect(sortValue, entry); // invert for sort by score 最后的输出格式形如(分数,(url,datum))的键值对
}
其中SelectorEntry的结构如下所示,是一个定制的Writable。map中的entry就是它的一个实例。
public static class SelectorEntry implements Writable {
public Text url;
public CrawlDatum datum;
public IntWritable segnum;
public SelectorEntry() {
url = new Text();
datum = new CrawlDatum();
segnum = new IntWritable(0);
}
public void readFields(DataInput in) throws IOException {
url.readFields(in);
datum.readFields(in);
segnum.readFields(in);
}
public void write(DataOutput out) throws IOException {
url.write(out);
datum.write(out);
segnum.write(out);
}
public String toString() {
return "url=" + url.toString() + ", datum=" + datum.toString() + ", segnum="
+ segnum.toString();
}
}
总的来说:generate 的Map主要完成了下面的几项功能:
1、根据url的格式和抓取时间,抓取间隔进行过滤。
2、如果顺利通过步骤1,没有被过滤掉。就产生该url的分数,将其写入到输出中。
下面是reduce的具体实现:
public void reduce(FloatWritable key, Iterator<SelectorEntry> values,
OutputCollector<FloatWritable,SelectorEntry> output, Reporter reporter)
throws IOException {
while (values.hasNext()) {
if (count == limit) {//取最顶部的N个链接.limit就是在命令行下输入的TOPN 。 job.setLong(GENERATOR_TOP_N, topN);
// do we have any segments left?
if (currentsegmentnum < maxNumSegments) {
count = 0;
currentsegmentnum++;
} else break;//最后是 limit * maxNumSegments 数量个url被加入到列表中。这些url都是同样地分数
}
SelectorEntry entry = values.next();
Text url = entry.url;
String urlString = url.toString();
URL u = null;
String hostordomain = null;
try {
if (normalise && normalizers != null) { //对url进行标准化
// return A normalized String, using the given scope
urlString = normalizers.normalize(urlString,
URLNormalizers.SCOPE_GENERATE_HOST_COUNT);
}
u = new URL(urlString);
if (byDomain) {
hostordomain = URLUtil.getDomainName(u);//by domain @ll
} else {
hostordomain = new URL(urlString).getHost();//by Host @ll
}
} catch (Exception e) {
LOG.warn("Malformed URL: '" + urlString + "', skipping ("
+ StringUtils.stringifyException(e) + ")");
reporter.getCounter("Generator", "MALFORMED_URL").increment(1);
continue;
}
hostordomain = hostordomain.toLowerCase();
// only filter if we are counting hosts or domains
if (maxCount > 0) {
int[] hostCount = hostCounts.get(hostordomain);// hostCounts = new HashMap<String,int[]>()
if (hostCount == null) {
hostCount = new int[] {1, 0};
hostCounts.put(hostordomain, hostCount);
}
// increment hostCount
hostCount[1]++;
// check if topN reached, select next segment if it is
// segCounts = new int[maxNumSegments];最大的segments的数量
// segCounts控制每个segment中的url数量,作为记录。是全局的
// 检查该segment(hostCount[0]-1)中的数量是不是达到limit
while (segCounts[hostCount[0]-1] >= limit && hostCount[0] < maxNumSegments) {
hostCount[0]++;
hostCount[1] = 0;
}
// reached the limit of allowed URLs per host / domain
// see if we can put it in the next segment?
if (hostCount[1] >= maxCount) {
if (hostCount[0] < maxNumSegments) {
hostCount[0]++;
hostCount[1] = 0;
} else {
if (hostCount[1] == maxCount + 1 && LOG.isInfoEnabled()) {
LOG.info("Host or domain " + hostordomain + " has more than " + maxCount
+ " URLs for all " + maxNumSegments + " segments. Additional URLs won't be included in the fetchlist.");
}
// skip this entry
continue;
}
}
entry.segnum = new IntWritable(hostCount[0]);//segnum 记录每个url的segmentNum
segCounts[hostCount[0]-1]++;
} else {
entry.segnum = new IntWritable(currentsegmentnum);
segCounts[currentsegmentnum-1]++;
}
output.collect(key, entry);
// Count is incremented only when we keep the URL
// maxCount may cause us to skip it.
count++;
}//end the while
}
}
在这个方法中,最重要的是:
在一个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中去。
int[] hostCount = hostCounts.get(hostordomain);
hostCounts.put(hostordomain, hostCount);
最终hostCounts是使用了形如这样的结构:(hostordomain,[SegmentNum,UrlNum])
表示的意思:同一个hostordomain在segmentNum中现在共有UrlNum个数量的url。比如(com,[2,34])就表示com域的现在已经存放到了2个segment中了。当然这些url同属于一个域。当然这个2不能表示现在只产生了2个segment,它仅表示这个域占用了2个segment用来存储这些urls。这个域目前总的url数量不一定等于1*maxCount+34,因为每个segment总的url数量limit的限制。
基于senmentNum的上限maxNumSegments,和每个segment中的limit,和每个域的最多url数量maxCount。所以产生了对他们的逻辑判断。
还有一个变量需要说明的是:segCounts = new int[maxNumSegments];
他会记录每个segmentNum所对应的总的url数量,用于判断是否超过limit。
第二部分是读取上面产生的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函数进行分析,看看到底做了些什么
// 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);
// 下面又用一个MP任务来做
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); // 这里的Reducer类,
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;
第三部分是更新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()); // 生成MP任务的配置
job.setJobName("generate: updatedb " + dbDir);
job.setLong(Nutch.GENERATE_TIME_KEY, generateTime);
// 加上面生成的所有segment的路径做为输入
for (Path segmpaths : generatedSegments) { // add each segment dir to input path
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方法做了些什么 genTime.set(0L);
while (values.hasNext()) { // 这里遍历相同url的CrawlDatum值
CrawlDatum val = values.next();
if (val.getMetaData().containsKey(Nutch.WRITABLE_GENERATE_TIME_KEY)) { // 判断当前url是否已经被generate过
LongWritable gt = (LongWritable) val.getMetaData().get(
Nutch.WRITABLE_GENERATE_TIME_KEY); // 得到Generate的时间
genTime.set(gt.get());
if (genTime.get() != generateTime) { // 还没看明白这里是什么意思
orig.set(val);
genTime.set(0L);
continue;
}
} else {
orig.set(val);
}
}
if (genTime.get() != 0L) {
orig.getMetaData().put(Nutch.WRITABLE_GENERATE_TIME_KEY, genTime); // 设置新的Generate时间
}
output.collect(key, orig);
部分引用: http://blog.youkuaiyun.com/amuseme_lu/article/details/6720079