學習腳步---- 網絡爬蟲(Spider)Java實現原理(轉載)
“網絡蜘蛛”或者說“網絡爬蟲”,是一種能訪問網站並跟蹤鏈接的程序,通過它,可快速地畫出一個網站所包含的網頁地圖信息。本文主要講述如何使用Java編程來構建一個“蜘蛛”,我們會先以一個可複用的蜘蛛類包裝一個基本的“蜘蛛”,並在示例程序中演示如何創建一個特定的“蜘蛛”來掃描相關網站並找出死鏈接。
* Java語言在此非常適合構建一個“蜘蛛”程序,其內建了對HTTP協議的支持,通過它可以傳輸大部分的網頁信息;其還內建了一個HTML解析器,正是這兩個原因使Java語言成爲本文構建“蜘蛛”程序的首選。
文章後面
例1的示例程序,將會掃描一個網站,並尋找死鏈接。
*如何使用?
使用這個程序時需先輸入一個URL並單擊“Begin”按鈕,程序開始之後,“Begin”按鈕會變成“Cancel”按鈕。在程序掃描網站期間,會在“Cancel”按鈕之下顯示進度,且在檢查當前網頁時,也會顯示相關正常鏈接與死鏈接的數目,死鏈接將顯示在程序底部的滾動文本框中。單擊“Cancel”按鈕會停止掃描過程,之後可以輸入一個新的URL;如果期間沒有單擊“Cancel”,程序將會一直運行直到查找完所有網頁,此後,“Cancel”按鈕會再次變回“Begin”,表示程序已停止。
下面將演示示例程序是如何與可複用“Spider”類交互的,示例程序包含在例1的CheckLinks類中,這個類實現了ISpiderReportable接口,如例2所示,正是通過這個接口,蜘蛛類才能與示例程序相交互。在這個接口中,定義了三個方法:
第一個方法是“spiderFoundURL”,它在每次程序定位一個URL時被調用,如果方法返回true,表示程序應繼續執行下去並找出其中的鏈接;
第二個方法是“spiderURLError”,它在每次程序檢測URL導致錯誤時被調用(如“404 頁面未找到”);
第三個方法是“spiderFoundEMail”,它在每次發現電子郵件地址時被調用。有了這三個方法,Spider類就能把相關信息反饋給創建它的程序了。
在begin方法被調用後,“蜘蛛”就開始工作了;爲允許程序重繪其用戶界面,“蜘蛛”是作爲一個單獨的線程啓動的。點擊“Begin”按鈕會開始這個後臺線程,當後臺線程運行之後,又會調用“CheckLinks”類的run方法,而run方法是由Spider對象實例化時啓動的,如下所示:
spider = new Spider(this);
spider.clear();
base = new URL(url.getText());
spider.addURL(base);
spider.begin();
首先,一個新的Spider對象被實例化,在此,需要傳遞一個“ISpiderReportable”對象給Spider對象的構造函數,因爲“CheckLinks”類實現了“ISpiderReportable”接口,只需簡單地把它作爲當前對象(可由關鍵字this表示)傳遞給構造函數即可;其次,在程序中維護了一個其訪問過的URL列表,而“clear”方法的調用則是爲了確保程序開始時URL列表爲空,程序開始運行之前必須添加一個URL到它的待處理列表中,此時用戶輸入的URL則是添加到列表中的第一個,程序就由掃描這個網頁開始,並找到與這個起始URL相鏈接的其他頁面;最後,調用“begin”方法開始運行“蜘蛛”,這個方法直到“蜘蛛”工作完畢或用戶取消纔會返回。
當“蜘蛛”運行時,可以調用由“ISpiderReportable”接口實現的三個方法來報告程序當前狀態,程序的大部分工作都是由“spiderFoundURL”方法來完成的,當“蜘蛛”發現一個新的URL時,它首先檢查其是否有效,如果這個URL導致一個錯誤,就會把它當作一個死鏈接;如果鏈接有效,就會繼續檢查它是否在一個不同的服務器上,如果鏈接在同一服務器上,“spiderFoundURL”返回true,表示“蜘蛛”應繼續跟蹤這個URL並找出其他鏈接,如果鏈接在另外的服務器上,就不會掃描是否還有其他鏈接,因爲這會導致“蜘蛛”不斷地瀏覽Internet,尋找更多、更多的網站,所以,示例程序只會查找用戶指定網站上的鏈接。
構造Spider類
前面已經講了如何使用Spider類,請看例3中的代碼。使用Spider類及“ISpiderReportable”接口能方便地爲某一程序添加“蜘蛛”功能,下面繼續講解Spider類是怎樣工作的。
Spider類必須保持對其訪問過的URL的跟蹤,這樣做的目的是爲了確保“蜘蛛”不會訪問同一URL一次以上;進一步來說,“蜘蛛”必須把URL分成三組:
第一組存儲在“workloadWaiting”屬性中,包含了一個未處理的URL列表,“蜘蛛”要訪問的第一個URL也存在其中;
第二組存儲在“workloadProcessed”中,它是“蜘蛛”已經處理過且無需再次訪問的URL;
第三組存儲在“workloadError”中,包含了發生錯誤的URL。
Begin方法包含了Spider類的主循環,其一直重複遍歷“workloadWaiting”,並處理其中的每一個頁面,當然我們也想到了,在這些頁面被處理時,很可能有其他的URL添加到“workloadWaiting”中,所以,begin方法一直繼續此過程,直到調用Spider類的cancel方法,或“workloadWaiting”中已不再剩有URL。這個過程如下:
cancel = false;
while ( !getWorkloadWaiting().isEmpty() && !cancel ) {
Object list[] = getWorkloadWaiting().toArray();
for ( int i=0; (i
processURL((URL)list[i]);
}
當上述代碼遍歷“workloadWaiting”時,它把每個需處理的URL都傳遞“processURL”方法,而這個方法纔是真正讀取並解析URL中HTML信息的。
讀取並解析HTML
Java同時支持訪問URL內容及解析HTML,而這正是“processURL”方法要做的。在Java中讀取URL內容相對還比較簡單,下面就是“processURL”方法實現此功能的代碼:
URLConnection connection = url.openConnection();
if ( (connection.getContentType()!=null) &&!connection.getContentType().toLowerCase().startsWith("text/") ) {
getWorkloadWaiting().remove(url);
getWorkloadProcessed().add(url);
log("Not processing because content type is: " +
connection.getContentType() );
return;
}
首先,爲每個傳遞進來的變量url中存儲的URL構造一個“URLConnection”對象,因爲網站上會有多種類型的文檔,而“蜘蛛”只對那些包含HTML,尤其是基於文本的文檔感興趣。前述代碼是爲了確保文檔內容以“text/”打頭,如果文檔類型爲非文本,會從等待區移除此URL,並把它添加到已處理區,這也是爲了保證不會再次訪問此URL。在對特定URL建立連接之後,接下來就要解析其內容了。下面的代碼打開了URL連接,並讀取內容:
InputStream is = connection.getInputStream();
Reader r = new InputStreamReader(is);
現在,我們有了一個Reader對象,可以用它來讀取此URL的內容,對本文中的“蜘蛛”來說,只需簡單地把其內容傳遞給HTML解析器就可以了。本例中使用的HTML解析器爲Swing HTML解析器,其由Java內置,但由於Java對HTML解析的支持力度不夠,所以必須重載一個類來實現對HTML解析器的訪問,這就是爲什麼我們要調用“HTMLEditorKit”類中的“getParser”方法。但不幸的是,Sun公司把這個方法置爲protected,唯一的解決辦法就是創建自己的類並重載“getParser”方法,並把它置爲public,這由“HTMLParse”類來實現,請看例4:
import javax.swing.text.html.*;
public class HTMLParse extends HTMLEditorKit {
public HTMLEditorKit.Parser getParser()
{
return super.getParser();
}
}
這個類用在Spider類的“processURL”方法中,我們也會看到,Reader對象會用於讀取傳遞到“HTMLEditorKit.Parser”中網頁的內容:
HTMLEditorKit.Parser parse = new HTMLParse().getParser();
parse.parse(r,new Parser(url),true);
請留意,這裏又構造了一個新的Parser類,這個Parser類是一個Spider類中的內嵌類,而且還是一個回調類,它包含了對應於每種HTML tag將要調用的特定方法。在本文中,我們只需關心兩類回調函數,它們分別對應一個簡單tag(即不帶結束tag的tag,如
)和一個開始tag,這兩類回調函數名爲“handleSimpleTag”和“handleStartTag”。因爲每種的處理過程都是一樣的,所以“handleStartTag”方法僅是簡單地調用“handleSimpleTag”,而“handleSimpleTag”則會負責從文檔中取出超鏈接,這些超鏈接將會用於定位“蜘蛛”要訪問的其他頁面。在當前tag被解析時,“handleSimpleTag”會檢查是否存在一個“href”或超文本引用:
String href = (String)a.getAttribute(HTML.Attribute.HREF);
if( (href==null) && (t==HTML.Tag.FRAME) )
href = (String)a.getAttribute(HTML.Attribute.SRC);
if ( href==null )
return;
如果不存在“href”屬性,會繼續檢查當前tag是否爲一個Frame,Frame會使用一個“src”屬性指向其他頁面,一個典型的超鏈接通常爲以下形式:
上面鏈接中的“href”屬性指向其鏈接到的頁面,但是“linkedpage.html”不是一個地址,它只是指定了這個Web服務器上一個頁面上的某處,這稱爲相對URL,相對URL必須被解析爲絕對URL,而這由以下代碼完成:
URL url = new URL(base,str);
這又會構造一個URL,str爲相對URL,base爲這個URL上的頁面,這種形式的URL類構造函數可構造一個絕對URL。在URL變爲正確的絕對形式之後,通過檢查它是否在等待區,來確認此URL是否已經被處理過。如果此URL沒有被處理過,它會添加到等待區,之後,它會像其他URL一樣被處理。
相關的代碼如下所示:
1.CheckLinks.java
import java.awt.*;
import javax.swing.*;
import java.net.*;
import java.io.*;
public class CheckLinks extends javax.swing.JFrame implements
Runnable,ISpiderReportable {
public CheckLinks()
{
//{{INIT_CONTROLS
setTitle("Find Broken Links");
getContentPane().setLayout(null);
setSize(405,288);
setVisible(true);
label1.setText("Enter a URL:");
getContentPane().add(label1);
label1.setBounds(12,12,84,12);
begin.setText("Begin");
begin.setActionCommand("Begin");
getContentPane().add(begin);
begin.setBounds(12,36,84,24);
getContentPane().add(url);
url.setBounds(108,36,288,24);
errorScroll.setAutoscrolls(true);
errorScroll.setHorizontalScrollBarPolicy(javax.swing.
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
errorScroll.setVerticalScrollBarPolicy(javax.swing.
ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
errorScroll.setOpaque(true);
getContentPane().add(errorScroll);
errorScroll.setBounds(12,120,384,156);
errors.setEditable(false);
errorScroll.getViewport().add(errors);
errors.setBounds(0,0,366,138);
current.setText("Currently Processing: ");
getContentPane().add(current);
current.setBounds(12,72,384,12);
goodLinksLabel.setText("Good Links: 0");
getContentPane().add(goodLinksLabel);
goodLinksLabel.setBounds(12,96,192,12);
badLinksLabel.setText("Bad Links: 0");
getContentPane().add(badLinksLabel);
badLinksLabel.setBounds(216,96,96,12);
//}}
//{{INIT_MENUS
//}}
//{{REGISTER_LISTENERS
SymAction lSymAction = new SymAction();
begin.addActionListener(lSymAction);
//}}
}
static public void main(String args[])
{
(new CheckLinks()).setVisible(true);
}
public void addNotify()
{
// Record the size of the window prior to calling parent's
// addNotify.
Dimension size = getSize();
super.addNotify();
if ( frameSizeAdjusted )
return;
frameSizeAdjusted = true;
// Adjust size of frame according to the insets and menu bar
Insets insets = getInsets();
javax.swing.JMenuBar menuBar = getRootPane().getJMenuBar();
int menuBarHeight = 0;
if ( menuBar != null )
menuBarHeight = menuBar.getPreferredSize().height;
setSize(insets.left + insets.right + size.width, insets.top +
insets.bottom + size.height +
menuBarHeight);
}
// Used by addNotify
boolean frameSizeAdjusted = false;
//{{DECLARE_CONTROLS
javax.swing.JLabel label1 = new javax.swing.JLabel();
javax.swing.JButton begin = new javax.swing.JButton();
javax.swing.JTextField url = new javax.swing.JTextField();
javax.swing.JScrollPane errorScroll =
new javax.swing.JScrollPane();
javax.swing.JTextArea errors = new javax.swing.JTextArea();
javax.swing.JLabel current = new javax.swing.JLabel();
javax.swing.JLabel goodLinksLabel = new javax.swing.JLabel();
javax.swing.JLabel badLinksLabel = new javax.swing.JLabel();
//}}
//{{DECLARE_MENUS
//}}
protected Thread backgroundThread;
protected Spider spider;
protected URL base;
protected int badLinksCount = 0;
protected int goodLinksCount = 0;
class SymAction implements java.awt.event.ActionListener {
public void actionPerformed(java.awt.event.ActionEvent event)
{
Object object = event.getSource();
if ( object == begin )
begin_actionPerformed(event);
}
}
void begin_actionPerformed(java.awt.event.ActionEvent event)
{
if ( backgroundThread==null ) {
begin.setLabel("Cancel");
backgroundThread = new Thread(this);
backgroundThread.start();
goodLinksCount=0;
badLinksCount=0;
} else {
spider.cancel();
}
}
public void run()
{
try {
errors.setText("");
spider = new Spider(this);
spider.clear();
base = new URL(url.getText());
spider.addURL(base);
spider.begin();
Runnable doLater = new Runnable()
{
public void run()
{
begin.setText("Begin");
}
};
SwingUtilities.invokeLater(doLater);
backgroundThread=null;
} catch ( MalformedURLException e ) {
UpdateErrors err = new UpdateErrors();
err.msg = "Bad address.";
SwingUtilities.invokeLater(err);
}
}
public boolean spiderFoundURL(URL base,URL url)
{
UpdateCurrentStats cs = new UpdateCurrentStats();
cs.msg = url.toString();
SwingUtilities.invokeLater(cs);
if ( !checkLink(url) ) {
UpdateErrors err = new UpdateErrors();
err.msg = url+"(on page " + base + ")\n";
SwingUtilities.invokeLater(err);
badLinksCount++;
return false;
}
goodLinksCount++;
if ( !url.getHost().equalsIgnoreCase(base.getHost()) )
return false;
else
return true;
}
public void spiderURLError(URL url)
{
}
protected boolean checkLink(URL url)
{
try {
URLConnection connection = url.openConnection();
connection.connect();
return true;
} catch ( IOException e ) {
return false;
}
}
public void spiderFoundEMail(String email)
{
}
class UpdateErrors implements Runnable {
public String msg;
public void run()
{
errors.append(msg);
}
}
class UpdateCurrentStats implements Runnable {
public String msg;
public void run()
{
current.setText("Currently Processing: " + msg );
goodLinksLabel.setText("Good Links: " + goodLinksCount);
badLinksLabel.setText("Bad Links: " + badLinksCount);
}
}
}
2.ISpiderReportable .java
import java.net.*;
interface ISpiderReportable {
public boolean spiderFoundURL(URL base,URL url);
public void spiderURLError(URL url);
public void spiderFoundEMail(String email);
}
3.Spider .java
import java.util.*;
import java.net.*;
import java.io.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
public class Spider {
protected Collection workloadError = new ArrayList(3);
protected Collection workloadWaiting = new ArrayList(3);
protected Collection workloadProcessed = new ArrayList(3);
protected ISpiderReportable report;
protected boolean cancel = false;
public Spider(ISpiderReportable report)
{
this.report = report;
}
public Collection getWorkloadError()
{
return workloadError;
}
public Collection getWorkloadWaiting()
{
return workloadWaiting;
}
public Collection getWorkloadProcessed()
{
return workloadProcessed;
}
public void clear()
{
getWorkloadError().clear();
getWorkloadWaiting().clear();
getWorkloadProcessed().clear();
}
public void cancel()
{
cancel = true;
}
public void addURL(URL url)
{
if ( getWorkloadWaiting().contains(url) )
return;
if ( getWorkloadError().contains(url) )
return;
if ( getWorkloadProcessed().contains(url) )
return;
log("Adding to workload: " + url );
getWorkloadWaiting().add(url);
}
public void processURL(URL url)
{
try {
log("Processing: " + url );
// get the URL's contents
URLConnection connection = url.openConnection();
if ( (connection.getContentType()!=null) &&
!connection.getContentType().toLowerCase().startsWith("text/") ) {
getWorkloadWaiting().remove(url);
getWorkloadProcessed().add(url);
log("Not processing because content type is: " +
connection.getContentType() );
return;
}
// read the URL
InputStream is = connection.getInputStream();
Reader r = new InputStreamReader(is);
// parse the URL
HTMLEditorKit.Parser parse = new HTMLParse().getParser();
parse.parse(r,new Parser(url),true);
} catch ( IOException e ) {
getWorkloadWaiting().remove(url);
getWorkloadError().add(url);
log("Error: " + url );
report.spiderURLError(url);
return;
}
// mark URL as complete
getWorkloadWaiting().remove(url);
getWorkloadProcessed().add(url);
log("Complete: " + url );
}
public void begin()
{
cancel = false;
while ( !getWorkloadWaiting().isEmpty() && !cancel ) {
Object list[] = getWorkloadWaiting().toArray();
for ( int i=0;(i<list.length)&&!cancel;i++ )
processURL((URL)list[i]);
}
}
protected class Parser
extends HTMLEditorKit.ParserCallback {
protected URL base;
public Parser(URL base)
{
this.base = base;
}
public void handleSimpleTag(HTML.Tag t,
MutableAttributeSet a,int pos)
{
String href = (String)a.getAttribute(HTML.Attribute.HREF);
if( (href==null) && (t==HTML.Tag.FRAME) )
href = (String)a.getAttribute(HTML.Attribute.SRC);
if ( href==null )
return;
int i = href.indexOf('#');
if ( i!=-1 )
href = href.substring(0,i);
if ( href.toLowerCase().startsWith("mailto:") ) {
report.spiderFoundEMail(href);
return;
}
handleLink(base,href);
}
public void handleStartTag(HTML.Tag t,
MutableAttributeSet a,int pos)
{
handleSimpleTag(t,a,pos); // handle the same way
}
protected void handleLink(URL base,String str)
{
try {
URL url = new URL(base,str);
if ( report.spiderFoundURL(base,url) )
addURL(url);
} catch ( MalformedURLException e ) {
log("Found malformed URL: " + str );
}
}
}
public void log(String entry)
{
System.out.println( (new Date()) + ":" + entry );
}
}
4.HTMLParse .java
import javax.swing.text.html.*;
public class HTMLParse extends HTMLEditorKit {
public HTMLEditorKit.Parser getParser()
{
return super.getParser();
}
}
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/wuhailin2005/archive/2009/01/08/3736026.aspx