-----------------------------------
上一節看了Fetcher中主要幾個類的實現,這一節會來分析一下其中用到的消費者FetcherThread,來看看它是幹嘛的。
1. Fetcher的Mapp模型
Fetcher.java代碼中可以看到,Fetcher繼承自MapRunable,它是Mapper的抽象接口,實現這個接口的子類能夠更好的對Map的流程進行控制,包括多線程與異步Maper。1.1 Fetcher的入口函數fetch(Path segment,int threads, boolean parsing)
下面是它的源代碼,來分析一下:- // 對配置進行檢測,看一些必要的配置是否已經配置了,如http.agent.name等參數
- checkConfiguration();
- // 記錄fetch的開始時間
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- long start = System.currentTimeMillis();
- if (LOG.isInfoEnabled()) {
- LOG.info("Fetcher: starting at " + sdf.format(start));
- LOG.info("Fetcher: segment: " + segment);
- }
- // 這裏對抓取的時候進行限制,在FetchItemQueue中會用到這個參數
- // set the actual time for the timelimit relative
- // to the beginning of the whole job and not of a specific task
- // otherwise it keeps trying again if a task fails
- long timelimit = getConf().getLong("fetcher.timelimit.mins", -1);
- if (timelimit != -1) {
- timelimit = System.currentTimeMillis() + (timelimit * 60 * 1000);
- LOG.info("Fetcher Timelimit set for : " + timelimit);
- getConf().setLong("fetcher.timelimit", timelimit);
- }
- // 生成一個Nutch的Map-Reduce配置
- JobConf job = new NutchJob(getConf());
- job.setJobName("fetch " + segment);
- // 配置抓取線程數,
- job.setInt("fetcher.threads.fetch", threads);
- job.set(Nutch.SEGMENT_NAME_KEY, segment.getName());
- // 配置是否對抓取的內容進行解析
- job.setBoolean("fetcher.parse", parsing);
- // for politeness, don't permit parallel execution of a single task
- job.setSpeculativeExecution(false);
- // 配置輸出的路徑名
- FileInputFormat.addInputPath(job, new Path(segment, CrawlDatum.GENERATE_DIR_NAME));
- // 配置輸入的文件格式,這裏類繼承自SequenceFileInputFormat
- // 它主要是覆蓋了其getSplits方法,其作用是不對文件進行切分,以文件數量作爲splits的依據
- // 就是有幾個文件,就有幾個Map操作
- job.setInputFormat(InputFormat.class);
- // 配置Map操作的類
- job.setMapRunnerClass(Fetcher.class);
- // 配置輸出路徑
- FileOutputFormat.setOutputPath(job, segment);
- // 這裏配置輸出文件方法,這個類在前面已經分析過
- job.setOutputFormat(FetcherOutputFormat.class);
- // 配置輸出<key,value>類型
- job.setOutputKeyClass(Text.class);
- job.setOutputValueClass(NutchWritable.class);
- JobClient.runJob(job);
1.2 Fetcher的run方法分析
這個是Map類的入口,用於啓動抓取的生產者與消費者,下面是部分源代碼:- // 生成生產者,用於讀取Generate出來的CrawlDatum,把它們放到共享隊列中
- feeder = new QueueFeeder(input, fetchQueues, threadCount * 50);
- //feeder.setPriority((Thread.MAX_PRIORITY + Thread.NORM_PRIORITY) / 2);
- // the value of the time limit is either -1 or the time where it should finish
- long timelimit = getConf().getLong("fetcher.timelimit", -1);
- if (timelimit != -1) feeder.setTimeLimit(timelimit);
- feeder.start();
- // set non-blocking & no-robots mode for HTTP protocol plugins.
- getConf().setBoolean(Protocol.CHECK_BLOCKING, false);
- getConf().setBoolean(Protocol.CHECK_ROBOTS, false);
- // 啓動消費者線程
- for (int i = 0; i < threadCount; i++) { // spawn threads
- new FetcherThread(getConf()).start();
- }
- // select a timeout that avoids a task timeout
- long timeout = getConf().getInt("mapred.task.timeout", 10*60*1000)/2;
- // 這裏用一個循環來等待線程結束
- do { // wait for threads to exit
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {}
- // 這個函數是得到相前線程的抓取狀態,如抓取了多少網頁,多少網頁抓取失敗,抓取速度是多少
- reportStatus();
- LOG.info("-activeThreads=" + activeThreads + ", spinWaiting=" + spinWaiting.get()
- + ", fetchQueues.totalSize=" + fetchQueues.getTotalSize());
- // 輸出抓取隊列中的信息
- if (!feeder.isAlive() && fetchQueues.getTotalSize() < 5) {
- fetchQueues.dump();
- }
- // 查看timelimit的值,這裏只要返回的hitByTimeLimit不爲0,checkTimelimit方法會清空抓取隊列中的所有數據
- // check timelimit
- if (!feeder.isAlive()) {
- int hitByTimeLimit = fetchQueues.checkTimelimit();
- if (hitByTimeLimit != 0) reporter.incrCounter("FetcherStatus",
- "hitByTimeLimit", hitByTimeLimit);
- }
- // 查看抓取抓取線程是否超時,如果超時,就退出等待
- // some requests seem to hang, despite all intentions
- if ((System.currentTimeMillis() - lastRequestStart.get()) > timeout) {
- if (LOG.isWarnEnabled()) {
- LOG.warn("Aborting with "+activeThreads+" hung threads.");
- }
- return;
- }
- } while (activeThreads.get() > 0);
- LOG.info("-activeThreads=" + activeThreads);
2. Fetcher.FetcherThread
2.1 這個類主要是用來從隊列中得到FetchItem,下面來看一下其run方法,其大概做了幾件事:
- 從抓取隊列中得到一個FetchItem,如果返回爲null,判斷生產者是否還活着或者隊列中是否還有數據, 如果隊列中還有數據,那就等待,如果上面條件沒有滿足,就認爲所有FetchItem都已經處理完了,退出當前抓取線程
- 得到FetchItem, 抽取其url,從這個url中分析出所使用的協議,調用相應的plugin來解析這個協議
- 得到相當url的robotRules,看是否符合抓取規則,如果不符合或者其delayTime大於我們配置的maxDelayTime,那就不抓取這個網頁
- 對網頁進行抓取,得到其抓取的Content和抓取狀態,調用FetchItemQueues的finishFetchItem方法,表明當前url已經抓取完成
- 根據抓取協議的狀態來進行下一步操作
- 如果狀態爲WOULDBLOCK,那就進行retry,把當前url放加FetchItemQueues中,進行重試
- 如果是MOVED或者TEMP_MOVED,這時這個網頁可以被重定向了,對其重定向的內容進行解析,得到重定向的網址,這時要生成一個新的FetchItem,根據其QueueID放到相應的隊列的inProgress集合中,然後再對這個重定向的網頁進行抓取
- 如果狀態是EXCEPTION,對當前url所屬的FetchItemQueue進行檢測,看其異常的網頁數有沒有超過最大異常網頁數,如果大於,那就清空這個隊列,認爲這個隊列中的所有網頁都有問題。
- 如果狀態是RETRY或者是BLOCKED,那就輸出CrawlDatum,將其狀態設置成STATUS_FETCH_RETRY,在下一輪進行重新抓取
- 如果狀態是GONE,NOTFOUND,ACCESS_DENIED,ROBOTS_DENIED,那就輸出CrawlDatum,設置其狀態爲STATUS_FETCH_GONE,可能在下一輪中就不進行抓取了,
- 如果狀態是NOTMODIFIED,那就認爲這個網頁沒有改變過,那就輸出其CrawlDatum,將其狀態設成成STATUS_FETCH_NOTMODIFIED.
- 如果所有狀態都沒有找到,那默認輸出其CrawlDatum,將其狀態設置成STATUS_FETCH_RETRY,在下一輪抓取中再重試
- 判斷網頁重定向的次數,如果超過最大重定向次數,就輸出其CrawlDatum,將其狀態設置成STATUS_FETCH_GONE
這裏有一些細節沒有說明,如網頁被重定向以後如果操作,相應的協議是如果產生的,這個是通過插件產生的,具體插件是怎麼調用的,這裏就不說了,以後有機會會再分析一下。
2.2 下面分析FetcherThread中的另外一個比較重要的方法,就是output
具體這個output大概做了如下幾件事:- 判斷抓取的content是否爲空,如果不爲空,那調用相應的解析插件來對其內容進行解析,然後就是設置當前url所對應的CrawlDatum的一些參數,如當前內容的MD5碼,分數等信息
- 然後就是使用FetchOutputFormat輸出當前url的CrawlDatum,Content和解析的結果ParseResult
在生成相應的ParseOutputFormat的RecordWriter過程中,這個RecordWriter會再生成三個RecordWriter來寫出parse_text(MapFile),parse_data(MapFile)和crawl_parse(SequenceFile),我們在segments下具體的segment中看到的三個這樣的目錄就是這個對象生成的,分別輸出了網頁的源代碼;網頁的解析數據,如網頁title、外鏈接、元數據、狀態等信息,這裏會對外鏈接進行過濾、規格化,並且用插件計算每一個外鏈接的初始分數;另一個是網頁解析後的CrawlDatum對象,這裏會分析當前CrawlDatum中的metadata,從中生成兩種新的CrawlDatum,還有就是它會對外鏈接生成相應的CrawlDatum,放入crawl_parse目錄中,這裏我還沒有看明白。
3. 總結
有點暈了,這裏的代碼有點複雜,我們來整理一下思路。3.1 從目錄生成的角度
- 從Generate後會在segments目錄下生成一些要抓取的具體的segment,這裏每一個segment下會有一個叫crawl_generate的目錄,其中放着要抓取CrawlDatum信息
- 在Fetch的時候,會輸出另外五個目錄
- content: 這個目錄只有在配置了要輸出抓取內容時纔會輸出
- crawl_fetch: 這個目錄是輸出抓取成功後的CrawlDatum信息,這裏是對原來crawl_generate目錄中的信息進行了一些修改,下面三個目錄只有配置瞭解析參數後纔會輸出,如果後面調用bin/nutch parse命令
- parse_text: 這個目錄存放了抓取的網頁內容,以提後面建立索引用
- parse_data: 這裏存入了網頁解析後的一些數據,如網頁title,外鏈接信息等
- crawl_parse: 這裏存儲了一些新生成的CrawlDatum信息,如外鏈接等,以供下一次迭代抓取使用
3.2 從數據流的角度
- Generate生成的CrawlDatum數據首先經過QueueFeeder生產者,放入共享隊列
- 多個消費者(FetcherThread)從共享隊列中取得要抓取的FetchItem數據
- 對FetchItem所對應的url進行抓取,得到相應的抓取內容,對抓取的狀態進行判斷,回調相應的操作
- 對抓取的內容進行解析,產生網頁的外鏈接,生成新的CrawlDatum抓取數據,產生解析後的數據
- 調用FetcherOutputFormat.Writer對象,把CrawlDatum,Content,ParseResult分別寫入crawl_fetch,content,(parse_text,parse_data,crawl_parse)目錄中
好了,Fetcher的分析也差不多了,可能有一些細節還沒有分析到,下面有機會再補上吧。