之前寫的是獲取單個網頁的內容,但是在實際項目中是需要遍歷整個網絡的相關網頁。圖論中有深度優先遍歷和寬度優先遍歷,深度優先可能會因爲過”深“或者進入黑洞;同時,也不能完全按照寬度優先進行遍歷,需要進行優先級排序。
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開頭的網頁結果: