HtmlParser + HttpClient 实现爬虫

简易爬虫的实现

HttpClient 提供了便利的 HTTP 协议访问,使得我们可以很容易的得到某个网页的源码并保存在本地;HtmlParser 提供了如此简便灵巧的类库,可以从网页中便捷的提取出指向其他网页的超链接。笔者结合这两个开源包,构建了一个简易的网络爬虫。

爬虫 (Crawler) 原理

学过数据结构的读者都知道有向图这种数据结构。如下图所示,如果将网页看成是图中的某一个节点,而将网页中指向其他网页的链接看成是这个节点指向其他节点的边,那么我们很容易将整个 Internet 上的网页建模成一个有向图。理论上,通过遍历算法遍历该图,可以访问到Internet 上的几乎所有的网页。最简单的遍历就是宽度优先以及深度优先。以下笔者实现的简易爬虫就是使用了宽度优先的爬行策略。

图 2. 网页关系的建模图

网页关系的建模图

简易爬虫实现流程

在看简易爬虫的实现代码之前,先介绍一下简易爬虫爬取网页的流程。

图 3. 爬虫流程图

爬虫流程图

各个类的源码以及说明

对应上面的流程图,简易爬虫由下面几个类组成,各个类职责如下:

Crawler.java:爬虫的主方法入口所在的类,实现爬取的主要流程。

LinkDb.java:用来保存已经访问的 url 和待爬取的 url 的类,提供url出对入队操作。

Queue.java: 实现了一个简单的队列,在 LinkDb.java 中使用了此类。

FileDownloader.java:用来下载 url 所指向的网页。

HtmlParserTool.java: 用来抽取出网页中的链接。

LinkFilter.java:一个接口,实现其 accept() 方法用来对抽取的链接进行过滤。

下面是各个类的源码,代码中的注释有比较详细的说明。

清单6 Crawler.java
package com.ie;

import java.util.Set;
public class Crawler {
	/* 使用种子 url 初始化 URL 队列*/
	private void initCrawlerWithSeeds(String[] seeds)
	{
		for(int i=0;i<seeds.length;i++)
			LinkDB.addUnvisitedUrl(seeds[i]);
	}
	
	/* 爬取方法*/
	public void crawling(String[] seeds)
	{
		LinkFilter filter = new LinkFilter(){
			//提取以 http://www.twt.edu.cn 开头的链接
			public boolean accept(String url) {
				if(url.startsWith("http://www.twt.edu.cn"))
					return true;
				else
					return false;
			}
		};
		//初始化 URL 队列
		initCrawlerWithSeeds(seeds);
		//循环条件:待抓取的链接不空且抓取的网页不多于 1000
		while(!LinkDB.unVisitedUrlsEmpty()&&LinkDB.getVisitedUrlNum()<=1000)
		{
			//队头 URL 出对
			String visitUrl=LinkDB.unVisitedUrlDeQueue();
			if(visitUrl==null)
				continue;
			FileDownLoader downLoader=new FileDownLoader();
			//下载网页
			downLoader.downloadFile(visitUrl);
			//该 url 放入到已访问的 URL 中
			LinkDB.addVisitedUrl(visitUrl);
			//提取出下载网页中的 URL
			
			Set<String> links=HtmlParserTool.extracLinks(visitUrl,filter);
			//新的未访问的 URL 入队
			for(String link:links)
			{
					LinkDB.addUnvisitedUrl(link);
			}
		}
	}
	//main 方法入口
	public static void main(String[]args)
	{
		Crawler crawler = new Crawler();
		crawler.crawling(new String[]{"http://www.twt.edu.cn"});
	}
}
清单7 LinkDb.java
package com.ie;

import java.util.HashSet;
import java.util.Set;

/**
 * 用来保存已经访问过 Url 和待访问的 Url 的类
 */
public class LinkDB {

	//已访问的 url 集合
	private static Set<String> visitedUrl = new HashSet<String>();
	//待访问的 url 集合
	private static Queue<String> unVisitedUrl = new Queue<String>();

	
	public static Queue<String> getUnVisitedUrl() {
		return unVisitedUrl;
	}

	public static void addVisitedUrl(String url) {
		visitedUrl.add(url);
	}

	public static void removeVisitedUrl(String url) {
		visitedUrl.remove(url);
	}

	public static String unVisitedUrlDeQueue() {
		return unVisitedUrl.deQueue();
	}

	// 保证每个 url 只被访问一次
	public static void addUnvisitedUrl(String url) {
		if (url != null && !url.trim().equals("")
 && !visitedUrl.contains(url)
				&& !unVisitedUrl.contians(url))
			unVisitedUrl.enQueue(url);
	}

	public static int getVisitedUrlNum() {
		return visitedUrl.size();
	}

	public static boolean unVisitedUrlsEmpty() {
		return unVisitedUrl.empty();
	}
}
清单8 Queue.java
package com.ie;

import java.util.LinkedList;
/**
 * 数据结构队列
 */
public class Queue<T> {

	private LinkedList<T> queue=new LinkedList<T>();
	
	public void enQueue(T t)
	{
		queue.addLast(t);
	}
	
	public T deQueue()
	{
		return queue.removeFirst();
	}
	
	public boolean isQueueEmpty()
	{
		return queue.isEmpty();
	}
	
	public boolean contians(T t)
	{
		return queue.contains(t);
	}
	
	public boolean empty()
	{
		return queue.isEmpty();
	}
}
清单 9 FileDownLoader.java
package com.ie;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;

public class FileDownLoader {
	
