圖的遍歷分爲寬度優先遍歷和深度優先遍歷兩種方式,由於網絡的無限性,爬蟲採用深度優先遍歷會導致陷入過深,故應採用寬度優先遍歷,同時,還可以根據遍歷網頁的權重分配優先級,這就是帶偏好的遍歷。寬度優先遍歷從一系列種子節點開始後,應將之後的子節點依次放入待訪問隊列,同時,應該保存一張已訪問的表,遍歷前應先查詢是否訪問過,從而避免重複訪問。即可分爲下列步驟:
1. 把解析出來的鏈接和已訪問表中的鏈接進行比較,若不存在此鏈接,則表示其未被訪問過。
2. 把鏈接放入TODO表,即待處理表。
3. 處理完畢後,再次從TODO表中取出一條鏈接進行處理,並放入以訪問表。
4. 針對該連接所示網頁,再次抓取和解析新鏈接,重複上述過程。
採用寬度優先搜索策略有以下原因:
1. 重要的網頁往往離種子較近。
2. 萬維網的深度最多能達到17層。總存在一條權重最短的路徑能快速到達指定網頁。
3. 寬度優先遍歷有利於多爬蟲合作抓取,多爬蟲合作通常先抓取站內鏈接,封閉性很強。
4. 鏈接優化:能避開抓取鏈接的死循環以及該抓取的資源沒有抓取到。
在這裏,我們使用HttpClient和Html Parser兩個工具包實現抓取,首先是自定義一個待訪問隊列。
package me.zzx.crawler;
import java.util.LinkedList;
/**
* 隊列,保存將要訪問的URL
* @author zzx
*
*/
public class Queue {
//使用鏈表實現隊列
private LinkedList<Object> queue = new LinkedList<Object>();
//入隊
public void enQueue(Object o) {
queue.addLast(o);
}
//出隊
public Object deQueue() {
return queue.removeFirst();
}
//判斷隊列是否爲空
public boolean isQueueEmpty() {
return queue.isEmpty();
}
//判斷隊列是否包含o
public boolean contains(Object o) {
return queue.contains(o);
}
}
然後用一個哈希表存放已訪問鏈接,並和待訪問隊列一起封裝成LinkQueue
package me.zzx.crawler;
import java.util.HashSet;
import java.util.Set;
/**
* 保存已訪問過的URL
* @author zzx
*
*/
public class LinkQueue {
//已訪問的URL集合
private static Set<Object> visitedUrls = new HashSet<Object>();
//待訪問的URL集合
private static Queue unVisitedUrls = new Queue();
//獲得URL隊列
public static Queue getUnVisitedUrl() {
return unVisitedUrls;
}
//添加到訪問過的URL隊列中
public static void addVisitedUrl(String url) {
visitedUrls.add(url);
}
//移除訪問過的URL
public static void removeVisitedUrl(String url) {
visitedUrls.remove(url);
}
//未訪問的URL出隊
public static Object unVisitedUrlDequeue() {
return unVisitedUrls.deQueue();
}
//添加到待訪問的URL隊列中,保證每個URL只被訪問一次
public static void addUnVisitedUrl(String url) {
if(url != null && !url.trim().equals("")
&& !visitedUrls.contains(url)
&& !unVisitedUrls.contains(url)) {
unVisitedUrls.enQueue(url);
}
}
//獲得已訪問的URL數目
public static int getVisitedUrlNum() {
return visitedUrls.size();
}
//判斷待訪問的URL隊列是否爲空
public static boolean unVisitedUrlIsEmpty() {
return unVisitedUrls.isQueueEmpty();
}
}
再創建一個文件下載工具類,用於抓取的下載工作
package me.zzx.crawler;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
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;
/**
* 下載網頁工具類
* @author zzx
*
*/
public class DownloadFileUtil {
/**
* 根據URL和網頁類型生成需要保存的網頁的文件名,去除URL中的非文件名字符
*/
public static String getFilenameByUrl(String url, String contentType) {
//移除http://或https://
url = url.charAt(4) == ':' ? url.substring(7) : url.substring(8);
//text/html類型
if(contentType.indexOf("html") != -1) {
url = url.replaceAll("[\\?/:*|<>\"]", "_");
return url.contains(".html")? url : url + ".html";
} else {
//application/pdf等其他類型
url = url.replaceAll("[\\?/:*|<>\"]", "_") + "."
+ contentType.substring(contentType.lastIndexOf("/") + 1);
return url;
}
}
/**
* 保存網頁字節數組到本地文件,filePath爲要保存文件的相對地址
*/
private static void saveToLocal(InputStream data, String filePath) {
DataOutputStream dos;
try {
dos = new DataOutputStream(new FileOutputStream(new File(filePath)));
int tempByte = -1;
while(((tempByte = data.read()) >= 0)) {
dos.write(tempByte);
}
dos.flush();
dos.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
data = null;
dos = null;
}
}
/**
* 下載URL指向的網頁
*/
public static String downloadFile(String url) {
String filePath = null;
//生成HttpClient對象
HttpClient httpClient = new HttpClient();
//設置HTTP連接超時5秒
httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
//生成GetMethod對象
GetMethod get = new GetMethod(url);
//設置get請求超時5秒
get.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
//設置請求重試處理
get.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
//執行HTTP GET請求
try {
int statusCode = httpClient.executeMethod(get);
//判斷訪問狀態碼
if(statusCode != HttpStatus.SC_OK) {
System.err.println("Method Failed: " + get.getStatusLine());
}
//處理HTTP響應內容
InputStream responseBody = get.getResponseBodyAsStream();
//根據網頁URL生成保存時的文件名
filePath = "temp\\" + getFilenameByUrl(url, get.getResponseHeader("Content-Type").getValue());
saveToLocal(responseBody, filePath);
} catch (HttpException e) {
//發生致命的異常,可能是協議不對或者返回的內容有問題
System.out.println("Please check your provided http address!");
e.printStackTrace();
} catch (IOException e) {
//發生IO異常
e.printStackTrace();
} finally {
//釋放連接,重要
get.releaseConnection();
}
return filePath;
}
}
另外提供下載文件工具類的基於HttpClient4的寫法供參考
package me.zzx.crawler;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.UnknownHostException;
import javax.net.ssl.SSLException;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
/**
* 下載網頁工具類
* @author zzx
*
*/
public class DownloadFileUtil {
private static final int DEFAULT_RETRY_TIME = 5;
/**
* 根據URL和網頁類型生成需要保存的網頁的文件名,去除URL中的非文件名字符
*/
public static String getFilenameByUrl(String url, String contentType) {
//移除http://或https://
url = url.charAt(4) == ':' ? url.substring(7) : url.substring(8);
//text/html類型
if(contentType.indexOf("html") != -1) {
url = url.replaceAll("[\\?/:*|<>\"]", "_");
return url.contains(".html")? url : url + ".html";
} else {
//application/pdf等其他類型
url = url.replaceAll("[\\?/:*|<>\"]", "_") + "."
+ contentType.substring(contentType.lastIndexOf("/") + 1);
return url;
}
}
/**
* 保存網頁字節數組到本地文件,filePath爲要保存文件的相對地址
*/
private static void saveToLocal(InputStream data, String filePath) {
DataOutputStream dos;
try {
dos = new DataOutputStream(new FileOutputStream(new File(filePath)));
int tempByte = -1;
while(((tempByte = data.read()) >= 0)) {
dos.write(tempByte);
}
dos.flush();
dos.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
data = null;
dos = null;
}
}
/**
* 下載URL指向的網頁
*/
public static String downloadFile(String url) {
String filePath = null;
//生成HttpClient對象
HttpClient httpClient = new DefaultHttpClient();
HttpParams params = httpClient.getParams();
//設置HTTP連接超時5秒
HttpConnectionParams.setConnectionTimeout(params, 5000);
//生成GetMethod對象
HttpGet get = new HttpGet(url);
//設置get請求超時5秒
get.getParams().setParameter(HttpConnectionParams.SO_TIMEOUT, 5000);
//設置請求重試處理
((AbstractHttpClient)httpClient).setHttpRequestRetryHandler(new HttpRequestRetryHandler() {
@Override
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
if (executionCount >= DEFAULT_RETRY_TIME) {
return false;
} else if (exception instanceof InterruptedIOException) {
// Timeout
return false;
} else if (exception instanceof UnknownHostException) {
// Unknown host
return false;
} else if (exception instanceof ConnectException) {
return false;
} else if (exception instanceof SSLException) {
// SSL handshake exception
return false;
}
HttpRequest request = (HttpRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST);
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
// Retry if the request is considered idempotent
if (idempotent)
return true;
return false;
}
});
//執行HTTP GET請求
try {
HttpResponse response = httpClient.execute(get);
int statusCode = response.getStatusLine().getStatusCode();
//判斷訪問狀態碼
if(statusCode != HttpStatus.SC_OK) {
System.err.println("Method Failed: " + statusCode);
}
//處理HTTP響應內容
InputStream responseBody = response.getEntity().getContent();
//根據網頁URL生成保存時的文件名
filePath = "temp\\" + getFilenameByUrl(url, response.getEntity().getContentType().getValue());
saveToLocal(responseBody, filePath);
} catch (IOException e) {
//發生IO異常
e.printStackTrace();
} finally {
//釋放連接,重要
get.abort();
}
return filePath;
}
}
再使用引入的Html Parser,構建一個網頁鏈接解析類
package me.zzx.crawler;
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.ImageTag;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
public class HtmlParserUtil {
//獲取一個網站上的鏈接,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() {
private static final long serialVersionUID = 1L;
@Override
public boolean accept(Node node) {
if(node.getText().startsWith("frame src="))
return true;
return false;
}
};
//OrFilter來設置過濾<a><frame><img>標籤
NodeFilter[] predicates = new NodeFilter[]{new NodeClassFilter(ImageTag.class), new NodeClassFilter(LinkTag.class), frameFilter};
OrFilter linkFilter = new OrFilter(predicates);
//得到所有經過過濾的標籤
NodeList list = parser.extractAllNodesThatMatch(linkFilter);
for(int i = 0; i < list.size(); i++) {
Node tag = list.elementAt(i);
//<a>標籤
if(tag instanceof LinkTag) {
LinkTag link = (LinkTag) tag;
String linkUrl = link.getLink();
if(filter.accept(linkUrl)) links.add(linkUrl);
//<image>標籤
} else if(tag instanceof ImageTag) {
ImageTag image = (ImageTag) tag;
String imageUrl = image.getImageURL();
if(filter.accept(imageUrl)) links.add(imageUrl);
//<frame>標籤
} else {
//提取frame裏的src屬性的鏈接
String frame = tag.getText();
//System.out.println(frame);
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;
}
}
創建一個監聽器接口,用於監聽抓取特定鏈接
package me.zzx.crawler;
public interface LinkFilter {
public boolean accept(String url);
}
最後是爬蟲的主程序
package me.zzx.crawler;
import java.util.Set;
public class TestCrawler {
/**
* 使用種子初始化URL隊列
* @param seeds 種子URL
*/
private void initCrawlerWithSeeds(String[] seeds) {
for(String seed : seeds)
LinkQueue.addUnVisitedUrl(seed);
}
/**
* 抓取過程
* @param seeds
*/
public void crawling(String[] seeds) {
//定義過濾器,提取以http(s)://www.alibaba.com開頭的鏈接
LinkFilter filter = new LinkFilter() {
@Override
public boolean accept(String url) {
if(url.startsWith("http://www.alibaba.com") || url.startsWith("https://www.alibaba.com"))
return true;
return false;
}
};
//初始化URL隊列
initCrawlerWithSeeds(seeds);
//循環條件:待抓取隊列不爲空且已抓取的網頁不多於1000
while(!LinkQueue.unVisitedUrlIsEmpty() && LinkQueue.getVisitedUrlNum() <= 1000) {
String visitingUrl = (String) LinkQueue.unVisitedUrlDequeue();
if(visitingUrl == null) continue;
//下載網頁
DownloadFileUtil.downloadFile(visitingUrl);
//該URL放入已訪問的URL中
LinkQueue.addVisitedUrl(visitingUrl);
//提取出新的URL
Set<String> links = HtmlParserUtil.extracLinks(visitingUrl, filter);
//新的未訪問URL入隊
for(String link : links) {
LinkQueue.addUnVisitedUrl(link);
}
}
}
//main方法入口
public static void main(String[] args) {
TestCrawler crawler = new TestCrawler();
crawler.crawling(new String[] {"https://www.alibaba.com"});
}
}
在將抓取的URL鏈接入隊後,不一定嚴格按照先進先出的策略去訪問,而是可以有選擇地將權重值較高地鏈接先訪問,影響權重的因素很多,包括鏈接地歡迎度IB(P),鏈接地重要度IL(P)以及平均鏈接深度等。在這裏,我們使用Java內置地支持優先級的隊列,來替換掉原有LinkQueue的實現,代碼如下
package me.zzx.crawler;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;
/**
* 保存已訪問過的URL
* @author zzx
*
*/
public class PreferenceLinkQueue {
//已訪問的URL集合
private static Set<Object> visitedUrls = new HashSet<Object>();
//待訪問的URL集合
private static PriorityQueue<Object> unVisitedUrls = new PriorityQueue<Object>();
//獲得URL隊列
public static PriorityQueue<Object> getUnVisitedUrl() {
return unVisitedUrls;
}
//添加到訪問過的URL隊列中
public static void addVisitedUrl(String url) {
visitedUrls.add(url);
}
//移除訪問過的URL
public static void removeVisitedUrl(String url) {
visitedUrls.remove(url);
}
//未訪問的URL出隊
public static Object unVisitedUrlDequeue() {
return unVisitedUrls.poll();
}
//添加到待訪問的URL隊列中,保證每個URL只被訪問一次
public static void addUnVisitedUrl(String url) {
if(url != null && !url.trim().equals("")
&& !visitedUrls.contains(url)
&& !unVisitedUrls.contains(url)) {
unVisitedUrls.add(url);
}
}
//獲得已訪問的URL數目
public static int getVisitedUrlNum() {
return visitedUrls.size();
}
//判斷待訪問的URL隊列是否爲空
public static boolean unVisitedUrlIsEmpty() {
return unVisitedUrls.isEmpty();
}
}