1. 引言html
從今天開始系統的學習網絡爬蟲。寫這篇博客的目的在於,一來記錄下本身的學習過程;二來但願能夠給像我同樣不懂爬蟲但又對爬蟲十分感興趣的人帶來一些幫助。java
昨天去圖書館找有關爬蟲書籍,竟然寥寥無幾,且都是泛泛而談。以後上某寶淘來淘去,只找到一本相關書籍《本身動手寫網絡爬蟲》,雖然在某瓣上看到此書的無數差評,但最終仍是忍痛買下……node
對我而言,學習爬蟲不是學習如何使用API(學API看幫助文檔就ok了),而是學習爬蟲的算法和數據結構,即學習爬蟲的爬取策略,任務調度,數據挖掘,數據存儲以及整個系統的架構。所以我會花較多的篇幅去記錄以上提到的點,而不會去過多地介紹API如何調用。算法
這篇文章做爲本身第一篇學習爬蟲的博文,只想記錄一些最最基本的概念,並簡單實現一個最最基本的爬蟲:它可以根據種子節點以特定的策略來爬取頁面,直到達到設定的條件,並將這些頁面保存在磁盤中。 咱們使用Java做爲編程語言。編程
2. 分析網絡
咱們如今從需求中提取關鍵詞來逐步分析問題。數據結構
首先是「種子節點」。它就是一個或多個在爬蟲程序運行前手動給出的URL(網址),爬蟲正是下載並解析這些種子URL指向的頁面,從中提取出新的URL,而後重複以上的工做,直到達到設定的條件才中止。多線程
而後是「特定的策略」。這裏所說的策略就是以怎樣的順序去請求這些URL。以下圖是一個簡單的頁面指向示意圖(實際狀況遠比這個複雜),頁面A是種子節點,固然最早請求。可是剩下的頁面該以何種順序請求呢?咱們能夠採用深度優先遍歷策略,通俗講就是一條路走到底,走完一條路纔再走另外一條路,在下圖中就是按A,B,C,F,D,G,E,H的順序訪問。咱們也能夠採用寬度優先遍歷策略,就是按深度順序去遍歷,在下圖中就是按A,B,C,D,E,F,G,H的順序請求各頁面。還有許多其餘的遍歷策略,如Google經典的PageRank策略,OPIC策略策略,大站優先策略等,這裏不一一介紹了。咱們還須要注意的一個問題是,頗有可能某個頁面被多個頁面同時指向,這樣咱們可能重複請求某一頁面,所以咱們還必須過濾掉已經請求過的頁面。架構
最後是「設定的條件」,爬蟲程序終止的條件能夠根據實際狀況靈活設置,好比設定爬取時間,爬取數量,爬行深度等。編程語言
到此,咱們分析完了爬蟲如何開始,怎麼運做,如何結束(固然,要實現一個強大,完備的爬蟲要考慮的遠比這些複雜,這裏只是入門分析),下面給出整個運做的流程圖:
根據以上的分析,咱們須要用一種數據結構來保存初始的種子URL和解析下載的頁面獲得的URL,而且咱們但願先解析出的URL先執行請求,所以咱們用隊列來儲存URL。由於咱們要頻繁的添加,取出URL,所以咱們採用鏈式存儲。下載的頁面解析後直接原封不動的保存到磁盤。
所謂網絡爬蟲,咱們固然要訪問網絡,咱們這裏使用jsoup,它對http請求和html解析都作了良好的封裝,使用起來十分方便。根據數據結構分析,咱們用LinkedList實現隊列,用來保存未訪問的URL,用HashSet來保存訪問過的URL(由於咱們要大量的判斷該URL是否在該集合內,而HashSet用元素的Hash值做爲「索引」,查找速度很快)。
3. 實現
以上分析,咱們一共要實現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(); } }
下面進行測試,咱們來抓取園子裏排行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<<<<<<<<<<<<"); } }
運行結果:
4. 總結
仔細分析以上過程,還有許多值得優化改進的地方:
① 咱們在請求頁面時,只是作了簡單的異常處理。好的作法是根據http響應的狀態碼來作不一樣的處理。如對於請求重定向的url咱們從新定向;對於找不到資源的url直接丟棄;對於鏈接超時的url咱們能夠從新將其放入未訪問url隊列中…
② 咱們的待訪問和已訪問url都是直接保存在內存中的。當url數量不少時,可能會發生內存溢出。所以須要將數據持久化到硬盤上,可是又要節約空間,可以快速訪問數據。
③ UrlQueue的enqueue和dequeue方法其實是有問題的,當解析url速度慢於下載頁面速度或其餘緣由引發的dequeue快於enqueue時,會致使程序提早終止。咱們能夠採用多線程,阻塞隊列(BlockingQueue)來解決這一問題。
④ 咱們目前的爬蟲效率過低,僅爬取600個左右頁面就花費了1分多鐘。咱們能夠採用多線程,分佈式爬取,來提升爬蟲效率。
⑤ 爬蟲的架構過於簡單,擴展性,靈活性不強。
但無論怎樣,咱們的實現基本知足了文章開始提出的需求,之後會在此基礎上慢慢進行迭代。在下一篇中咱們會引入多線程來提升爬蟲的效率;並採用Bloom Filter(布隆過濾器)來構建visited集合;引入Berkeley DB來進行url數據的持久化。