轉載:http://blog.csdn.net/mailbomb/archive/2004/11/07/171356.aspx
創(chuàng)建智能網(wǎng)絡蜘蛛
——如何使用Java網(wǎng)絡對象和HTML對象(翻譯)
作者:Mark O. Pendergast
原文:http://www.javaworld.com/javaworld/jw-11-2004/jw-1101-spider.html
摘要
你是否想過創(chuàng)建自己的符合特定標準的網(wǎng)站數(shù)據(jù)庫呢?網(wǎng)絡蜘蛛,有時也稱為網(wǎng)絡爬蟲,是一些根據(jù)網(wǎng)絡鏈接從一個網(wǎng)站到另外一個網(wǎng)站,檢查內容和記錄位置的程序。商業(yè)搜索站點使用網(wǎng)絡蜘蛛豐富它們的數(shù)據(jù)庫,研究人員可以使用蜘蛛獲得相關的信息。創(chuàng)建自己的蜘蛛搜索的內容、主機和網(wǎng)頁特征,比如文字密度和內置的多媒體內容。這篇文章將告訴你如何使用Java的HTML和網(wǎng)絡類來創(chuàng)建你自己的功能強大的網(wǎng)絡蜘蛛。
這篇文章將介紹如何在標準Java網(wǎng)絡對象的基礎上創(chuàng)建一個智能的網(wǎng)絡蜘蛛。蜘蛛的核心是一個基于關鍵字/短語標準和網(wǎng)頁特征進行深入網(wǎng)絡搜索的遞歸程序。搜索過程在圖形上類似于JTree結構。我主要介紹的問題,例如處理相關的URL,防止循環(huán)引用和監(jiān)視內存/堆棧使用。另外,我將介紹再訪問和分解遠程網(wǎng)頁中如何正確是用Java網(wǎng)絡對象。
l 蜘蛛示例程序
示例程序包括用戶界面類SpiderControl、網(wǎng)絡搜索類Spider,兩個用作創(chuàng)建JTree顯示結果的類UrlTreeNode和UrlNodeRenderer,和兩個幫助驗證用戶界面中數(shù)字輸入的類IntegerVerifier和VerifierListener。文章末尾的資源中有完整代碼和文檔的璉接。
SpiderControl界面由三個屬性頁組成,一個用來設置搜索參數(shù),另一個顯示結果搜索樹(JTree),第三個顯示錯誤和狀態(tài)信息,如圖1
圖1 搜索參數(shù)屬性頁
搜索參數(shù)包括訪問網(wǎng)站的最大數(shù)量,搜索的最大深度(鏈接到鏈接到鏈接),關鍵字/短語列表,搜索的頂級主機,起始網(wǎng)站或者門戶。一旦用戶輸入了搜索參數(shù),并按下開始按鈕,網(wǎng)絡搜索將開始,第二個屬性頁將顯示搜索的進度。
圖2 搜索樹
一個Spider類的實例以獨立進程的方式執(zhí)行網(wǎng)絡搜索。獨立進程的使用是為了SpiderControl模塊可以不斷更新搜索樹顯示和處理停止搜索按鈕。當Spider運行時,它不斷在第二個屬性頁中為JTree增加節(jié)點(UrlTreeNode)。包含關鍵字和短語的搜索樹節(jié)點以藍色顯示(UrlNodeRenderer)。
當搜索完成以后,用戶可以查看站點的統(tǒng)計,還可以用外部瀏覽器(默認是位于Program Files目錄的Internet Explorer)查看站點。統(tǒng)計包括關鍵字出現(xiàn)次數(shù),總字符數(shù),總圖片數(shù)和總鏈接數(shù)。
l Spider類
Spider類負責搜索給出起點(入口)的網(wǎng)絡,一系列的關鍵字和主機,和搜索深度和大小的限制。Spider繼承了Thread,所以可以以獨立線程運行。這允許SpiderControl模塊不斷更新搜索樹顯示和處理停止搜索按鈕。
構造方法接受包含對一個空的JTree和一個空的JtextArea引用的搜索參數(shù)。JTree被用作創(chuàng)建一個搜索過程中的分類站點記錄。這樣為用戶提供了可見的反饋,幫助跟蹤Spdier循環(huán)搜索的位置。JtextArea顯示錯誤和過程信息。
構造器將參數(shù)存放在類變量中,使用UrlNodeRenderer類初始化顯示節(jié)點的JTree。直到SpiderControl調用run()方法搜索才開始。
run()方法以獨立的線程開始執(zhí)行。它首先判斷入口站點是否是一個Web引用(以http,ftp或者www開始)或是一個本地文件引用。它接著確認入口站點是否具有正確的符號,重置運行統(tǒng)計,接著調用searchWeb()開始搜索:
public void run()
{
DefaultTreeModel treeModel = (DefaultTreeModel)searchTree.getModel(); // get our model
DefaultMutableTreeNode root = (DefaultMutableTreeNode)treeModel.getRoot();
String urllc = startSite.toLowerCase();
if(!urllc.startsWith("http://") && !urllc.startsWith("ftp://") &&
!urllc.startsWith("www."))
{
startSite = "file:///"+startSite; // Note you must have 3 slashes !
}
else // Http missing ?
if(urllc.startsWith("www."))
{
startSite = "http://"+startSite; // Tack on http://
}
startSite = startSite.replace('""', '/'); // Fix bad slashes
sitesFound = 0;
sitesSearched = 0;
updateStats();
searchWeb(root,startSite); // Search the Web
messageArea.append("Done!"n"n");
}
searchWeb()是一個接受搜索樹父節(jié)點和搜索Web地址參數(shù)的遞歸方法。searchWeb()首先檢查給出的站點是否已被訪問和未被執(zhí)行的搜索深度和站點。SearchWeb()接著允許SpiderControl運行(更新界面和檢查停止搜索按鈕是否按下)。如果所有正常,searchWeb()繼續(xù),否則返回。
在searchWeb()開始讀和解析站點以前,它首先檢驗基于站點創(chuàng)建的URL對象是否具有正確的類型和主機。URL協(xié)議被檢查來確認它是一個HTML地址或者一個文件地址(不必搜索mailto:和其他協(xié)議)。接著檢查文件擴展名(如果當前有)來確認它是一個HTML文件(不必解析pdf或者gif文件)。一旦這些工作完成,通過isDomainOk()方法檢查根據(jù)用戶指定的列表檢查主機:
...URL url = new URL(urlstr); // Create the URL object from a string.
String protocol = url.getProtocol(); // Ask the URL for its protocol
if(!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("file"))
{
messageArea.append(" Skipping : "+urlstr+" not a http site"n"n");
return;
}
String path = url.getPath(); // Ask the URL for its path
int lastdot = path.lastIndexOf("."); // Check for file extension
if(lastdot > 0)
{
String extension = path.substring(lastdot); // Just the file extension
if(!extension.equalsIgnoreCase(".html") && !extension.equalsIgnoreCase(".htm"))
return; // Skip everything but html files
}
if(!isDomainOk(url))
{
messageArea.append(" Skipping : "+urlstr+" not in domain list"n"n");
return;
}
這里,searchWeb()公平的確定它是否有值得搜索的URL,接著它為搜索樹創(chuàng)建一個新節(jié)點,添加到樹中,打開一個輸入流解析文件。下面的章節(jié)涉及很多關于解析HTML文件,處理相關URL和控制遞歸的細節(jié)。
l 解析HTML文件
這里有兩個為了查找A HREF來解析HTML文件方法——一個麻煩的方法和一個簡單的方法。
如果你選擇麻煩的方法,你將使用Java的StreamTokenizer類創(chuàng)建你自己的解析規(guī)則。使用這些技術,你必須為StreamTokenizer對象指定單詞和空格,接著去掉<和>符號來查找標簽,屬性,在標簽之間分割文字。太多的工作要做。
簡單的方法是使用內置的ParserDelegator類,一個HTMLEditorKit.Parser抽象類的子類。這些類在Java文檔中沒有完善的文檔。使用ParserDelegator有三個步驟:首先為你的URL創(chuàng)建一個InputStreamReader對象,接著創(chuàng)建一個ParserCallback對象的實例,最后創(chuàng)建一個ParserDelegator對象的實例并調用它的public方法parse():
UrlTreeNode newnode = new UrlTreeNode(url); // Create the data node
InputStream in = url.openStream(); // Ask the URL object to create an input stream
InputStreamReader isr = new InputStreamReader(in); // Convert the stream to a reader
DefaultMutableTreeNode treenode = addNode(parentnode, newnode);
SpiderParserCallback cb = new SpiderParserCallback(treenode); // Create a callback object
ParserDelegator pd = new ParserDelegator(); // Create the delegator
pd.parse(isr,cb,true); // Parse the stream
isr.close(); // Close the stream
parse()接受一個InputStreamReader,一個ParseCallback對象實例和一個指定CharSet標簽是否忽略的標志。parse()方法接著讀和解碼HTML文件,每次完成解碼一個標簽或者HTML元素后調用ParserCallback對象的方法。
在示例代碼中,我實現(xiàn)了ParserCallback作為Spider的一個內部類,這樣就允許ParseCallback訪問Spider的方法和屬性。基于ParserCallback的類可以覆蓋下面的方法:
n handleStartTag():當遇到起始HTML標簽時調用,比如>A <
n handleEndTag():當遇到結束HTML標簽時調用,比如>/A<
n handleSimpleTag():當遇到?jīng)]有匹配結束標簽時調用
n handleText():當遇到標簽之間的文字時調用
在示例代碼中,我覆蓋了handleSimpleTag()以便我的代碼可以處理HTML的BASE和IMG標簽。BASE標簽告訴當處理相關的URL引用時使用什么URL。如果沒有BASE標簽出現(xiàn),那么當前URL就用來處理相關的引用。HandleSimpleTag()接受三個參數(shù),一個HTML.Tag對象,一個包含所有標簽屬性的MutableAttributeSet,和在文件中的相應位置。我的代碼檢查標簽來判斷它是否是一個BASE對象實例,如果是則HREF屬性被提取出來并保存在頁面的數(shù)據(jù)節(jié)點中。這個屬性以后在處理鏈接站點的URL地址中被用到。每次遇到IMG標簽,頁面圖片數(shù)就被更新。
我覆蓋了handleStartTag以便程序可以處理HTML的A和TITLE標簽。方法檢查t參數(shù)是否是一個事實上的A標簽,如果是則HREF屬性將被提取出來。
fixHref()被用作清理大量的引用(改變反斜線為斜線,添加缺少的結束斜線),鏈接的URL通過使用基礎URL和引用創(chuàng)建URL對象來處理。接著遞歸調用searchWeb()來處理鏈接。如果方法遇到TITLE標簽,它就清除存儲最后遇到文字的變量以便標題的結束標記具有正確的值(有時網(wǎng)頁的title標簽之間沒有標題)。
我覆蓋了handleEndTag()以便HTML的TITLE結束標記可以被處理。這個結束標記指出前面的文字(存在lastText中)是頁面的標題文字。這個文字接著存在頁面的數(shù)據(jù)節(jié)點中。因為添加標題信息到數(shù)據(jù)節(jié)點中將改變樹中數(shù)據(jù)節(jié)點的顯示,nodeChanged()方法必須被調用以便樹可以更新。
我覆蓋了handleText()方法以便HTML頁面的文字可以根據(jù)被搜索的任意關鍵字或者短語來檢查。HandleText()接受一個包含一個子符數(shù)組和該字符在文件中位置作為參數(shù)。HandleText()首先將字符數(shù)組轉換成一個String對象,在這個過程中全部轉換為大寫。接著在搜索列表中的每個關鍵字/短語根據(jù)String對象的indexof()方法來檢查。如果indexof()返回一個非負結果,則關鍵字/短語在頁面的文字中顯示。如果關鍵字/短語被顯示,匹配被記錄在匹配列表的節(jié)點中,統(tǒng)計數(shù)據(jù)被更新:
public class SpiderParserCallback extends HTMLEditorKit.ParserCallback {
/**
* Inner class used to html handle parser callbacks
*/
public class SpiderParserCallback extends HTMLEditorKit.ParserCallback {
/** URL node being parsed */
private UrlTreeNode node;
/** Tree node */
private DefaultMutableTreeNode treenode;
/** Contents of last text element */
private String lastText = "";
/**
* Creates a new instance of SpiderParserCallback
* @param atreenode search tree node that is being parsed
*/
public SpiderParserCallback(DefaultMutableTreeNode atreenode) {
treenode = atreenode;
node = (UrlTreeNode)treenode.getUserObject();
}
/**
* Handle HTML tags that don't have a start and end tag
* @param t HTML tag
* @param a HTML attributes
* @param pos Position within file
*/
public void handleSimpleTag(HTML.Tag t,
MutableAttributeSet a,
int pos)
{
if(t.equals(HTML.Tag.IMG))
{
node.addImages(1);
return;
}
if(t.equals(HTML.Tag.BASE))
{
Object value = a.getAttribute(HTML.Attribute.HREF);
if(value != null)
node.setBase(fixHref(value.toString()));
}
}
/**
* Take care of start tags
* @param t HTML tag
* @param a HTML attributes
* @param pos Position within file
*/
public void handleStartTag(HTML.Tag t,
MutableAttributeSet a,
int pos)
{
if(t.equals(HTML.Tag.TITLE))
{
lastText="";
return;
}
if(t.equals(HTML.Tag.A))
{
Object value = a.getAttribute(HTML.Attribute.HREF);
if(value != null)
{
node.addLinks(1);
String href = value.toString();
href = fixHref(href);
try{
URL referencedURL = new URL(node.getBase(),href);
searchWeb(treenode, referencedURL.getProtocol()+"://"+referencedURL.getHost()+referencedURL.getPath());
}
catch (MalformedURLException e)
{
messageArea.append(" Bad URL encountered : "+href+""n"n");
return;
}
}
}
}
/**
* Take care of start tags
* @param t HTML tag
* @param pos Position within file
*/
public void handleEndTag(HTML.Tag t,
int pos)
{
if(t.equals(HTML.Tag.TITLE) && lastText != null)
{
node.setTitle(lastText.trim());
DefaultTreeModel tm = (DefaultTreeModel)searchTree.getModel();
tm.nodeChanged(treenode);
}
}
/**
* Take care of text between tags, check against keyword list for matches, if
* match found, set the node match status to true
* @param data Text between tags
* @param pos position of text within Webpage
*/
public void handleText(char[] data, int pos)
{
lastText = new String(data);
node.addChars(lastText.length());
String text = lastText.toUpperCase();
for(int i = 0; i < keywordList.length; i++)
{
if(text.indexOf(keywordList[i]) >= 0)
{
if(!node.isMatch())
{
sitesFound++;
updateStats();
}
node.setMatch(keywordList[i]);
return;
}
}
}
}
l 處理和補全URL
當遇到相關頁面的鏈接,你必須在它們基礎URL上創(chuàng)建完整的鏈接。基礎URL可能通過BASE標簽在頁面中明確的定義,或者暗含在當前頁面的鏈接中。Java的URL對象為你解決這個問題提供了構造器,提供了根據(jù)它的鏈接結構創(chuàng)建相似的。
URL(URL context, String spec)接受spec參數(shù)的鏈接和context參數(shù)的基礎鏈接。如果spec是一個相關鏈接,構建器將使用context來創(chuàng)建一個完整引用的URL對象。URL它推薦URL遵循嚴格的(Unix)格式。使用反斜線,在Microsoft Windows中,而不是斜線,將是錯誤的引用。如果spec或者context指向一個目錄(包含index.html或default.html),而不是一個HTML文件,它必須有一個結束斜線。fixHref()方法檢查這些引用并且修正它們:
public static String fixHref(String href)
{
String newhref = href.replace('""', '/'); // Fix sloppy Web references
int lastdot = newhref.lastIndexOf('.');
int lastslash = newhref.lastIndexOf('/');
if(lastslash > lastdot)
{
if(newhref.charAt(newhref.length()-1) != '/')
newhref = newhref+"/"; // Add missing /
}
return newhref;
}
l 控制遞歸
searchWeb()開始是為了搜索用戶指定的起始Web地址而被調用的。它接著在遇到HTML鏈接時調用自身。這形成了深度優(yōu)先搜索的基礎,也帶來了兩種問題。首先非常危險的內存/堆棧溢出問題將因為太多的遞歸調用而產(chǎn)生。如果出現(xiàn)環(huán)形的引用,這個問題就將發(fā)生,也就是說,一個頁面鏈接另外一個鏈接回來的連接,這是WWW中常見的事情。為了預防這種現(xiàn)象,searchWeb()檢查搜索樹(通過urlHasBeenVisited()方法)來確定是否引用的頁面已經(jīng)存在。如果已經(jīng)存在,這個鏈接將被忽略。如果你選擇實現(xiàn)一個沒有搜索樹的蜘蛛,你仍然必須維護一個以訪問站點的列表(在Vector或數(shù)組中)以便你可以判斷是否你正在重復訪問站點。
遞歸的第二個問題來自深度優(yōu)先的搜索和WWW的結構。根據(jù)選擇的入口,深度優(yōu)先的搜索在初始頁面的初始鏈接在完成處理以前造成大量的遞歸調用。這就造成了兩種不需要的結果:首先內存/堆棧溢出可能發(fā)生,第二被搜索過的頁面可能很久才被從初始入口眾多的結果中刪除。為了控制這些,我為蜘蛛添加了最大搜索深度設置。用戶可以選擇可以達到的深度等級(鏈接到鏈接到鏈接),當遇到每個鏈接時,當前深度通過調用depthLimitExceeded()方法進行檢查。如果達到限制,鏈接就被忽略。測試僅僅檢查JTree中節(jié)點的級別。
示例程序也增加了站點限制,用戶來指定,可以在特定數(shù)目的URL被檢查以后停止搜索,這樣確保程序可以最后停止!站點限制通過一個簡單的數(shù)字計數(shù)器sitesSearched來控制,這個數(shù)字每次調用searchWeb()后都被更新和檢查。
l UrlTreeNode和UrlNodeRenderer
UrlTreeNode和UrlNodeRenderer是用來在SpiderControl用戶界面中創(chuàng)建JTree中個性化的樹節(jié)點的類。UrlTreeNode包含每個搜索過的站點鐘的URL信息和統(tǒng)計數(shù)據(jù)。UrlTreeNode以作為用戶對象屬性的標準DefaultMutableTreeNode對象形式存儲在JTree中。數(shù)據(jù)包括節(jié)點中跟蹤關鍵字出現(xiàn)的能力,節(jié)點的URL,節(jié)點的基礎URL,鏈接的數(shù)量,圖片的數(shù)量和字符的個數(shù),以及節(jié)點是否符合搜索規(guī)則。
UrlTreeNodeRenderer是DefaultTreeCellRenderer界面的實現(xiàn)。UrlTreeNodeRenderer使節(jié)點包含匹配關鍵字顯示為藍色。UrlTreeNodeRenderer也為JtreeNodes加入了個性化的圖標。個性化的顯示通過覆蓋getTreeCellRendererComponent()方法(如下)實現(xiàn)。這個方法在樹中創(chuàng)建了一個Component對象。大部分的Component屬性通過子類來進行設置,UrlTreeNodeRenderer改變了文字的顏色(前景色)和圖標:
public Component getTreeCellRendererComponent(
JTree tree,
Object value,
boolean sel,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
super.getTreeCellRendererComponent(
tree, value, sel,
expanded, leaf, row,
hasFocus);
UrlTreeNode node = (UrlTreeNode)(((DefaultMutableTreeNode)value).getUserObject());
if (node.isMatch()) // Set color
setForeground(Color.blue);
else
setForeground(Color.black);
if(icon != null) // Set a custom icon
{
setOpenIcon(icon);
setClosedIcon(icon);
setLeafIcon(icon);
}
return this;
}
l 總結
這篇文章向你展示了如何創(chuàng)建網(wǎng)絡蜘蛛和控制它的用戶界面。用戶界面使用JTree來跟蹤蜘蛛的進展和記錄訪問過的站點。當然,你也可以使用Vector來記錄訪問過的站點和使用一個簡單的計數(shù)器來顯示進展。其他增強可以包含通過數(shù)據(jù)庫記錄關鍵字和站點的接口,增加通過多個入口搜索的能力,用大量或者很少的文字內容來顯現(xiàn)站點,以及為搜索引擎提供同義搜索的能力。
這篇文章中展示的Spider類使用遞歸調用搜索程序,當然,一個新蜘蛛的獨立線程可以在遇到每個鏈接時開始。這樣的好處是允許鏈接遠程URL并發(fā)執(zhí)行,提高速度。然而記住那些叫做DefaultMutableTreeNode的JTree對象,不是線程安全的,所以程序員必須自己實現(xiàn)同步。
資源:
該文章的源代碼和Java文檔:
http://www.javaworld.com/javaworld/jw-11-2004/spider/jw-1101-spider.zip