CSDN爬蟲(二)——博客列表分頁爬蟲+數據表設計

CSDN爬蟲(二)——博客列表分頁爬蟲+數據庫設計

說明

  • 開發環境:jdk1.7+myeclipse10.7+win74bit+mysql5.5+webmagic0.5.2+jsoup1.7.2
  • 爬蟲框架:webMagic
  • 建議:建議首先閱讀webMagic的文檔,再查看此係列文章,便於理解,快速學習:http://webmagic.io/
  • 開發所需jar下載(不包括數據庫操作相關jar包):點我下載
  • 該系列文章會省略webMagic文檔已經講解過的相關知識。

博客列表爬蟲核心代碼預覽

    package com.wgyscsf.spider;

    import java.util.List;

    import us.codecraft.webmagic.Page;
    import us.codecraft.webmagic.Site;
    import us.codecraft.webmagic.Spider;
    import us.codecraft.webmagic.selector.Html;
    import us.codecraft.webmagic.selector.Selectable;

    import com.wgyscsf.utils.MyStringUtils;

    /**
     * @author wgyscsf</n> 編寫日期 2016-9-24下午7:25:36</n> 郵箱 [email protected]</n> 博客
     *         http://blog.csdn.net/wgyscsf</n> TODO</n>
     */
    public class CsdnBlogListSpider extends BaseSpider {
        private Site site = Site
                .me()
                .setDomain("blog.csdn.net")
                .setSleepTime(300)
                .setUserAgent(
                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31");

        @Override
        public void process(Page page) {
            // 列表頁: 這裏進行匹配,匹配出列表頁進行相關處理。在列表頁我們獲取必要信息。對於全文、評論、頂、踩在文章詳情中。
            if ((page.getUrl()).regex(
                    "^http://blog.csdn.net/\\w+/article/list/[0-9]*[1-9][0-9]*$")
                    .match()) {
                // 遍歷出頁碼:遍歷出div[@class=\"pagelist\"]節點下的所有超鏈接,該鏈接下是頁碼鏈接。將其加入到爬蟲隊列。【核心代碼】
                page.addTargetRequests(page
                        .getHtml()
                        .xpath("//div[@class=\"list_item_new\"]//div[@class=\"pagelist\"]")
                        .links().all());
                // 作者
                Selectable links = page.getHtml()
                        .xpath("//div[@class=\"header\"]//div[@id=\"blog_title\"]")
                        .links();
                String blogUrl = links.get();
                String id_author = MyStringUtils.getLastSlantContent(blogUrl);
                id_author = id_author != null ? id_author : "獲取作者id失敗";
                // System.out.println(TAG + author);
                // 獲取列表最外層節點的所有子節點。經過分析可以知道子節點有3個,“置頂文章”列表和“普通文章列表”和分頁div。
                List<String> out_div = page.getHtml()
                        .xpath("//div[@class=\"list_item_new\"]/div").all();

                // 判斷是否存在置頂文章:如何div的個數爲3說明存在置頂文章,否則不存在置頂文章。
                if (out_div.size() == 3) {
                    // 存在
                    processTopArticle(out_div.get(0), id_author);
                    processCommArticle(out_div.get(1), id_author);
                } else if (out_div.size() == 2) {
                    // 不存在
                    processCommArticle(out_div.get(0), id_author);
                } else {
                    System.err.println(TAG + ":邏輯出錯");
                }

            } else if (page.getUrl()
                    .regex("http://blog.csdn.net/\\w+/article/details/\\w+")
                    .match()) {
                // 這裏的邏輯還沒處理,主要是爲了獲取全文、標籤、頂、踩、評論等在列表頁獲取不到的數據
            }
        }

        /**
         * 處理普通文章列表
         */
        private void processCommArticle(String str, String id_author) {
            // 從列表頁獲取列表信息
            List<String> all;
            all = new Html(str).xpath("//div[@id=\"article_list\"]/div").all();
            if (!all.isEmpty())
                for (String string : all) {
                    // 這裏開始獲取具體內容

                    // 單項第一部分:article_title
                    // 文章地址
                    String detailsUrl = new Html(string)
                            .xpath("//div[@class='article_title']//span[@class='link_title']//a/@href")
                            .toString();
                    // 文章id
                    String id_blog = MyStringUtils.getLastSlantContent(detailsUrl);
                    // 文章標頭
                    String title = new Html(string)
                            .xpath("//div[@class='article_title']//span[@class='link_title']//a/text()")
                            .toString();
                    // 文章類型
                    String type = getArticleType(string);
                    // 單項第二部分:article_description
                    String summary = new Html(string).xpath(
                            "//div[@class='article_description']//text()")
                            .toString();
                    // 單項第三部分:article_manage
                    String publishDateTime = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_postdate']//text()")
                            .toString();
                    // 閱讀量
                    String viewNums = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_view']//text()")
                            .toString();
                    viewNums = MyStringUtils.getStringPureNumber(viewNums);
                    // 評論數
                    String commentNums = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_comments']//text()")
                            .toString();
                    commentNums = MyStringUtils.getStringPureNumber(commentNums);
                    // 開始組織數據
                    System.out.println(TAG + ":,文章id:" + id_blog + ",文章標頭:" + title
                            + ",文章類型('0':原創;'1':轉載;'2':翻譯):" + type + ",發表日期:"
                            + publishDateTime + ",閱讀量:" + viewNums + ",評論數:"
                            + commentNums + ",文章地址:" + detailsUrl + ",文章摘要:"
                            + summary + "");

                }

        }

        /**
         * 處理置頂文章列表
         */
        private void processTopArticle(String topListDiv, String id_author) {
            // 從列表頁獲取列表信息
            List<String> all;
            all = new Html(topListDiv).xpath("//div[@id=\"article_toplist\"]/div")
                    .all();
            if (!all.isEmpty())
                for (String string : all) {
                    // 單項第一部分:article_title
                    // 文章地址
                    String detailsUrl = new Html(string)
                            .xpath("//div[@class='article_title']//span[@class='link_title']//a/@href")
                            .toString();
                    // 文章id
                    String id_blog = MyStringUtils.getLastSlantContent(detailsUrl);
                    // 文章標頭
                    String title = new Html(string)
                            .xpath("//div[@class='article_title']//span[@class='link_title']//a/text()")
                            .toString();
                    // 文章類型
                    String type = getArticleType(string);
                    // 單項第二部分:article_description
                    String summary = new Html(string).xpath(
                            "//div[@class='article_description']//text()")
                            .toString();
                    // 單項第三部分:article_manage
                    String publishDateTime = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_postdate']//text()")
                            .toString();
                    // 閱讀量
                    String viewNums = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_view']//text()")
                            .toString();
                    viewNums = MyStringUtils.getStringPureNumber(viewNums);
                    // 評論數
                    String commentNums = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_comments']//text()")
                            .toString();
                    commentNums = MyStringUtils.getStringPureNumber(commentNums);

                    // 開始組織數據
                    System.out.println(TAG + ":,文章id:" + id_blog + ",文章標頭:" + title
                            + ",文章類型('0':原創;'1':轉載;'2':翻譯):" + type + ",發表日期:"
                            + publishDateTime + ",閱讀量:" + viewNums + ",評論數:"
                            + commentNums + ",文章地址:" + detailsUrl + ",文章摘要:"
                            + summary + "");

                }
        }

        /**
         * 獲取文章類型
         */
        private String getArticleType(String string) {
            String type;
            type = new Html(string)
                    .xpath("//div[@class='article_title']//span[@class='ico ico_type_Original']//text()")
                    .get();// 原創類型
            if (type != null)
                return 0 + "";
            type = new Html(string)
                    .xpath("//div[@class='article_title']//span[@class='ico ico_type_Repost']//text()")
                    .get();// 原創類型
            if (type != null)
                return 1 + "";
            type = new Html(string)
                    .xpath("//div[@class='article_title']//span[@class='ico ico_type_Translated']//text()")
                    .get();// 原創類型
            if (type != null)
                return 2 + "";
            return 3 + "";
        }

        @Override
        public Site getSite() {
            return site;
        }

        public static void main(String[] args) {
            Spider.create(new CsdnBlogListSpider())
                    .addPipeline(null)
                    .addUrl("http://blog.csdn.net/" + "wgyscsf" + "/"
                            + "article/list/1").run();
        }

    }

關鍵代碼解釋

  • page.getUrl()).regex("^http://blog.csdn.net/\\w+/article/list/[0-9]*[1-9][0-9]*$").match() ,每次正則表達式都是一個難點。這句話的意思是:http://blog.csdn.net/用戶id/article/list/頁碼,這個網址包含兩個可變字符串:用戶id和頁碼,這個正則主要是爲了匹配這個規則。如果是爬取單個用戶就不用這個麻煩。但是,後期如果我們有很多用戶id,這樣寫才能更加方便的去爬取任意用戶。
  • page.addTargetRequests(page.getHtml().xpath("//div[@class=\"list_item_new\"]//div[@class=\"pagelist\"]").links().all())這句話是整個博客列表爬取的核心。它負責找到個頁面下所有有效的列表鏈接,加入到爬蟲隊列,到爬蟲隊列,還會走上面所提到的正則進行匹配,加入到列表頁的解析,是一個迭代的過程。當然,如果想要更加嚴謹,也可以做一個正則的匹配,比如我只抓取div[@class=\"pagelist\"]下符合頁表頁規則的鏈接。當然,這裏是進行了分析,裏面只包含有效鏈接,就沒有再進行判斷。頁碼所在的div如下:

    <!--顯示分頁-->
    <div id="papelist" class="pagelist">
        <span> 49條  共4頁</span>
        <a href="/wgyscsf/article/list/1">首頁</a> 
        <a href="/wgyscsf/article/list/1">上一頁</a> 
        <a href="/wgyscsf/article/list/1">1</a> 
        <strong>2</strong> 
        <a href="/wgyscsf/article/list/3">3</a> 
        <a href="/wgyscsf/article/list/4">4</a> 
        <a href="/wgyscsf/article/list/3">下一頁</a> 
        <a href="/wgyscsf/article/list/4">尾頁</a> 
    </div>
    
  • page.getHtml().xpath("//div[@class=\"list_item_new\"]/div").all();這裏需要注意的是,需要判斷返回List的個數,進而判斷是否存在“置頂文章”。如果存在先處理“置頂文章”邏輯,再處理普通文章邏輯。經過分析可以知道子節點有3個,“置頂文章”列表div和“普通文章”列表div和分頁div。

  • getArticleType(String string)特別注意這個方法裏面關於文章類型的處理,如何判斷是何種類型的文章,可以嘗試獲取對應文章所處的div,如果返回不爲null,即說明存在,否則不存在。大致如下:

    String type;
    type = new Html(string).xpath("//div[@class='article_title']//span[@class='ico ico_type_Original']//text()").get();// 原創類型
    if (type != null)
        return 0 + "";// 說明是原創
    

數據庫設計

設計原則

  • 全部字段允許爲空,包括相關所屬唯一id。另外新建一個id,隨機生成UUID,作爲主鍵。原因:因爲爬蟲可能會出現爬不到的數據,或者“髒數據”,所以儘可能使數據庫不那麼“嚴謹”,保證程序能夠正常走下去。在表中新建一個id作爲主鍵,這個可以保證在獲取相應表id失敗的情況下,仍然可以正常運行。
  • 每個表的所屬id(不是新建的),比如作者id、文章id、個人id等可以標示一行數據的字段,全部加上【索引】。原因:後期需要保存大量用戶以及文章數據,保存之前我們需要拿獲取到表的id去查詢數據庫中是否存在,需要有一個查詢的過程。查詢是一個遍歷的過程,加與不加【索引】對程序影響巨大。簡單測試如下(不帶索引與帶索引的查詢時間,數據量:80W):
    這裏寫圖片描述
    這裏寫圖片描述
  • 儘可能加上所有直接相關的所有字段,不管現有爬蟲技術是否能實現。並且,再加上必要的備用字段。原因:數據表修改麻煩,儘可能後期不要修改。不能直接過去的數據,可能會在其它模塊獲取,比如“博客詳情”與“博客列表”之間的關係。
  • 不使用外鍵,原因:同第一條。
  • 主鍵不用自增的,而是採用手動生成。

表結構

這裏寫圖片描述

建表語句

  • 在操作代碼中附帶

操作代碼

點我下載

個人公衆號,及時更新技術文章(請移步公衆號,文章會被官方刪除)

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