	/**根据 url 和网页类型生成需要保存的网页的文件名
	 *去除掉 url 中非文件名字符 
	 */
	public  String getFileNameByUrl(String url,String contentType)
	{
		url=url.substring(7);//remove http://
		if(contentType.indexOf("html")!=-1)//text/html
		{
			url= url.replaceAll("[\\?/:*|<>\"]", "_")+".html";
			return url;
		}
		else//如application/pdf
		{
return url.replaceAll("[\\?/:*|<>\"]", "_")+"."+ \
          contentType.substring(contentType.lastIndexOf("/")+1);
		}	
	}

	/**保存网页字节数组到本地文件
	 * filePath 为要保存的文件的相对地址
	 */
	private void saveToLocal(byte[] data,String filePath)
	{
		try {
			DataOutputStream out=new DataOutputStream(
new FileOutputStream(new File(filePath)));
			for(int i=0;i<data.length;i++)
			out.write(data[i]);
			out.flush();
			out.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/*下载 url 指向的网页*/
	public String  downloadFile(String url)
	{
		  String filePath=null;
		  /* 1.生成 HttpClinet 对象并设置参数*/
		  HttpClient httpClient=new HttpClient();
		  //设置 Http 连接超时 5s
		  	  httpClient.getHttpConnectionManager().getParams().
setConnectionTimeout(5000);
		  
		  /*2.生成 GetMethod 对象并设置参数*/
		  GetMethod getMethod=new GetMethod(url);	 
		  //设置 get 请求超时 5s
		  getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT,5000);
		  //设置请求重试处理
		  getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
			new DefaultHttpMethodRetryHandler());
		  
		  /*3.执行 HTTP GET 请求*/
		  try{ 
			  int statusCode = httpClient.executeMethod(getMethod);
			  //判断访问的状态码
			  if (statusCode != HttpStatus.SC_OK) 
			  {
System.err.println("Method failed: "+ getMethod.getStatusLine());
				  filePath=null;
			  }
			  
			  /*4.处理 HTTP 响应内容*/
 byte[] responseBody = getMethod.getResponseBody();//读取为字节数组
			  //根据网页 url 生成保存时的文件名
filePath="temp\\"+getFileNameByUrl(url,
		   getMethod.getResponseHeader("Content-Type").getValue());
			saveToLocal(responseBody,filePath);
		  } catch (HttpException e) {
				   // 发生致命的异常,可能是协议不对或者返回的内容有问题
				   System.out.println("Please check your provided http 
address!");
				   e.printStackTrace();
				  } catch (IOException e) {
				   // 发生网络异常
				   e.printStackTrace();
				  } finally {
				   // 释放连接
				   getMethod.releaseConnection();		   
				  }
				  return filePath;
	}
	//测试的 main 方法
	public static void main(String[]args)
	{
		FileDownLoader downLoader = new FileDownLoader();
		downLoader.downloadFile("http://www.twt.edu.cn");
	}
}
清单 10 HtmlParserTool.java
package com.ie;

import java.util.HashSet;
import java.util.Set;

import org.htmlparser.Node;
import org.htmlparser.NodeFilter;
import org.htmlparser.Parser;
import org.htmlparser.filters.NodeClassFilter;
import org.htmlparser.filters.OrFilter;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;

public class HtmlParserTool {
	// 获取一个网站上的链接,filter 用来过滤链接
	public static Set<String> extracLinks(String url,LinkFilter filter) {

		Set<String> links = new HashSet<String>();
		try {
			Parser parser = new Parser(url);
			parser.setEncoding("gb2312");
			// 过滤 <frame >标签的 filter,用来提取 frame 标签里的 src 属性所表示的链接
			NodeFilter frameFilter = new NodeFilter() {
				public boolean accept(Node node) {
					if (node.getText().startsWith("frame src=")) {
						return true;
					} else {
						return false;
					}
				}
			};
			// OrFilter 来设置过滤 <a> 标签,和 <frame> 标签
			OrFilter linkFilter = new OrFilter(new NodeClassFilter(
					LinkTag.class), frameFilter);
			// 得到所有经过过滤的标签
			NodeList list = parser.extractAllNodesThatMatch(linkFilter);
			for (int i = 0; i < list.size(); i++) {
				Node tag = list.elementAt(i);
				if (tag instanceof LinkTag)// <a> 标签
				{
					LinkTag link = (LinkTag) tag;
					String linkUrl = link.getLink();// url
					if(filter.accept(linkUrl))
						links.add(linkUrl);
				} else// <frame> 标签
				{
		        // 提取 frame 里 src 属性的链接如 <frame src="test.html"/>
					String frame = tag.getText();
					int start = frame.indexOf("src=");
					frame = frame.substring(start);
					int end = frame.indexOf(" ");
					if (end == -1)
						end = frame.indexOf(">");
					String frameUrl = frame.substring(5, end - 1);
					if(filter.accept(frameUrl))
						links.add(frameUrl);
				}
			}
		} catch (ParserException e) {
			e.printStackTrace();
		}
		return links;
	}
	//测试的 main 方法
	public static void main(String[]args)
	{
Set<String> links = HtmlParserTool.extracLinks(
"http://www.twt.edu.cn",new LinkFilter()
		{
			//提取以 http://www.twt.edu.cn 开头的链接
			public boolean accept(String url) {
				if(url.startsWith("http://www.twt.edu.cn"))
					return true;
				else
					return false;
			}
			
		});
		for(String link : links)
			System.out.println(link);
	}
}
清单11 LinkFilter.java
package com.ie;

public interface LinkFilter {
	public boolean accept(String url);
}

这些代码中关键的部分都在 HttpClient 和 HtmlParser 介绍中说明过了。

posted on 2015-12-21 22:43  Moriatry  阅读(303)  评论(0编辑  收藏  举报