【转】Nutch源代码研究 网页抓取 下载插件

今天我们来看看Nutch的源代码中的protocol-http插件,是如何抓取和下载web页面的。protocol-http就两个类HttpRespose和Http类,其中HttpRespose主要是向web服务器发请求来获取响应,从而下载页面。Http类则非常简单,其实可以说是HttpResponse的一个Facade,设置配置信息,然后创建HttpRespose。用户似乎只需要和Http类打交道就行了(我也没看全,所以只是猜测)。 
我们来看看HttpResponse类: 
看这个类的源码需要从构造函数 
public HttpResponse(HttpBase http, URL url, CrawlDatum datum) throws ProtocolException, IOException开始

首先判断协议是否为http

1 if (!"http".equals(url.getProtocol()))
2       throw new HttpException("Not an HTTP url:" + url);

获得路径,如果url.getFile()的为空直接返回”/”,否则返回url.getFile() 
String path = "".equals(url.getFile()) ? "/" : url.getFile();

然后根据url获取到主机名和端口名。如果端口不存在,则端口默认为80,请求的地址将不包括端口号portString= "",否则获取到端口号,并得到portString

 1 String host = url.getHost();
 2     int port;
 3     String portString;
 4     if (url.getPort() == -1) {
 5       port= 80;
 6       portString= "";
 7     } else {
 8       port= url.getPort();
 9       portString= ":" + port;
10 }

然后创建socket,并且设置连接超时的时间:

1 socket = new Socket();                    // create the socket socket.setSoTimeout(http.getTimeout());

根据是否使用代理来得到socketHost和socketPort:

1 String sockHost = http.useProxy() ? http.getProxyHost() : host;
2 int sockPort = http.useProxy() ? http.getProxyPort() : port;

创建InetSocketAddress,并且开始建立连接:

1 InetSocketAddress sockAddr= new InetSocketAddress(sockHost, sockPort);
2 socket.connect(sockAddr, http.getTimeout());

获取输入流:

1 // make request
2       OutputStream req = socket.getOutputStream();

以下代码用来向服务器发Get请求:

 1 StringBuffer reqStr = new StringBuffer("GET ");
 2       if (http.useProxy()) {
 3          reqStr.append(url.getProtocol()+"://"+host+portString+path);
 4       } else {
 5          reqStr.append(path);
 6       }
 7
 8       reqStr.append(" HTTP/1.0\r\n");
 9       reqStr.append("Host: ");
10       reqStr.append(host);
11       reqStr.append(portString);
12       reqStr.append("\r\n");
13       reqStr.append("Accept-Encoding: x-gzip, gzip\r\n");
14       String userAgent = http.getUserAgent();
15       if ((userAgent == null) || (userAgent.length() == 0)) {
16         if (Http.LOG.isFatalEnabled()) { Http.LOG.fatal("User-agent is not set!"); }
17       } else {
18         reqStr.append("User-Agent: ");
19         reqStr.append(userAgent);
20         reqStr.append("\r\n");
21       }
22       reqStr.append("\r\n");
23       byte[] reqBytes= reqStr.toString().getBytes();
24       req.write(reqBytes);
25       req.flush();

接着来处理相应,获得输入流并且包装成PushbackInputStream来方便操作:

1 PushbackInputStream in =                  // process response
2         new PushbackInputStream(
3           new BufferedInputStream(socket.getInputStream(), Http.BUFFER_SIZE),
4           Http.BUFFER_SIZE) ;

提取状态码和响应中的HTML的header:

1 boolean haveSeenNonContinueStatus= false;
2       while (!haveSeenNonContinueStatus) {
3         // parse status code line
4         this.code = parseStatusLine(in, line);
5         // parse headers
6         parseHeaders(in, line);
7         haveSeenNonContinueStatus= code != 100; // 100 is "Continue"
8       }

接着读取内容:

1 readPlainContent(in);

获取内容的格式,如果是压缩的则处理压缩

