之前写的是获取单个网页的内容,但是在实际项目中是需要遍历整个网络的相关网页。图论中有深度优先遍历和宽度优先遍历,深度优先可能会因为过”深“或者进入黑洞;同时,也不能完全按照宽度优先进行遍历,需要进行优先级排序。
1.图的宽度优先遍历
先回顾一下图论中的有向图的BFS宽度优先遍历算法。
例题:如图,根据BFS写出各个节点的遍历顺序
首先任选一点A作为开始节点(种子节点)。
操作 | 队列中的元素 |
---|---|
初始 | 空 |
A入队 | A |
A出队 | 空 |
BCDEF入队 | BCDEF |
B出队 | CDEF |
C出队 | DEF |
D出队 | EF |
E出队 | F |
H入队 | FH |
F出队 | H |
G入队 | HG |
H出队 | G |
I入队 | GI |
G出队 | I |
I出队 | 空 |
所以图的优先遍历顺序为ABCDEFHGI
算法总结:
1.任选顶点V入队
2.当队列非空时继续执行否则停止算法
3.队列头部元素M出队列,访问并且标记M已经被访问过
4.查找M的邻接顶点X
5.若X已经被访问则继续寻找邻接顶点,若没有,则X入队
6.循环第五步,直到M的所有邻接顶点均已入栈,若M的所有邻接顶点均已被访问过(即没有一个X入栈)则跳转步骤2
2.宽度优先遍历互联网
在网页中所有的节点都是html网页,对于非HTML文档可以看成是终端节点。并且网络的宽度优先遍历不是从单个的链接开始的,而是从一系列链接开始的,把这些网页中的“子节点”就是超链接提取出来,放入TODO队列重依次进行抓取。被处理过的链接需要放入一张Visited表中,每次处理一个链接之前,需要判断这个链接是否已经在Visited表中,若是,则已经处理过,则跳过这个链接,若不是,则继续处理。
上面拿到例题,用TODO表和Visited表来表示就是:
TODO表 | Visited表 |
---|---|
A | 空 |
BCDEF | A |
CDEF | AB |
DEF | ABC |
EF | ABCD |
FH | ABCDE |
HG | ABCDEF |
GI | ABCDEFH |
I | ABCDEFHG |
空 | ABCDEFHGI |
为什么使用宽度优先遍历的爬虫策略
他是爬虫中使用最广泛的一种策略
1.重要的网页往往离最初选择的种子节点比较近,随着宽度的深入,网页的重要性就会降低
2.万维网的实际深度最多能够达到17层,但是到达某一个具体的网页总是存在一条很短路径,宽度优先遍历总是能以最快的速度到达这个网页
3.宽度有限有利于多爬虫的合作抓取,多爬虫通常先抓取站内的链接,抓取的封闭性很强
3.宽度优先遍历的Java实现
实现一个存储URL的队列
public class Queue {
//使用链表实现队列
private LinkedList queue=new LinkedList();
//入队列
public void enQueue(Object t){
queue.addLast(t);
}
//出队列
public Object deQueue(){
return queue.removeFirst();
}
//判断队列是否为空
public boolean isQueueEmpty(){
return queue.isEmpty();
}
//判断队列是否包含某一个元素
public boolean contains(Object t){
return queue.contains(t);
}
}
这里书上多了一个函数,可能是作者忘记删除的:
这两个函数的作用是一模一样的,所以只需要其中一个就行了,删除另外一个。
实现一个存储已经访问过的URL的队列
每当从URL队列中取得一个url进行查询之前,需要先在
visitedUrl队列中查询是否已经访问过该节点,然后才能对该节点进行处理
public class LinkQueue {
//已经访问的URL集合
private static Set visitedUrl = new HashSet();
//带访问的url集合
private static Queue unVisitedUrl = new Queue();
//获得URL队列
public static Queue getUnVisitedUrl() {
return unVisitedUrl;
}
//添加到访问过的URL队列中
public static void addVisitedUrl(String url) {
visitedUrl.add(url);
}
//移除访问过的URL
public static void removeVisitedUrl(String url) {
visitedUrl.remove(url);
}
//未访问的URL出队列
public static Object unVisitedUrl(String url) {
return unVisitedUrl.deQueue();
}
//保证每个URL只能被访问一次
/**
* 每一个url不是null并且字符串有效,不包含在已经访问过的节点集合中
* 也不包含在没有访问过的节点集合中
*
* @param url
*/
public static void addUnvisitedUrl(String url) {
if (url != null && !url.trim().equals("") &&
!visitedUrl.contains(url) &&
!unVisitedUrl.contains(url))
unVisitedUrl.enQueue(url);
}
//获得已经访问过的URL的数量
public static int getVisitedUrlNum() {
return visitedUrl.size();
}
//判断未访问的URL队列是否为空
public static boolean unVisitedUrlEmpty() {
return unVisitedUrl.isQueueEmpty();
}
}
实现一个网页信息下载类
public class DownLoadFile {
//根据URL和网页类型生成需要保存的网页的文件名,取出URL中的非文件名字符
public String getFileNameByUrl(String url, String contentType) {
//移除http
url = url.substring(7);
//text或者html类型
if (contentType.indexOf("html") != -1) {
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 {
//System.out.println(filepath);
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 (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//下载URL指向的网页
public String downloadFile(String url) {
String filepath = null;
//1.生成HttpClient对象并设置参数
HttpClient httpClient = new HttpClient();
//设置HTTP链接超时5秒
httpClient.getHttpConnectionManager().getParams()
.setConnectionTimeout(5000);
//2.生成GetMethod对象并设置参数
GetMethod getMethod = new GetMethod(url);
//设置GetMethod请求超时5秒
getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
//设置请求重试处理
getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler());
//3.执行HttpGet请求
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());
//System.out.println(filepath);
saveToLocal(responseBody, filepath);
} catch (IOException e) {
e.printStackTrace();
} finally {
getMethod.releaseConnection();
}
return filepath;
}
}
这里需要注意的是,书上的地址保存是temp/网页名,这里的temp文件夹需要自己新建,建在所在工程文件的文件夹下,之后所有爬到的网页都会在这个temp文件夹里面这是相对路径,你也可以在某一个盘上建一个绝对路径的文件夹,如D://temp//
页面解析工具类
这个类需要用到外部包HTMLParser 2.0的包,一定要下载最新的包,不然里面缺少某些类如org.htmlParser.Parser。
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("UTF-8");
//过滤<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();
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 interface LinkFilter {
public boolean accept(String url);
}
}
1.书上也说了LinkFilter是一个接口,并且实现为内部类
2.编码形式设置为UTF-8 parser.setEncoding("UTF-8");
不然可能会出现中文字或者其他字符乱码的情况
主程序
public class Main {
/**
* 使用种子初始化URL队列
*
* @param seeds
*/
private void initCrawlerWithSeeds(String[] seeds) {
for (int i = 0; i < seeds.length; i++) {
LinkQueue.addUnvisitedUrl(seeds[i]);
}
}
/**
* 抓取过程
*
* @param seeds
*/
public void crawling(String[] seeds) {
//定义过滤器,提取以http://www.lietu.com开头的链接
HtmlParserTool.LinkFilter filter = new HtmlParserTool.LinkFilter() {
@Override
public boolean accept(String url) {
if (url.startsWith("http://www.lietu.com")) return true;
else return false;
}
};
//抓取过程
initCrawlerWithSeeds(seeds);
//循环条件:待抓取的链接不空并且抓取的网页数量不多于1000
while (!LinkQueue.unVisitedUrlEmpty() && LinkQueue.getVisitedUrlNum() <= 1000) {
//队头出队
String visitUrl = (String) LinkQueue.unVisitedDeUrl();
if (visitUrl == null) continue;
DownLoadFile downLoad = new DownLoadFile();
//下载网页
downLoad.downloadFile(visitUrl);
//将该URL放入已访问的URL队列中
LinkQueue.addVisitedUrl(visitUrl);
//提取出下载网页中的URL
Set<String> links = HtmlParserTool.extracLinks(visitUrl, filter);
//新的未访问的url入队
for (String link : links) {
LinkQueue.addUnvisitedUrl(link);
}
}
}
public static void main(String[] args) {
Main crawler = new Main();
crawler.crawling(new String[]{"http://lietu.com"});
}
}
得到的结果
换一个百度搜索www.baidu.com,搜以www.baidu.com开头的网页结果: