爬蟲入門——01


1. 引言

    從今天開始系統的學習網絡爬蟲。寫這篇博客的目的在於,一來記錄下自己的學習過程;二來希望可以給像我一樣不懂爬蟲但又對爬蟲十分感興趣的人帶來一些幫助。

    昨天去圖書館找有關爬蟲書籍,居然寥寥無幾,且都是泛泛而談。之后上某寶淘來淘去,只找到一本相關書籍《自己動手寫網絡爬蟲》,雖然在某瓣上看到此書的無數差評,但最終還是忍痛買下……

    對我而言,學習爬蟲不是學習如何使用API(學API看幫助文檔就ok了),而是學習爬蟲的算法和數據結構,即學習爬蟲的爬取策略,任務調度,數據挖掘,數據存儲以及整個系統的架構。因此我會花較多的篇幅去記錄以上提到的點,而不會去過多地介紹API如何調用。

    這篇文章作為自己第一篇學習爬蟲的博文,只想記錄一些最最基本的概念,並簡單實現一個最最基本的爬蟲:它能夠根據種子節點以特定的策略來爬取頁面,直到達到設定的條件,並將這些頁面保存在磁盤中。 我們使用Java作為編程語言。

 

 

2. 分析

 

 

(1) 算法分析

 

    我們現在從需求中提取關鍵詞來逐步分析問題。

    首先是“種子節點”。它就是一個或多個在爬蟲程序運行前手動給出的URL(網址),爬蟲正是下載並解析這些種子URL指向的頁面,從中提取出新的URL,然后重復以上的工作,直到達到設定的條件才停止。

    然后是“特定的策略”。這里所說的策略就是以怎樣的順序去請求這些URL。如下圖是一個簡單的頁面指向示意圖(實際情況遠比這個復雜),頁面A是種子節點,當然最先請求。但是剩下的頁面該以何種順序請求呢?我們可以采用深度優先遍歷策略,通俗講就是一條路走到底,走完一條路才再走另一條路,在下圖中就是按A,B,C,F,D,G,E,H的順序訪問。我們也可以采用寬度優先遍歷策略,就是按深度順序去遍歷,在下圖中就是按A,B,C,D,E,F,G,H的順序請求各頁面。還有許多其他的遍歷策略,如Google經典的PageRank策略,OPIC策略策略,大站優先策略等,這里不一一介紹了。我們還需要注意的一個問題是,很有可能某個頁面被多個頁面同時指向,這樣我們可能重復請求某一頁面,因此我們還必須過濾掉已經請求過的頁面。image

    最后是“設定的條件”,爬蟲程序終止的條件可以根據實際情況靈活設置,比如設定爬取時間,爬取數量,爬行深度等。

    到此,我們分析完了爬蟲如何開始,怎么運作,如何結束(當然,要實現一個強大,完備的爬蟲要考慮的遠比這些復雜,這里只是入門分析),下面給出整個運作的流程圖:

 image

(2) 數據結構分析

    根據以上的分析,我們需要用一種數據結構來保存初始的種子URL和解析下載的頁面得到的URL,並且我們希望先解析出的URL先執行請求,因此我們用隊列來儲存URL。因為我們要頻繁的添加,取出URL,因此我們采用鏈式存儲。下載的頁面解析后直接原封不動的保存到磁盤。

(3) 技術分析

    所謂網絡爬蟲,我們當然要訪問網絡,我們這里使用jsoup,它對http請求和html解析都做了良好的封裝,使用起來十分方便。根據數據結構分析,我們用LinkedList實現隊列,用來保存未訪問的URL,用HashSet來保存訪問過的URL(因為我們要大量的判斷該URL是否在該集合內,而HashSet用元素的Hash值作為“索引”,查找速度很快)。

 

3. 實現

 

 

(1) 代碼

 

    以上分析,我們一共要實現2個類:

    ① JsoupDownloader,該類是對Jsoup做一個簡單的封裝,方便調用。暴露出以下幾個方法:

—public Document downloadPage(String url);根據url下載頁面

—public Set<String> parsePage(Document doc, String regex);從Document中解析出匹配regex的url。

—public void savePage(Document doc, String saveDir, String saveName, String regex);保存匹配regex的url對應的Document到指定路徑。

 

    ② UrlQueue,該類用來保存和獲取URL。暴露出以下幾個方法:

—public void enQueue(String url);添加url。

—public String deQueue();取出url。

—public int getVisitedCount();獲取訪問過的url的數量;

下面給出具體代碼:

JsoupDownloader.java