1 String contentEncoding = getHeader(Response.CONTENT_ENCODING);
2       if ("gzip".equals(contentEncoding) || "x-gzip".equals(contentEncoding)) {
3         content = http.processGzipEncoded(content, url);
4       } else {
5         if (Http.LOG.isTraceEnabled()) {
6           Http.LOG.trace("fetched " + content.length + " bytes from " + url);
7         }
8       }

整个过程结束。

下面我们来看看parseStatusLine parseHeaders  readPlainContent以及readChunkedContent的过程。

private int parseStatusLine(PushbackInputStream in, StringBuffer line) 
throws IOException, HttpException: 
这个函数主要来提取响应得状态,例如200 OK这样的状态码:

请求的状态行一般格式(例如响应Ok的话) HTTP/1.1 200" 或 "HTTP/1.1 200 OK

1 int codeStart = line.indexOf(" ");
2 int codeEnd = line.indexOf(" ", codeStart+1);

如果是第一种情况:

1 if (codeEnd == -1)
2       codeEnd = line.length();

状态码结束(200)位置便是line.length() 
否则状态码结束(200)位置就是line.indexOf(" ", codeStart+1); 
接着开始提取状态码:

1 int code;
2     try {
3       code= Integer.parseInt(line.substring(codeStart+1, codeEnd));
4     } catch (NumberFormatException e) {
5       throw new HttpException("bad status line ‘" + line
6                               + "‘: " + e.getMessage(), e);
7 }

下面看看

1 private void parseHeaders(PushbackInputStream in, StringBuffer line)
2 throws IOException, HttpException:

这个函数主要是将响应的headers加入我们已经建立的结构header的Metadata中。

一个循环读取headers: 
一般HTTP response的header部分和内容部分会有一个空行,使用readLine如果是空行就会返回读取的字符数为0,具体readLine实现看完这个函数在仔细看: 
while (readLine(in, line, true) != 0)

如果没有空行,那紧接着就是正文了,正文一般会以<!DOCTYPE、<HTML、<html开头。如果读到的一行中包含这个,那么header部分就读完了。

1       // handle HTTP responses with missing blank line after headers
2       int pos;
3       if ( ((pos= line.indexOf("<!DOCTYPE")) != -1)
4            || ((pos= line.indexOf("<HTML")) != -1)
5            || ((pos= line.indexOf("<html")) != -1) ) 

接着把多读的那部分压回流中,并设置那一行的长度为pos

1        in.unread(line.substring(pos).getBytes("UTF-8"));
2         line.setLength(pos);

接着把对一行的处理委托给processHeaderLine(line)来处理:

 1         try {
 2             //TODO: (CM) We don‘t know the header names here
 3             //since we‘re just handling them generically. It would
 4             //be nice to provide some sort of mapping function here
 5             //for the returned header names to the standard metadata
 6             //names in the ParseData class
 7           processHeaderLine(line);
 8        } catch (Exception e) {
 9           // fixme:
10           e.printStackTrace(LogUtil.getErrorStream(Http.LOG));
11         }
12         return;
13       }
14       processHeaderLine(line);

下面我们看看如何处理一行header的: 
private void processHeaderLine(StringBuffer line) 
throws IOException, HttpException 
请求的头一般格式: 
Cache-Control: private 
Date: Fri, 14 Dec 2007 15:32:06 GMT 
Content-Length: 7602 
Content-Type: text/html 
Server: Microsoft-IIS/6.0

这样我们就比较容易理解下面的代码了:

1 int colonIndex = line.indexOf(":");       // key is up to colon

如果没有”:”并且这行不是空行则抛出HttpException异常

1     if (colonIndex == -1) {
2       int i;
3       for (i= 0; i < line.length(); i++)
4         if (!Character.isWhitespace(line.charAt(i)))
5           break;
6       if (i == line.length())
7         return;
8       throw new HttpException("No colon in header:" + line);
9 }

否则,可以可以提取出键-值对了: 
key为0~colonIndex部分,然后过滤掉开始的空白字符,作为value部分。

最后放到headers中:

 1     String key = line.substring(0, colonIndex);
 2
 3     int valueStart = colonIndex+1;            // skip whitespace
 4     while (valueStart < line.length()) {
 5       int c = line.charAt(valueStart);
 6       if (c != ‘ ‘ && c != ‘\t‘)
 7        break;
 8       valueStart++;
 9     }
10     String value = line.substring(valueStart);
11     headers.set(key, value);

下面我们看看用的比较多的辅助函数 
private static int readLine(PushbackInputStream in, StringBuffer line, 
                      boolean allowContinuedLine) throws IOException

代码的实现: 
开始设置line的长度为0不断的读,直到c!=-1,对于每个c:

如果是\r并且下一个字符是\n则读入\r,如果是\n,并且如果line.length() > 0,也就是这行前面已经有非空白字符,并且还允许连续行,在读一个字符,如果是’ ’或者是\t说明此行仍未结束,读入该字符,一行结束,返回读取的实际长度。其他情况下直接往line追加所读的字符:

 1     line.setLength(0);
 2     for (int c = in.read(); c != -1; c = in.read()) {
 3       switch (c) {
 4         case ‘\r‘:
 5           if (peek(in) == ‘\n‘) {
 6             in.read();
 7           }
 8         case ‘\n‘:
 9           if (line.length() > 0) {
10             // at EOL -- check for continued line if the current
11             // (possibly continued) line wasn‘t blank
12             if (allowContinuedLine)
13               switch (peek(in)) {
14                 case ‘ ‘ : case ‘\t‘:                   // line is continued
15                   in.read();
16                   continue;
17               }
18           }
19           return line.length();      // else complete
20         default :
21           line.append((char)c);
22       }
23     }
24     throw new EOFException();
25   }

接着看如何读取内容的,也就是 
private void readPlainContent(InputStream in) 
throws HttpException, IOException的实现:

首先从headers(在此之前已经读去了headers放到metadata中了)中获取响应的长度,

 1 int contentLength = Integer.MAX_VALUE;    // get content length
 2     String contentLengthString = headers.get(Response.CONTENT_LENGTH);
 3     if (contentLengthString != null) {
 4       contentLengthString = contentLengthString.trim();
 5       try {
 6         contentLength = Integer.parseInt(contentLengthString);
 7       } catch (NumberFormatException e) {
 8        throw new HttpException("bad content length: "+contentLengthString);
 9       }
10 }

如果大于http.getMaxContent()(这个值在配置文件中http.content.limit来配置),

则截取maxContent那么长的字段:

 1     if (http.getMaxContent() >= 0
 2      && contentLength > http.getMaxContent())   // limit download size
 3       contentLength  = http.getMaxContent();
 4
 5     ByteArrayOutputStream out = new ByteArrayOutputStream(Http.BUFFER_SIZE);
 6     byte[] bytes = new byte[Http.BUFFER_SIZE];
 7     int length = 0;                           // read content
 8     for (int i = in.read(bytes); i != -1; i = in.read(bytes)) {
 9       out.write(bytes, 0, i);
10       length += i;
11       if (length >= contentLength)
12         break;
13     }
14     content = out.toByteArray();
15   }
时间: 2024-10-25 20:21:43

【转】Nutch源代码研究 网页抓取 下载插件的相关文章

【转】Nutch源代码研究 网页抓取 数据结构

今天我们看看Nutch网页抓取,所用的几种数据结构: 主要涉及到了这几个类:FetchListEntry,Page, 首先我们看看FetchListEntry类: public final class FetchListEntry implements Writable, Cloneable 实现了Writable, Cloneable接口,Nutch许多类实现了Writable, Cloneable. 自己负责自己的读写操作其实是个很合理的设计方法,分离出来反倒有很琐碎 的感觉. 看看里面的成

【网页抓取】判断URL是否有效并可提供下载

问题的产生: 今天在提供API接口给客户的时候,客户提出了一个要求,有一个接口返回的语音文件的URL地址需要做有效性验证,这里所指的有效是指请求这个URL后能直接下载语音文件,反之则视为无效. 先来看看两个请求语音文件的URL地址: 有效的:http://xxx.xxx.xxx.xxx:60000/GetRecord.ashx?cid=1586887989,访问后页面输出,如下图所示: 无效的:http://xxx.xxx.xxx.xxx:60000/GetRecord.ashx?cid=176

