Java爬虫之宽度优先爬虫

在实际应用中,使用网络爬虫遍历互联网,把网络中我们感兴趣的网页全部抓取过来。为便于理解,我们把整个Internet看做一个超级大图,每个页面作为图中的一个节点,页面中的超链接可看做图中的有向边。爬虫在抓取网页过程中有两种遍历方式:深度优先遍历和宽度优先遍历。由于在深度优先遍历中,随着遍历深度的增加,可能抓取到的网页与主题的相关性降低,所以一般不采用这种遍历方式。在实际中开发者总喜欢将相关主题的链接放在同一个页面中,故按照宽度优先遍历的方式抓取的网页与主题相关性大大增强。有的时候也不能完全的按照宽度优先的方式,而是给待遍历的网页赋予一定的优先级,根据优先级进行遍历,我们称之为带偏好的遍历。

图的宽度优先遍历

我们说爬虫,是从一个点开始爬取的,从一个点爬取到另外一个点。这里假设A是我们的顶点,那么我们的具体算法如下。

1.原始节点A入队列


2.当队列里面的值不为空时,执行方法三,否则该算法为空。

3.出队列,获得对头节点A,访问节点A并且标记A已经被访问过了。

4.查找节点A的第一个邻接顶点col。


 5.若节点A相邻的其他节点,比如B,C,D,E,都没有访问过,讲会依次访问这节节点。这里会循环执行4和5的步骤,直到访问完相邻节点在执行步骤6;

6.若没有找到相邻节点了,将会把F节点顶级节点,重复步骤。


宽度优先遍历互联网

实际的爬虫项目是从一系列的种子链接开始的。所谓的种子链接,就好比宽度优先先遍历中的种子节点一样,实际的爬虫项目中种子链接可以有多个,而宽度优先的种子节点只有一个,必然要可以指定:http://www.csdn.net/像首页这样的宽度。

在页面中每个链接对应一个HTML页面货主其他文件,在这些文件中,每个页面都有对应的子节点,这些子节点就是HTML页面上对应的超链接。比如:


我们认为页面上每一个可以点击进去的超链接都是子节点,现在可以幻想一下整个页面的节点图。

整个宽度优先爬虫过程就是从一系列的种子节点开始,把这些网页的“子节点”也就是超链接提取出来,放入队列中依次进行抓取。被处理过的连接需要放入一张表中。每次新处理一个连接之前,需要查看这个连接是否已经存在于表中,如果存在,证明链接已经处理过,跳过,不做处理,否则就进行下一步处理。

初始的URL地址是爬虫系统中提供的种子URL(一般在系统的配置文件中指定)。当解析这些种子的URL所表示的网页时,会产生新的URL然后进行一下工作;

1.把解析出的链接和Visited表中的链接进行比较,若Visited表中不存在其链接,表示其从未被访问过。

2.把链接放入到TODO表中。

3.处理完毕后,再次从TODO表中取得一条链接,直接放入Visited表中。

4.针对这个链接所表示的网页,继续上述过程,如此往复循环。

宽度优先遍历是爬虫中使用最广泛的一种策略,之所以使用宽度优先搜索策略,主要有以下三点:

1.重要的网页往往离种子比较近,例如我们打开网址首页往往是最热门的资讯,随着不断的点击深入,所看到的网页的重要性越来越低。

2.万维网的实际深度最多只能达到17层,但到达某个网页总存在一条最短路径,而宽度优先遍历会以最快的速度到达这个网页。

3.宽度优先有利于多爬虫的合作抓取,多爬虫合作通常先抓取站内链接,被抓取的封闭性很强。

例子:

使用 HttpClient 和 HtmlParser 实现简易爬虫

前面有讲过HttpClient,这里重点说一下HTMLParser;

HtmlParser 简介

