Nutch源代码研究 网页抓取 fetch

来源:互联网 发布:mysql导出整个数据库 编辑:程序博客网 时间:2024/05/16 11:15
搜索引擎Nutch源代码研究之一 网页抓取: 
Nutch的爬虫代码部分主要集中在:package org.apache.nutch.fetcher和插件protocol-file 
Protocol-ftp protocol-http protocol-httpclient以及相应的Parser插件中: 
下面我们先从org.apache.nutch.fetcher开始: 
最主要的类是Fetcher类,我们从它入手一步步跟踪整个代码: 
我们从run函数入手: 

首先: 

for (int i = 0; i < threadCount; i++) {       // spawn threads      FetcherThread thread = new FetcherThread(THREAD_GROUP_NAME+i);       thread.start();}

建立了多个FetcherThread线程来抓取网页,threadCount可以配置或者使用默认值。 

接着一个while(true)的循环里面的代码: 

 int n = group.activeCount();      Thread[] list = new Thread[n];      group.enumerate(list);      boolean noMoreFetcherThread = true; // assumption      for (int i = 0; i < n; i++) {        // this thread may have gone away in the meantime        if (list[i] == null) continue;        String tname = list[i].getName();        if (tname.startsWith(THREAD_GROUP_NAME)) // prove it          noMoreFetcherThread = false;        if (LOG.isLoggable(Level.FINE))          LOG.fine(list[i].toString());      }      if (noMoreFetcherThread) {        if (LOG.isLoggable(Level.FINE))          LOG.fine("number of active threads: "+n);        if (pages == pages0 && errors == errors0 && bytes == bytes0)          break;        status();        pages0 = pages; errors0 = errors; bytes0 = bytes;      }

相当于维护一个线程池,并在Log中输入抓取页面的速度,状态之类的信息。其实可以使用java.util.concurrent包的Executors来创建一个线程池来使用。 

现在我们看看抓取的线程FetcherThread是如何工作的: 
线程当然要从run方法来跟踪了: 
FetchListEntry fle = new FetchListEntry(); 
建立一个抓取列表类,为了不分散精力,我们稍候在看看这个FetchListEntry以及相关类的数据结构。 

然后又是一个while (true)的循环,我们看看里面做了些什么: 

if (fetchList.next(fle) == null)            break;url = fle.getPage().getURL().toString();从当前的FetchListEntry中获得一个要抓取的url,然后if (!fle.getFetch()) {                  // should we fetch this page?            if (LOG.isLoggable(Level.FINE))                LOG.fine("not fetching " + url);            handleFetch(fle, new ProtocolOutput(null, ProtocolStatus.STATUS_NOTFETCHING));            continue;}

如果不需要抓取,在handleFetch进行相应的处理。 
然后又是一个do…while循环,用来处理抓取过程中重定向指定的次数: 
整个循环的条件 refetch && (redirCnt < MAX_REDIRECT) 
重新抓取并且重定向次数没有超出最大次数 
ProtocolFactory工厂创建protocol实例: 
Protocol protocol = ProtocolFactory.getProtocol(url); 
Protocol的实现是以插件的形式提供的,我们先跳过Protocol实现的细节: 
可以从protocol中获取到Fetch的输出流: 
            ProtocolOutput output = protocol.getProtocolOutput(fle); 

通过输出流可以获取到抓取的状态ProtocolStatus和抓取的内容Content: 

ProtocolStatus pstat = output.getStatus();Content content = output.getContent();

然后根据抓取的状态: 

switch(pstat.getCode()) 
如果成功 case ProtocolStatus.SUCCESS: 
如果内容不为空if (content != null) 

修改抓取的页数,抓取的字节数,并且如果抓取了100页,根据pages,bytes在日志中记录抓取的速度等信息。 

synchronized (Fetcher.this) {           // update status                    pages++;                    bytes += content.getContent().length;                    if ((pages % 100) == 0) {             // show status every                       status();                    }}

在handleFetch进行相应的处理 

ParseStatus ps = handleFetch(fle, output); 
如果处理返回的状态不为空,并且成功的重定向: 
if (ps != null && ps.getMinorCode() == ParseStatus.SUCCESS_REDIRECT) 

获取重定向的链接并进行过滤: 

String newurl = ps.getMessage();newurl = URLFilters.filter(newurl);

如果重定向的链接newurl不为空并且和现在的url不同: 

if (newurl != null && !newurl.equals(url)) 

重新获取,更新refetch、url、redirCnt++; 

      refetch = true;      url = newurl;      redirCnt++;

创建当前页面的FetchListEntry: 

fle = new FetchListEntry(true, new Page(url, NEW_INJECTED_PAGE_SCORE), new String[0]); 
如果链接页面已经转移或者临时转移: 
case ProtocolStatus.MOVED: // try to redirect immediately 
              case ProtocolStatus.TEMP_MOVED: // try to redirect immediately 
立即重定向: 
处理抓取的结果: 
handleFetch(fle, output); 

获取重定向的url: 

 String newurl = pstat.getMessage();     newurl = URLFilters.filter(newurl);     if (newurl != null && !newurl.equals(url)) {             refetch = true;             url = newurl;             redirCnt++;                  // create new entry.             fle = new FetchListEntry(true, new Page(url, NEW_INJECTED_PAGE_SCORE), new String[0]);}

过程和上面的重定向类似。 

以下几种状态: 

case ProtocolStatus.GONE:case ProtocolStatus.NOTFOUND:case ProtocolStatus.ACCESS_DENIED: case ProtocolStatus.ROBOTS_DENIED: case ProtocolStatus.RETRY: case ProtocolStatus.NOTMODIFIED:

直接交由handleFetch(fle, output);来处理 

如果发生异常,logger异常信息,然后交给handleFetch处理: 
case ProtocolStatus.EXCEPTION: 
          logError(url, fle, new Exception(pstat.getMessage()));                
          handleFetch(fle, output); 
其他情况为未知状态,log出当前的状态,然后交给handleFetch处理 
default: 
         LOG.warning("Unknown ProtocolStatus: " + pstat.getCode()); 
         handleFetch(fle, output); 
循环结束。 

最后如果完成的线程数等于threadCount,关闭所有的插件: 

synchronized (Fetcher.this) {        atCompletion++;        if (atCompletion == threadCount) {          try {            PluginRepository.getInstance().finalize();          } catch (java.lang.Throwable t) {            // do nothing          }        }      }

我们看到Fetch到页面后大多数的处理都交给了handleFetch了。 

现在我们来看看private ParseStatus handleFetch(FetchListEntry fle, ProtocolOutput output) 的代码: 

根据output获取到内容和url 

Content content = output.getContent();MD5Hash hash = null;String url = fle.getPage().getURL().toString();

如果content为null,我们直接空的content,然后对url 用digest编码,否则对content 用digest来编码: 

if (content == null) {        content = new Content(url, url, new byte[0], "", new Properties());        hash = MD5Hash.digest(url);} else {        hash = MD5Hash.digest(content.getContent());}

在获取ProtocolStatus 
ProtocolStatus protocolStatus = output.getStatus(); 

如果Fetcher不进行解析(parse),直接把抓取的页面写入磁盘 

 if (!Fetcher.this.parsing) {        outputPage(new FetcherOutput(fle, hash, protocolStatus),                content, null, null);        return null;      }

否则进行parse: 
首先获取页面contentType,以便根据正确编码进行Parse的: 
String contentType = content.getContentType(); 

下面便是使用Parser进行页面提取得过程: 

Parser parser = null;      Parse parse = null;      ParseStatus status = null;      try {        parser = ParserFactory.getParser(contentType, url);        parse = parser.getParse(content);        status = parse.getData().getStatus();      } catch (Exception e) {        e.printStackTrace();        status = new ParseStatus(e);      }

如果提取页面成功:if (status.isSuccess()) 

将FetcherOutput提取的内容以及状态作为写入保存: 

 outputPage(new FetcherOutput(fle, hash, protocolStatus),                content, new ParseText(parse.getText()), parse.getData());

否则 else 将FetcherOutput和空的parse内容保存: 

LOG.info("fetch okay, but can't parse " + url + ", reason: "                + status.toString()); outputPage(new FetcherOutput(fle, hash, protocolStatus),                content, new ParseText(""),                new ParseData(status, "", new Outlink[0], new Properties()));

我们先跳过Parser的过程。下次我们看看如何在http协议下载的web页面,这就是Protocol插件的实现。 

=======================================================

Fetcher.FetcherThread

2.1 这个类主要是用来从队列中得到FetchItem,下面来看一下其run方法,其大概做了几件事:

  • 从抓取队列中得到一个FetchItem,如果返回为null,判断生产者是否还活着或者队列中是否还有数据,  如果队列中还有数据,那就等待,如果上面条件没有满足,就认为所有FetchItem都已经处理完了,退出当前抓取线程
  • 得到FetchItem, 抽取其url,从这个url中分析出所使用的协议,调用相应的plugin来解析这个协议
  • 得到相当url的robotRules,看是否符合抓取规则,如果不符合或者其delayTime大于我们配置的maxDelayTime,那就不抓取这个网页
  • 对网页进行抓取,得到其抓取的Content和抓取状态,调用FetchItemQueues的finishFetchItem方法,表明当前url已经抓取完成
  • 根据抓取协议的状态来进行下一步操作
    1. 如果状态为WOULDBLOCK,那就进行retry,把当前url放加FetchItemQueues中,进行重试
    2. 如果是MOVED或者TEMP_MOVED,这时这个网页可以被重定向了,对其重定向的内容进行解析,得到重定向的网址,这时要生成一个新的FetchItem,根据其QueueID放到相应的队列的inProgress集合中,然后再对这个重定向的网页进行抓取
    3. 如果状态是EXCEPTION,对当前url所属的FetchItemQueue进行检测,看其异常的网页数有没有超过最大异常网页数,如果大于,那就清空这个队列,认为这个队列中的所有网页都有问题。
    4. 如果状态是RETRY或者是BLOCKED,那就输出CrawlDatum,将其状态设置成STATUS_FETCH_RETRY,在下一轮进行重新抓取
    5. 如果状态是GONE,NOTFOUND,ACCESS_DENIED,ROBOTS_DENIED,那就输出CrawlDatum,设置其状态为STATUS_FETCH_GONE,可能在下一轮中就不进行抓取了,
    6. 如果状态是NOTMODIFIED,那就认为这个网页没有改变过,那就输出其CrawlDatum,将其状态设成成STATUS_FETCH_NOTMODIFIED.
    7. 如果所有状态都没有找到,那默认输出其CrawlDatum,将其状态设置成STATUS_FETCH_RETRY,在下一轮抓取中再重试
  • 判断网页重定向的次数,如果超过最大重定向次数,就输出其CrawlDatum,将其状态设置成STATUS_FETCH_GONE

这里有一些细节没有说明,如网页被重定向以后如果操作,相应的协议是如果产生的,这个是通过插件产生的,具体插件是怎么调用的,这里就不说了,以后有机会会再分析一下。


2.2 下面分析FetcherThread中的另外一个比较重要的方法,就是output

具体这个output大概做了如下几件事:
  • 判断抓取的content是否为空,如果不为空,那调用相应的解析插件来对其内容进行解析,然后就是设置当前url所对应的CrawlDatum的一些参数,如当前内容的MD5码,分数等信息
  • 然后就是使用FetchOutputFormat输出当前url的CrawlDatum,Content和解析的结果ParseResult
下面分析一下FetcherOutputFormat中所使用到的ParseOutputFormat.RecordWriter
在生成相应的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的时候,会输出另外五个目录
    1. content: 这个目录只有在配置了要输出抓取内容时才会输出
    2. crawl_fetch: 这个目录是输出抓取成功后的CrawlDatum信息,这里是对原来crawl_generate目录中的信息进行了一些修改,下面三个目录只有配置了解析参数后才会输出,如果后面调用bin/nutch parse命令
    3. parse_text: 这个目录存放了抓取的网页内容,以提后面建立索引用
    4. parse_data: 这里存入了网页解析后的一些数据,如网页title,外链接信息等
    5. 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)目录中

原创粉丝点击