基于Casperjs的网页抓取技术【抓取豆瓣信息网络爬虫实战示例】

CasperJS is a navigation scripting & testing utility for the PhantomJS (WebKit) and SlimerJS (Gecko) headless browsers, written in Javascript. PhantomJS是基于WebKit内核的headless browser SlimerJS则是基于Gecko内核的headless browser Headless browser: 无界面显示的浏览器,可以用于

用python做网页抓取与解析入门笔记[zz]

(from http://chentingpc.me/article/?id=961) 事情的起因是,我做survey的时候搜到了这两本书:Computational Social Network Analysis和Computational Social Network,感觉都蛮不错的,想下载下来看看,但是点开网页发现这个只能分章节下载,晕,我可没时间一章一章下载,想起了迅雷的下载全部链接,试试看,果真可以把他们一网打尽,但是,sadly,迅雷下载的时候,文件名没办法跟章节名对应起来,晕,我可

淘搜索之网页抓取系统分析与实现(2)—redis + scrapy

1.scrapy+redis使用 (1)应用 这里redis与scrapy一起,scrapy作为crawler,而redis作为scrapy的调度器.如架构图中的②所示.图1 架构图 (2)为什么选择redis redis作为调度器的实现仍然和其特性相关,可见<一淘搜索之网页抓取系统分析与实现(1)--redis使用>(http://blog.csdn.net/u012150179/article/details/38226711)中关于redis的分析. 2.redis实现scrapy sc

网页抓取与处理的一些方法

昨天还是2014,今天就变成了2015.时间总是那么快,这篇文章就作为2015年的一个开始吧. 这篇文章主要介绍一些网页抓取及抓取下来的内容处理. 所需要的jar包点击打开链接,我放在百度云盘里.有需要的可以下载,其他的请自行下载. 百度百科对网页抓取的定义,当然本文并没有介绍的那么多,只是介绍对单个页面的抓取,和模拟提交表单抓取页面,如需深究,请自行baidu or google. 上面的方法直接返回String字符串,只需传入一个链接即可.相信大家都看的懂. 那么获取到的String字符串,

一个小型的网页抓取系统的架构设计

一个小型的网页抓取系统的架构设计 网页抓取服务是互联网中的常用服务,在搜索引擎中spider(网页抓取爬虫)是必需的核心服务.搜索引擎的衡量指标"多.快.准.新"四个指标中,多.快.新都是对spider的要求.搜索引擎公司比如google.baidu都维护者自己负责的spider系统.当然他们的系统很复杂,在这里我们介绍一个小型的网页抓取系统的架构,目标是快速的抓取某个或者几个指定的网站的数据,它的作用有很多,比如做竞品分析,还有其他不可告人的J. 下面这个小型的网页抓取系统,分成下面

网络爬虫(网络蜘蛛)之网页抓取

现在有越来越多的人热衷于做网络爬虫(网络蜘蛛),也有越来越多的地方需要网络爬虫,比如搜索引擎.资讯采集.舆情监测等等,诸如此类.网络爬虫涉及到的技术(算法/策略)广而复杂,如网页获取.网页跟踪.网页分析.网页搜索.网页评级和结构/非结构化数据抽取以及后期更细粒度的数据挖掘等方方面面,对于新手来说,不是一朝一夕便能完全掌握且熟练应用的,对于作者来说,更无法在一篇文章内就将其说清楚.因此在本篇文章中,我们仅将视线聚焦在网络爬虫的最基础技术--网页抓取方面. 说到网页抓取,往往有两个点是不得不说的,首

网页抓取小工具(IE法)

网页抓取小工具(IE法)-- 吴姐 http://club.excelhome.net/thread-1095707-1-1.html 用IE提取网页资料的好处在于:所见即所得,网页上能看到的信息一般都能获取. 本工具功能不多,主要是便于提取网页上展示的信息所在元素的代码.希望能对大家有点小帮助. 网页抓取小工具.rar (22.91 KB, 下载次数: 2426) 本工具使用方法: 1.在B1内输入网址,可以是已打开的网页,也可以是未打开的 2.A2和B2的内容不要更改,第二行的其他单元格可以