当今的 Internet 上面有数亿记的网页,越来越多应用程序将这些网页作为分析和处理的数据对象。这些网页多为半结构化的文本,有着大量的标签和嵌套的结构。当我们自己开发一些处理网页的应用程序时,会想到要开发一个单独的网页解析器,这一部分的工作必定需要付出相当的精力和时间。事实上,做为 JAVA 应用程序开发者, HtmlParser 为其提供了强大而灵活易用的开源类库,大大节省了写一个网页解析器的开销。 HtmlParser 是 http://sourceforge.net 上活跃的一个开源项目,它提供了线性和嵌套两种方式来解析网页,主要用于 html 网页的转换(Transformation) 以及网页内容的抽取 (Extraction)。HtmlParser 有如下一些易于使用的特性:过滤器 (Filters),访问者模式 (Visitors),处理自定义标签以及易于使用的 JavaBeans。正如 HtmlParser 首页所说:它是一个快速,健壮以及严格测试过的组件;以它设计的简洁,程序运行的速度以及处理 Internet 上真实网页的能力吸引着越来越多的开发者。 本文中就是利用HtmlParser 里提取网页里的链接,实现简易爬虫里的关键部分


感觉使用JAVA就是好,开源库绝对是最好最多的,什么PHP真的不能比。好了感慨一下,下面正式开始。

这里首先创建一个队列用来保存TODO

/** 
 * 自定义队列类 保存TODO表 
 */  
public class Queue {
		//使用链表实现队列
	private   static final LinkedList<Object> queue;
	static{
		queue=new LinkedList<Object>();
	}
	/** 
	  * 将t加入到队列中 
	  */  
	 public void inQueue(Object t) {  
	  queue.addLast(t);  
	 }  
	 /** 
	  * 移除队列中的第一项并将其返回 
	  */  
	 public Object outQueue() {  
	  return queue.removeFirst();  
	 }  
	 /** 
	  * 返回队列是否为空 
	  */  
	 public boolean isQueueEmpty() {  
	  return queue.isEmpty();  
	 }  
	 /** 
	  * 判断并返回队列是否包含t 
	  */  
	 public boolean contians(Object t) {  
	  return queue.contains(t);  
	 }  
	 /** 
	  * 判断并返回队列是否为空 
	  */  
	 public boolean empty() {  
	  return queue.isEmpty();  
	 }  
	
}
除了URL队列之外,在爬虫过程中,还需要一个数据结构来记录已经访问过的URL,每当需要访问一个URL的时候,首先在这个数据结构中进行查找,如果当前的URL已经存在,则丢弃它,这个数据结构有几个特点:

1.结构中保存的URL不能重复。
2.能够快速的查找。

3.不需要使用键值对的方式存储。

所以我们抛弃了Map,而使用HashSet作为存储结构。

public class SetQueue {
	/** 
	  * 已访问的url集合,即Visited表 
	  */  
	private static Set visitedUrl=new HashSet();
	//待访问的URL集合
	private static Queue unVisitedUrl=new Queue();
	
	/** 
	  * 添加到访问过的 URL 队列中 
	  */  
	 public static void addVisitedUrl(String url) {  
	  visitedUrl.add(url);  
	 }  
	 /** 
	  * 移除访问过的 URL 
	  */  
	 public static void removeVisitedUrl(String url) {  
	  visitedUrl.remove(url);  
	 }  
	 /** 
	  * 获得已经访问的 URL 数目 
	  */  
	 public static int getVisitedUrlNum() {  
	  return visitedUrl.size();  
	 }  
	 /** 
	  * 获得UnVisited队列 
	  */  
	 public static Queue getUnVisitedUrl() {  
	  return unVisitedUrl;  
	 }  
	 /** 
	  * 未访问的unVisitedUrl出队列 
	  */  
	 public static Object unVisitedUrlDeQueue() {  
	  return unVisitedUrl.outQueue();  
	 }  
	 /** 
	  * 保证添加url到unVisitedUrl的时候每个 URL只被访问一次 
	  */  
	 public static void addUnvisitedUrl(String url) {  
	  if (url != null && !url.trim().equals("") && !visitedUrl.contains(url)  
	    && !unVisitedUrl.contians(url))  
	   unVisitedUrl.inQueue(url);  
	 }  
	 /** 
	  * 判断未访问的 URL队列中是否为空 
	  */  
	 public static boolean unVisitedUrlsEmpty() {  
	  return unVisitedUrl.empty();  
	 }  
}
下面的代码详细说明了网页下载并处理的过程,和以前的相比,它考虑了如何存储网页,设置超时策略等等。

