程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 用Java編程實現“網絡蜘蛛”

用Java編程實現“網絡蜘蛛”

編輯:關於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”屬性指向其他頁面,一個典型的超鏈接通常為以下形式:

Click Here

上面鏈接中的“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:查找死鏈接(ChcekLinks.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("找到死鏈接");
getContentPane().setLayout(null);
setSize(405,288);
setVisible(false);
label1.setText("輸入一個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("當前處理進度:");
getContentPane().add(current);
current.setBounds(12,72,384,12);
goodLinksLabel.setText("正常鏈接:0");
getContentPane().add(goodLinksLabel);
goodLinksLabel.setBounds(12,96,192,12);
badLinksLabel.setText("死鏈接:0");
getContentPane().add(badLinksLabel);
badLinksLabel.setBounds(216,96,96,12);
//}}
//{{INIT_MENUS
//}}
//{{REGISTER_LISTENERS
SymAction lSymAction = new SymAction();
begin.addActionListener(lSymAction);
//}}
}
/**
*參數args未使用
*/
static public void main(String args[])
{
(new CheckLinks()).setVisible(true);
}
/**
*添加通知
*/
public void addNotify()
{
//記錄窗口尺寸並調用父類的addNotify.
Dimension size = getSize();
super.addNotify();
if ( frameSizeAdjusted )
return;
frameSizeAdjusted = true;
//根據菜單欄等調整Frame尺寸
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);
}
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);
}
}
/**
*當begin或cancel按鈕被點擊時調用
*
*參數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 = "錯誤地址。";
SwingUtilities.invokeLater(err);
}
}
/**
*當找到某一URL時由蜘蛛調用,在此驗證鏈接。
*
*參數base是找到鏈接時的頁面
*參數url是鏈接地址
*/
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;
}
/**
*當發現URL錯誤時調用
*
*參數url是導致錯誤的URL
*/
public void spiderURLError(URL url)
{
}
/**
*由內部調用檢查鏈接是否有效
*
*參數url是被檢查的鏈接
*返回True表示鏈接正常有效
*/
protected boolean checkLink(URL url)
{
try {
URLConnection connection = url.openConnection();
connection.connect();
return true;
} catch ( IOException e ) {
return false;
}
}
/**
*當蜘蛛找到電子郵件地址時調用
*
*參數email為找到的電子郵件地址
*/
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("當前進度:" + msg );
goodLinksLabel.setText("正常鏈接:" + goodLinksCount);
badLinksLabel.setText("死鏈接:" + 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 {
/**
*導致錯誤的URL集合
*/
protected Collection workloadError = new ArrayList(3);
/**
*等待區URL集合
*/
protected Collection workloadWaiting = new ArrayList(3);
/**
*處理過的URL集合
*/
protected Collection workloadProcessed = new ArrayList(3);
protected ISpiderReportable report;
/**
*表明處理過程是否應取消的標志
*/
protected boolean cancel = false;
/**
*構造函數
*
*參數report為實現了ISpiderReportable接口的類
*/
public Spider(ISpiderReportable report)
{
this.report = report;
}
/**
*獲取導致錯誤的URL
*/
public Collection getWorkloadError()
{
return workloadError;
}
/**
*獲取在等待的URL
*應添加至少一個URL到此集合以啟動蜘蛛
*/
public Collection getWorkloadWaiting()
{
return workloadWaiting;
}
/**
*獲取被處理過的URL
*/
public Collection getWorkloadProcessed()
{
return workloadProcessed;
}
/**
*清空所有
*/
public void clear()
{
getWorkloadError().clear();
getWorkloadWaiting().clear();
getWorkloadProcessed().clear();
}
/**
*設置一標志,使begin方法在完成之前返回
*/
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("正添加到工作區:" + url );
getWorkloadWaiting().add(url);
}
public void processURL(URL url)
{
try {
log("正在處理:" + url );
//獲取URL的內容
URLConnection connection = url.openConnection();
if ( (connection.getContentType()!=null) &&
!connection.getContentType().toLowerCase().s
tartsWith("text/") ) {
getWorkloadWaiting().remove(url);
getWorkloadProcessed().add(url);
log("不會進行正理,因為類型為:" +
connection.getContentType() );
return;
}
//讀取URL
InputStream is = connection.getInputStream();
Reader r = new InputStreamReader(is);
//解析URL
HTMLEditorKit.Parser parse = new HTMLParse().getParser();
parse.parse(r,new Parser(url),true);
} catch ( IOException e ) {
getWorkloadWaiting().remove(url);
getWorkloadError().add(url);
log("錯誤:" + url );
report.spiderURLError(url);
return;
}
//標記此URL已完成
getWorkloadWaiting().remove(url);
getWorkloadProcessed().add(url);
log("已完成:" + url );
}
public void begin()
{
cancel = false;
while ( !getWorkloadWaiting().isEmpty() && !cancel ) {
Object list[] = getWorkloadWaiting().toArray();
for ( int i=0; (i
processURL((URL)list[i]);
}
}
/**
*HTML解析器回調函數
*/
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); //以同樣的方式處理
}
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("找到畸形URL:" + str );
}
}
}
/**
*由內部調用來記錄信息
*僅是把日志寫到標准輸出
*
*參數entry為寫到日志的信息
*/
public void log(String entry)
{
System.out.println( (new Date()) + ":" + entry );
}
}

例4:解析HTML(HTMLParse.java)

import javax.swing.text.html.*;
public class HTMLParse extends HTMLEditorKit {
public HTMLEditorKit.Parser getParser()
{
return super.getParser();
}
}

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved