Nutch 學習比較2 ---------Generate過程

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的專欄

發佈了20 篇原創文章 · 獲贊 3 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章