package com.dk.spider.spider_01;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class JsoupDownloader {

    public static final String DEFAULT_SAVE_DIR = "c:/download/";
    private static JsoupDownloader downloader;

    private JsoupDownloader() {
    }

    public static JsoupDownloader getInstance() {
        if (downloader == null) {
            synchronized (JsoupDownloader.class) {
                if (downloader == null) {
                    downloader = new JsoupDownloader();
                }
            }
        }
        return downloader;
    }

    public Document downloadPage(String url) {
        try {
            System.out.println("正在下載" + url);
            return Jsoup.connect(url).get();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public Set<String> parsePage(Document doc, String regex) {
        Set<String> urlSet = new HashSet<>();
        if (doc != null) {
            Elements elements = doc.select("a[href]");
            for (Element element : elements) {
                String url = element.attr("href");
                if (url.length() > 6 && !urlSet.contains(url)) {
                    if (regex != null && !url.matches(regex)) {
                        continue;
                    }
                    urlSet.add(url);
                }
            }
        }
        return urlSet;
    }

    public void savePage(Document doc, String saveDir, String saveName, String regex) {
        if (doc == null) {
            return;
        }
        if (regex != null && doc.baseUri() != null && !doc.baseUri().matches(regex)) {
            return;
        }
        saveDir = saveDir == null ? DEFAULT_SAVE_DIR : saveDir;
        saveName = saveName == null ? doc.title().trim().replaceAll("[\\?/:\\*|<>\" ]", "_") + System.nanoTime() + ".html" : saveName;
        File file = new File(saveDir + "/" + saveName);
        File dir = file.getParentFile();
        if (!dir.exists()) {
            dir.mkdirs();
        }
        PrintWriter printWriter;
        try {
            printWriter = new PrintWriter(file);
            printWriter.write(doc.toString());
            printWriter.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

UrlQueue.java

package com.dk.spider.spider_01;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Set;

public class UrlQueue {

    private Set<String> visitedSet;// 用來存放已經訪問過多url
    private LinkedList<String> unvisitedList;// 用來存放未訪問過多url

    public UrlQueue(String[] seeds) {
        visitedSet = new HashSet<>();
        unvisitedList = new LinkedList<>();
        unvisitedList.addAll(Arrays.asList(seeds));
    }

    /**
     * 添加url
     * 
     * @param url
     */
    public void enQueue(String url) {
        if (url != null && !visitedSet.contains(url)) {
            unvisitedList.addLast(url);
        }
    }

    /**
     * 添加url
     * 
     * @param urls
     */
    public void enQueue(Collection<String> urls) {
        for (String url : urls) {
            enQueue(url);
        }
    }

    /**
     * 取出url
     * 
     * @return
     */
    public String deQueue() {
        try {
            String url = unvisitedList.removeFirst();
            while(visitedSet.contains(url)) {
                url = unvisitedList.removeFirst();
            }
            visitedSet.add(url);
            return url;
        } catch (NoSuchElementException e) {
            System.err.println("URL取光了");
        }
        return null;
    }

    /**
     * 得到已經請求過的url的數目
     * 
     * @return
     */
    public int getVisitedCount() {
        return visitedSet.size();
    }
}
(2) 測試

    下面進行測試,我們來抓取園子里排行No1的Artech的文章,以他的博客首頁地址:http://www.cnblogs.com/artech/作為種子節點。通過分析發現,形如:http://www.cnblogs.com/artech/p/…和http://www.cnblogs.com/artech/archive/2012/09/08/…的鏈接都是有效的文章地址,而形如:http://www.cnblogs.com/artech/default/…的鏈接是下一頁鏈接,這些都作為我們篩選url的依據。我們采用寬度優先遍歷策略。Artech的文章數是500余篇,因此我們以請求頁面數達到1000或遍歷完所有滿足條件的url為終止條件。下面是具體的測試代碼:

package com.dk.spider.spider_01;

import java.util.Set;

import org.jsoup.nodes.Document;

public class Main {

    public static void main(String[] args) {
        UrlQueue urlQueue = new UrlQueue(new String[] { "http://www.cnblogs.com/artech/" });
        JsoupDownloader downloader = JsoupDownloader.getInstance();
        long start = System.currentTimeMillis();
        while (urlQueue.getVisitedCount() < 1000) {
            String url = urlQueue.deQueue();
            if (url == null) {
                break;
            }
            Document doc = downloader.downloadPage(url);
            if (doc == null) {
                continue;
            }
            Set<String> urlSet = downloader.parsePage(doc, "(http://www.cnblogs.com/artech/p|http://www.cnblogs.com/artech/default|http://www.cnblogs.com/artech/archive/\\d{4}/\\d{2}/\\d{2}/).*");
            urlQueue.enQueue(urlSet);
            downloader.savePage(doc, "C:/Users/Administrator/Desktop/test/", null, "(http://www.cnblogs.com/artech/p|http://www.cnblogs.com/artech/archive/\\d{4}/\\d{2}/\\d{2}/).*");
            System.out.println("已請求" + urlQueue.getVisitedCount() + "個頁面");
        }
        long end = System.currentTimeMillis();
        System.out.println(">>>>>>>>>>抓去完成,共抓取" + urlQueue.getVisitedCount() + "到個頁面,用時" + ((end - start) / 1000) + "s<<<<<<<<<<<<");
    }
}

運行結果:

image

image

4. 總結

    仔細分析以上過程,還有許多值得優化改進的地方:

① 我們在請求頁面時,只是做了簡單的異常處理。好的做法是根據http響應的狀態碼來做不同的處理。如對於請求重定向的url我們重新定向;對於找不到資源的url直接丟棄;對於連接超時的url我們可以重新將其放入未訪問url隊列中…

② 我們的待訪問和已訪問url都是直接保存在內存中的。當url數量很多時,可能會發生內存溢出。因此需要將數據持久化到硬盤上,但是又要節約空間,能夠快速訪問數據。

③ UrlQueue的enqueue和dequeue方法實際上是有問題的,當解析url速度慢於下載頁面速度或其他原因引起的dequeue快於enqueue時,會導致程序提前終止。我們可以采用多線程,阻塞隊列(BlockingQueue)來解決這一問題。

④ 我們目前的爬蟲效率太低,僅爬取600個左右頁面就花費了1分多鍾。我們可以采用多線程,分布式爬取,來提高爬蟲效率。

⑤ 爬蟲的架構過於簡單,擴展性,靈活性不強。

    但不管怎樣,我們的實現基本滿足了文章開始提出的需求,以后會在此基礎上慢慢進行迭代。在下一篇中我們會引入多線程來提高爬蟲的效率;並采用Bloom Filter(布隆過濾器)來構建visited集合;引入Berkeley DB來進行url數據的持久化。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM