《自己動手寫網絡爬蟲》筆記3-寬度優先遍歷互聯網

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


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開頭的網頁結果
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章