Nutch1.7 generator源码阅读

本文深入探讨Nutch1.7的生成过程,涉及MapReduce任务,包括选择URL子集的过滤与规范化、生成segment的逻辑,以及CrawlDbUpdater类的角色。分析了partitionSegment函数和CrawlDbUpdater如何处理URL,确保segment的合理分布,并遵循用户设定的最大段数和每个段内的URL限制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

以下主要介绍了下generator的第一个MapReduce
首先generate可能主要分成三部分,

  1.    + 第一部分是产生要抓取的url子集,进行相应的过滤和规格化操作
  2.    + 第二部分是读取上面产生的url子集,生成多个segment
  3.    + 第三部分是更新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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值