在实际应用中,使用网络爬虫遍历互联网,把网络中我们感兴趣的网页全部抓取过来。为便于理解,我们把整个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.宽度优先有利于多爬虫的合作抓取,多爬虫合作通常先抓取站内链接,被抓取的封闭性很强。
当今的 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
除了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 extracLinks(String url, LinkFilter filter) {
Set links = new HashSet();
try {
Parser parser = new Parser(url);
parser.setEncoding("gb2312");
// 过滤 标签的 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 来设置过滤 标签和 标签
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)// 标签
{
LinkTag link = (LinkTag) tag;
String linkUrl = link.getLink();// URL
if (filter.accept(linkUrl))
links.add(linkUrl);
} else// 标签
{
// 提取 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) {
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]);
}
// 定义过滤器,提取以 http://www.xxxx.com开头的链接
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 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 小样的下次再抓取你。
最后附上一张抓取的流程图
源码如下
点击打开链接