跟我一步一步学爬虫---宽度优先遍历篇(四)

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

1、重要的网页往往离种子比较近,例如我们打开的新闻网站的时候往往是最热门的新闻,随着不断的深入冲浪,所看到的网页的重要性越来越低。

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

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


本节需要多导入一个HtmlParser.jar   点击下载    其他jar包从第一篇中下载


先画一个程序的结构

首先,需要定义一个图中所描述的“URL入队列”,这里使用了LinkedList来实现这个队列

Quequ类

package kuandu;

import java.util.LinkedList;

/**
 * 队列,保存将要访问的URL
 */
public class Quequ {
	
	//使用链表保存队列
	private LinkedList quequ = new LinkedList();
	
	//入队列
	public void enQuequ(Object t) {
		quequ.addLast(t);
	}
	
	//出队列
	public Object deQuequ() {
		return quequ.removeFirst();
	}
	
	//判断是否是空
	public boolean isQuequEmpty() {
		return quequ.isEmpty();
	}
	
	//判断队列是否包含t
	public boolean contians(Object t) {
		return quequ.contains(t);
	}
	
	public boolean empty() {
		return quequ.isEmpty();
	}
}

除了URL队列之外,在爬虫的过程中,还需要一个数据结构来记录已经访问过的URL。每当要访问一个URL时候,首先阿紫这个数据结构中进行查找,如果当前的URL已经存在,则丢弃它。这个数据结构要有两个特点:

1、结构中保存的URL不能重复。

2、能快速的查找。

针对以上两点我们选择HashSet作为存储结构。

LinkQuequ类

package kuandu;

import java.util.LinkedList;

/**
 * 队列,保存将要访问的URL
 */
public class Quequ {
	
	//使用链表保存队列
	private LinkedList quequ = new LinkedList();
	
	//入队列
	public void enQuequ(Object t) {
		quequ.addLast(t);
	}
	
	//出队列
	public Object deQuequ() {
		return quequ.removeFirst();
	}
	
	//判断是否是空
	public boolean isQuequEmpty() {
		return quequ.isEmpty();
	}
	
	//判断队列是否包含t
	public boolean contians(Object t) {
		return quequ.contains(t);
	}
	
	public boolean empty() {
		return quequ.isEmpty();
	}
}

下面的代码详细的说明了网页下载并处理的过程。

DownLoadFile类

package kuandu;

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 DownLoadFile {

	/**
	 * 根据URL和网页类型生成需要保存的网页的文件名,去除URL中非文件名字符
	 */
	public String getFileNameByUrl(String url, String contentType) {
		
		//移除http
		url = url.substring(7);
		
		//text/html类型
		if (contentType.indexOf("html") != -1) {
			return url.replaceAll("[\\?/:*|<>\"]", "_") + ".html";
		}
		//如application/pdf类型
		else {
			return url.replaceAll("[\\?/:*|<>\"]", "_") + "." + contentType.substring(contentType.lastIndexOf("/") + 1);
		}
	}
	
	/**
	 * 保存网页字节数组到本地文件,filePath为保存文件的相对地址
	 */
	private void saveToLoacl(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、生成HttppClient对象并生成参数
		HttpClient httpClient = new HttpClient();
		//设置http连接超时5s
		httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5 * 1000);
		httpClient.getParams().setParameter(HttpMethodParams.USER_AGENT, "Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.1.2) Gecko/20090803");
		//2、生成GetMethod对象并生成参数
		GetMethod getMethod = new GetMethod(url);
		//设置请求超时5s
		getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5 * 1000);
		//设置请求重试处理
		getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
		
		//执行HttpGet请求
		try {
			int statusCode = httpClient.executeMethod(getMethod);
			
			if (statusCode != HttpStatus.SC_OK) {
				System.err.println("请求失败:" + getMethod.getStatusLine());
			}
			
			//4、处理http响应内容
			byte[] responseBody = getMethod.getResponseBody();
			//根据网页的URL生成保存时的文件名
			filePath = "c:\\temp\\" + getFileNameByUrl(url, getMethod.getResponseHeader("Content-Type").getValue());
			
			//5、保存
			saveToLoacl(responseBody, filePath);
		} catch (HttpException e) {
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return filePath;
	}
}

接下来,演示如何从获得的网页中提取URL。JAVA有一个非常实用的开源工具包HtmlParser,它专门针对Html页面进行处理,不仅能提取URL,还能提取文本以及你想要的任何内容。

HtmlParserTool类

package kuandu;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
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 {
			URLConnection conn = null;
			URL url1 = new URL(url);
			conn = url1.openConnection();
			conn.setRequestProperty("User-Agent","Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.1.2) Gecko/20090803"); 
			Parser parser = new Parser();
			parser.setConnection(conn);
			parser.setEncoding("gb2312");
			
			//过滤<frame>的filter,用来提取里面的src属性
			NodeFilter frameFilter = new NodeFilter() {
				
				@Override
				public boolean accept(Node node) {
					if (node.getText().startsWith("src=")) {
						return true;
					}
					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
					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) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return links;
	}
}


最后再写一个爬虫的主函数就OK了。

MyCrawler类

package kuandu;

import java.util.Set;

public class MyCrawler {

	/**
	 * 使用种子初始化URL队列
	 * @return
	 * @param seeds 种子URL
	 */
	private void initCrawlerWithSeeds(String[] seeds) {
		for(int i = 0; i < seeds.length; i++) {
			LinkQuequ.addUnVisitedUrl(seeds[i]);
		}
	}
	
	/**
	 * 抓取过程
	 * @return
	 * @param seeds
	 */
	private void crawling(String[] seeds, final String headTo) {
		//定义过滤器,定义以http://www.lietu.com开头的连接
		LinkFilter filter = new LinkFilter() {
			@Override
			public boolean accept(String url) {
				if (url.startsWith(headTo)) {
					return true;
				}
				else {
					return false;
				}
			}
		};
		//初始化URL队列
		initCrawlerWithSeeds(seeds);
		
		//循环条件:待抓取的连接不空且抓取的网页不超过1000个
		while (!LinkQuequ.unVisitedUrlsdEmpty() && LinkQuequ.getVisitedUrlNum() < 1000) {
			//对列头URL出列
			String visitUrl = (String)LinkQuequ.unVisitedUrlDeQuequ();
			if (visitUrl == null) {
				continue;
			}
			DownLoadFile downLoadFile = new DownLoadFile();
			//下载网页
			downLoadFile.downLoadFile(visitUrl);
			//放入已访问的URL队集合中
			LinkQuequ.addVisitedUrl(visitUrl);
			//提取出下载网页中的URL
			Set<String> links = HtmlParserTool.extracLinks(visitUrl, filter);
			//新的未访问的URL队列
			for (String link : links) {
//				System.out.println(link);
				LinkQuequ.addUnVisitedUrl(link);
			}
			
		}
	}
	
	//程序入口
	public static void main(String[] args) {
		
		String[] seeds = new String[] {"http://bbs.csdn.net/forums/Java_WebDevelop"};//要爬的网站
		String headTo = "http://bbs.csdn.net";//以他开头的
		MyCrawler myCrawler = new MyCrawler();
		myCrawler.crawling(seeds, headTo);
	}
}


实践证明,CSDN是可以爬得。http://可是不能够省略的哟。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值