public class DownLoad {
	/** 
	  * 根据 URL 和网页类型生成需要保存的网页的文件名,去除 URL 中的非文件名字符 
	  */  
	 private String getFileNameByUrl(String url, String contentType) {  
	  // 移除 "http://" 这七个字符  
	  url = url.substring(7);  
	  // 确认抓取到的页面为 text/html 类型  
	  if (contentType.indexOf("html") != -1) {  
	   // 把所有的url中的特殊符号转化成下划线  
	   url = url.replaceAll("[\\?/:*|<>\"]", "_") + ".html";  
	  } else {  
	   url = url.replaceAll("[\\?/:*|<>\"]", "_") + "."  
	     + contentType.substring(contentType.lastIndexOf("/") + 1);  
	  }  
	  return url;  
	 }  
	 /** 
	  * 保存网页字节数组到本地文件,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.执行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("请检查你的http地址是否正确");  
	   e.printStackTrace();  
	  } catch (IOException e) {  
	   // 发生网络异常  
	   e.printStackTrace();  
	  } finally {  
	   // 释放连接  
	   getMethod.releaseConnection();  
	  }  
	  return filePath;  
	 }  
}
接下来,将要从获得的网页中提取URL,现在就要使用到我们的 HtmlParser。

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() {
		
		@Override
		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;  
	 }  
}
最后写我们的爬虫主方法:

public class MyCrawler {
	/** 
	  * 使用种子初始化URL队列 
	  */  
	 private void initCrawlerWithSeeds(String[] seeds) {  
	  for (int i = 0; i < seeds.length; i++)  
	   SetQueue.addUnvisitedUrl(seeds[i]);  
	 }  
	 // 定义过滤器,提取以 <a target=_blank href="http://www.xxxx.com/" style="color: rgb(0, 102, 153); text-decoration: none;">http://www.xxxx.com</a>开头的链接  
	 public void crawling(String[] seeds) {  
	  LinkFilter filter = new LinkFilter() {  
	   @Override
	public boolean accept(String url) {  
	    if (url.startsWith("http://www.csdn.net/"))  
	     return true;  
	    else  
	     return false;  
	   }  
	  };  
	  // 初始化 URL 队列  
	  initCrawlerWithSeeds(seeds);  
	  // 循环条件:待抓取的链接不空且抓取的网页不多于 1000  
	  while (!SetQueue.unVisitedUrlsEmpty()  
	    && SetQueue.getVisitedUrlNum() <= 1000) {  
	   // 队头 URL 出队列  
	   String visitUrl = (String) SetQueue.unVisitedUrlDeQueue();  
	   if (visitUrl == null)  
	    continue;  
	   DownLoad downLoader = new DownLoad();  
	   // 下载网页  
	   downLoader.downloadFile(visitUrl);  
	   // 该 URL 放入已访问的 URL 中  
	   SetQueue.addVisitedUrl(visitUrl);  
	   // 提取出下载网页中的 URL  
	   Set<String> links = HtmlParserTool.extracLinks(visitUrl, filter);  
	   // 新的未访问的 URL 入队  
	   for (String link : links) {  
	    SetQueue.addUnvisitedUrl(link);  
	   }  
	  }  
	 }  
	
	 public static void main(String[] args) {  
	  MyCrawler crawler = new MyCrawler();  
	  crawler.crawling(new String[] { "http://www.csdn.net/" });  
	 }  
}
这里去抓取CSDN的主页,不过结果只抓取到了



然后出现了

Invalid uri 'http://www.csdn.net/tag/云计算': escaped absolute path not valid 小样的下次再抓取你。

最后附上一张抓取的流程图


源码如下

点击打开链接





评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值