国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

NodeJs爬蟲抓取古代典籍,共計16000個頁面心得體會總結及項目分享

legendmohe / 1467人閱讀

摘要:目前這個爬蟲還是比較簡單的類型的,直接抓取頁面,然后在頁面中提取數據,保存數據到數據庫。總結寫這個項目其實主要的難點在于程序穩定性的控制,容錯機制的設置,以及錯誤的記錄,目前這個項目基本能夠實現直接運行一次性跑通整個流程。

前言

之前研究數據,零零散散的寫過一些數據抓取的爬蟲,不過寫的比較隨意。有很多地方現在看起來并不是很合理 這段時間比較閑,本來是想給之前的項目做重構的。
后來 利用這個周末,索性重新寫了一個項目,就是本項目 guwen-spider。目前這個爬蟲還是比較簡單的類型的, 直接抓取頁面,然后在頁面中提取數據,保存數據到數據庫。
通過與之前寫的對比,我覺得難點在于整個程序的健壯性,以及相應的容錯機制。在昨天寫代碼的過程中其實也有反映, 真正的主體代碼其實很快就寫完了 ,花了大部分時間是在
做穩定性的調試, 以及尋求一種更合理的方式來處理數據與流程控制的關系。

背景

項目的背景是抓取一個一級頁面是目錄列表 ,點擊一個目錄進去 是一個章節 及篇幅列表 ,點擊章節或篇幅進入具體的內容頁面。

概述

本項目github地址 : guwen-spider (PS:最后面還有彩蛋 ~~逃

項目技術細節
項目大量用到了 ES7 的async 函數, 更直觀的反應程序了的流程。為了方便,在對數據遍歷的過程中直接使用了著名的async這個庫,所以不可避免的還是用到了回調promise ,因為數據的處理發生在回調函數中,不可避免的會遇到一些數據傳遞的問題,其實也可以直接用ES7的async await 寫一個方法來實現相同的功能。這里其實最贊的一個地方是使用了 Class 的 static 方法封裝對數據庫的操作, static 顧名思義 靜態方法 就跟 prototype 一樣 ,不會占用額外空間。
項目主要用到了

1 ES7的 async await 協程做異步有關的邏輯處理。

2 使用 npm的 async庫 來做循環遍歷,以及并發請求操作。

3 使用 log4js 來做日志處理

4 使用 cheerio 來處理dom的操作。

5 使用 mongoose 來連接mongoDB 做數據的保存以及操作。

目錄結構


├── bin // 入口
│? ├── booklist.js // 抓取書籍邏輯
│? ├── chapterlist.js // 抓取章節邏輯
│? ├── content.js // 抓取內容邏輯
│? └── index.js // 程序入口
├── config // 配置文件
├── dbhelper // 數據庫操作方法目錄
├── logs // 項目日志目錄
├── model // mongoDB 集合操作實例
├── node_modules
├── utils // 工具函數
├── package.json

項目實現方案分析

項目是一個典型的多級抓取案例,目前只有三級,即 書籍列表, 書籍項對應的 章節列表,一個章節鏈接對應的內容。 抓取這樣的結構可以采用兩種方式, 一是 直接從外層到內層 內層抓取完以后再執行下一個外層的抓取, 還有一種就是先把外層抓取完成保存到數據庫,然后根據外層抓取到所有內層章節的鏈接,再次保存,然后從數據庫查詢到對應的鏈接單元 對之進行內容抓取。這兩種方案各有利弊,其實兩種方式我都試過, 后者有一個好處,因為對三個層級是分開抓取的, 這樣就能夠更方便,盡可能多的保存到對應章節的相關數據。 可以試想一下 ,如果采用前者 按照正常的邏輯
對一級目錄進行遍歷抓取到對應的二級章節目錄, 再對章節列表進行遍歷 抓取內容,到第三級 內容單元抓取完成 需要保存時,如果需要很多的一級目錄信息,就需要 這些分層的數據之間進行數據傳遞 ,想想其實應該是比較復雜的一件事情。所以分開保存數據 一定程度上避開了不必要的復雜的數據傳遞。

目前我們考慮到 其實我們要抓取到的古文書籍數量并不多,古文書籍大概只有180本囊括了各種經史。其和章節內容本身是一個很小的數據 ,即一個集合里面有180個文檔記錄。 這180本書所有章節抓取下來一共有一萬六千個章節,對應需要訪問一萬六千個頁面爬取到對應的內容。所以選擇第二種應該是合理的。

項目實現

主程有三個方法 bookListInit ,chapterListInit,contentListInit, 分別是抓取書籍目錄,章節列表,書籍內容的方法對外公開暴露的初始化方法。通過async 可以實現對這三個方法的運行流程進行控制,書籍目錄抓取完成將數據保存到數據庫,然后執行結果返回到主程序,如果運行成功 主程序則執行根據書籍列表對章節列表的抓取,同理對書籍內容進行抓取。

項目主入口

/**
 * 爬蟲抓取主入口
 */
const start = async() => {
    let booklistRes = await bookListInit();
    if (!booklistRes) {
        logger.warn("書籍列表抓取出錯,程序終止...");
        return;
    }
    logger.info("書籍列表抓取成功,現在進行書籍章節抓取...");

    let chapterlistRes = await chapterListInit();
    if (!chapterlistRes) {
        logger.warn("書籍章節列表抓取出錯,程序終止...");
        return;
    }
    logger.info("書籍章節列表抓取成功,現在進行書籍內容抓取...");

    let contentListRes = await contentListInit();
    if (!contentListRes) {
        logger.warn("書籍章節內容抓取出錯,程序終止...");
        return;
    }
    logger.info("書籍內容抓取成功");
}
// 開始入口
if (typeof bookListInit === "function" && typeof chapterListInit === "function") {
    // 開始抓取
    start();
}

引入的 bookListInit ,chapterListInit,contentListInit, 三個方法

booklist.js

/**
 * 初始化方法 返回抓取結果 true 抓取成果 false 抓取失敗
 */
const bookListInit = async() => {
    logger.info("抓取書籍列表開始...");
    const pageUrlList = getPageUrlList(totalListPage, baseUrl);
    let res = await getBookList(pageUrlList);
    return res;
}

chapterlist.js

/**
 * 初始化入口
 */
const chapterListInit = async() => {
    const list = await bookHelper.getBookList(bookListModel);
    if (!list) {
        logger.error("初始化查詢書籍目錄失敗");
    }
    logger.info("開始抓取書籍章節列表,書籍目錄共:" + list.length + "條");
    let res = await asyncGetChapter(list);
    return res;
};

content.js

/**
 * 初始化入口
 */
const contentListInit = async() => {
    //獲取書籍列表
    const list = await bookHelper.getBookLi(bookListModel);
    if (!list) {
        logger.error("初始化查詢書籍目錄失敗");
        return;
    }
    const res = await mapBookList(list);
    if (!res) {
        logger.error("抓取章節信息,調用 getCurBookSectionList() 進行串行遍歷操作,執行完成回調出錯,錯誤信息已打印,請查看日志!");
        return;
    }
    return res;
}

內容抓取的思考

書籍目錄抓取其實邏輯非常簡單,只需要使用async.mapLimit做一個遍歷就可以保存數據了,但是我們在保存內容的時候 簡化的邏輯其實就是 遍歷章節列表 抓取鏈接里的內容。但是實際的情況是鏈接數量多達幾萬 我們從內存占用角度也不能全部保存到一個數組中,然后對其遍歷,所以我們需要對內容抓取進行單元化。
普遍的遍歷方式 是每次查詢一定的數量,來做抓取,這樣缺點是只是以一定數量做分類,數據之間沒有關聯,以批量方式進行插入,如果出錯 則容錯會有一些小問題,而且我們想一本書作為一個集合多帶帶保存會遇到問題。因此我們采用第二種就是以一個書籍單元進行內容抓取和保存。
這里使用了 async.mapLimit(list, 1, (series, callback) => {})這個方法來進行遍歷,不可避免的用到了回調,感覺很惡心。async.mapLimit()的第二個參數可以設置同時請求數量。

 /* 
 * 內容抓取步驟:
 * 第一步得到書籍列表, 通過書籍列表查到一條書籍記錄下 對應的所有章節列表, 
 * 第二步 對章節列表進行遍歷獲取內容保存到數據庫中 
 * 第三步 保存完數據后 回到第一步 進行下一步書籍的內容抓取和保存
 */

/**
 * 初始化入口
 */
const contentListInit = async() => {
    //獲取書籍列表
    const list = await bookHelper.getBookList(bookListModel);
    if (!list) {
        logger.error("初始化查詢書籍目錄失敗");
        return;
    }
    const res = await mapBookList(list);
    if (!res) {
        logger.error("抓取章節信息,調用 getCurBookSectionList() 進行串行遍歷操作,執行完成回調出錯,錯誤信息已打印,請查看日志!");
        return;
    }
    return res;
}
/**
 * 遍歷書籍目錄下的章節列表
 * @param {*} list 
 */
const mapBookList = (list) => {
    return new Promise((resolve, reject) => {
        async.mapLimit(list, 1, (series, callback) => {
            let doc = series._doc;
            getCurBookSectionList(doc, callback);
        }, (err, result) => {
            if (err) {
                logger.error("書籍目錄抓取異步執行出錯!");
                logger.error(err);
                reject(false);
                return;
            }
            resolve(true);
        })
    })
}

/**
 * 獲取單本書籍下章節列表 調用章節列表遍歷進行抓取內容
 * @param {*} series 
 * @param {*} callback 
 */
const getCurBookSectionList = async(series, callback) => {

    let num = Math.random() * 1000 + 1000;
    await sleep(num);
    let key = series.key;
    const res = await bookHelper.querySectionList(chapterListModel, {
        key: key
    });
    if (!res) {
        logger.error("獲取當前書籍: " + series.bookName + " 章節內容失敗,進入下一部書籍內容抓取!");
        callback(null, null);
        return;
    }
    //判斷當前數據是否已經存在
    const bookItemModel = getModel(key);
    const contentLength = await bookHelper.getCollectionLength(bookItemModel, {});
    if (contentLength === res.length) {
        logger.info("當前書籍:" + series.bookName + "數據庫已經抓取完成,進入下一條數據任務");
        callback(null, null);
        return;
    }
    await mapSectionList(res);
    callback(null, null);
}

數據抓取完了 怎么保存是個問題

這里我們通過key 來給數據做分類,每次按照key來獲取鏈接,進行遍歷,這樣的好處是保存的數據是一個整體,現在思考數據保存的問題

1 可以以整體的方式進行插入

優點 : 速度快 數據庫操作不浪費時間。

缺點 : 有的書籍可能有幾百個章節 也就意味著要先保存幾百個頁面的內容再進行插入,這樣做同樣很消耗內存,有可能造成程序運行不穩定。

2可以以每一篇文章的形式插入數據庫。

優點 : 頁面抓取即保存的方式 使得數據能夠及時保存,即使后續出錯也不需要重新保存前面的章節,

缺點 : 也很明顯 就是慢 ,仔細想想如果要爬幾萬個頁面 做 幾萬次*N 數據庫的操作 這里還可以做一個緩存器一次性保存一定條數 當條數達到再做保存這樣也是一個不錯的選擇。

/**
 * 遍歷單條書籍下所有章節 調用內容抓取方法
 * @param {*} list 
 */
const mapSectionList = (list) => {
    return new Promise((resolve, reject) => {
        async.mapLimit(list, 1, (series, callback) => {
            let doc = series._doc;
            getContent(doc, callback)
        }, (err, result) => {
            if (err) {
                logger.error("書籍目錄抓取異步執行出錯!");
                logger.error(err);
                reject(false);
                return;
            }
            const bookName = list[0].bookName;
            const key = list[0].key;

            // 以整體為單元進行保存
            saveAllContentToDB(result, bookName, key, resolve);

            //以每篇文章作為單元進行保存
            // logger.info(bookName + "數據抓取完成,進入下一部書籍抓取函數...");
            // resolve(true);

        })
    })
}

兩者各有利弊,這里我們都做了嘗試。 準備了兩個錯誤保存的集合,errContentModel, errorCollectionModel,在插入出錯時 分別保存信息到對應的集合中,二者任選其一即可。增加集合來保存數據的原因是 便于一次性查看以及后續操作, 不用看日志。

(PS ,其實完全用 errorCollectionModel 這個集合就可以了 ,errContentModel這個集合可以完整保存章節信息)

//保存出錯的數據名稱
const errorSpider = mongoose.Schema({
    chapter: String,
    section: String,
    url: String,
    key: String,
    bookName: String,
    author: String,
})
// 保存出錯的數據名稱 只保留key 和 bookName信息
const errorCollection = mongoose.Schema({
    key: String,
    bookName: String,
})

我們將每一條書籍信息的內容 放到一個新的集合中,集合以key來進行命名。

總結

寫這個項目 其實主要的難點在于程序穩定性的控制,容錯機制的設置,以及錯誤的記錄,目前這個項目基本能夠實現直接運行 一次性跑通整個流程。 但是程序設計也肯定還存在許多問題 ,歡迎指正和交流。

彩蛋

寫完這個項目 做了一個基于React開的前端網站用于頁面瀏覽 和一個基于koa2.x開發的服務端, 整體技術棧相當于是 React + Redux + Koa2 ,前后端服務是分開部署的,各自獨立可以更好的去除前后端服務的耦合性,比如同一套服務端代碼,不僅可以給web端 還可以給 移動端 ,app 提供支持。目前整個一套還很簡陋,但是可以滿足基本的查詢瀏覽功能。希望后期有時間可以把項目變得更加豐富。

本項目地址 地址 : guwen-spider

對應前端 React + Redux + semantic-ui 地址 : guwen-react

對應Node端 Koa2.2 + mongoose 地址 : guwen-node

項目挺簡單的 ,但是多了一個學習和研究 從前端到服務端的開發的環境。

以上です

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/19165.html

相關文章

  • 前端開發收集 - 收藏集 - 掘金

    摘要:責編現代化的方式開發一個圖片上傳工具前端掘金對于圖片上傳,大家一定不陌生。之深入事件機制前端掘金事件綁定的方式原生的事件綁定方式有幾種想必有很多朋友說種目前,在本人目前的研究中,只有兩種半兩種半還有半種的且聽我道來。 Ajax 與數據傳輸 - 前端 - 掘金背景 在沒有ajax之前,前端與后臺傳數據都是靠表單傳輸,使用表單的方法傳輸數據有一個比較大的問題就是每次提交數據都會刷新頁面,用...

    ygyooo 評論0 收藏0
  • Python爬蟲學習路線

    摘要:以下這些項目,你拿來學習學習練練手。當你每個步驟都能做到很優秀的時候,你應該考慮如何組合這四個步驟,使你的爬蟲達到效率最高,也就是所謂的爬蟲策略問題,爬蟲策略學習不是一朝一夕的事情,建議多看看一些比較優秀的爬蟲的設計方案,比如說。 (一)如何學習Python 學習Python大致可以分為以下幾個階段: 1.剛上手的時候肯定是先過一遍Python最基本的知識,比如說:變量、數據結構、語法...

    liaoyg8023 評論0 收藏0
  • Evil Python

    摘要:用將倒放這次讓我們一個用做一個小工具將動態圖片倒序播放發現引力波的機構使用的包美國科學家日宣布,他們去年月首次探測到引力波。宣布這一發現的,是激光干涉引力波天文臺的負責人。這個機構誕生于上世紀年代,進行引力波觀測已經有近年。 那些年我們寫過的爬蟲 從寫 nodejs 的第一個爬蟲開始陸陸續續寫了好幾個爬蟲,從爬拉勾網上的職位信息到爬豆瓣上的租房帖子,再到去爬知乎上的妹子照片什么的,爬蟲...

    Turbo 評論0 收藏0
  • 不再羨慕python,nodejs爬蟲擼起袖子就是干,爬取宅男女神網大姐姐的高清圖片!

    摘要:年前無心工作,上班刷知乎發現一篇分享爬蟲的文章。另外攜帶的數據是用來告訴服務器當前請求是從哪個頁面請求過來的。 年前無心工作,上班刷知乎發現一篇分享python爬蟲的文章。 感覺他爬取的網站里的妹子都好好看哦,超喜歡這里的,里面個個都是美女。 無小意丶:自我發掘爬蟲實戰1:宅男女神網妹子圖片批量抓取,分類保存到本地和MongoDB數據庫 無奈python雖然入門過但太久沒用早已荒廢,最...

    rubyshen 評論0 收藏0
  • Nodejs爬蟲實戰項目之鏈家

    摘要:很基礎,不喜勿噴轉載注明出處爬蟲實戰項目之鏈家效果圖思路爬蟲究竟是怎么實現的通過訪問要爬取的網站地址,獲得該頁面的文檔內容,找到我們需要保存的數據,進一步查看數據所在的元素節點,他們在某方面一定是有規律的,遵循規律,操作,保存數據。 說明 作為一個前端界的小學生,一直想著自己做一些項目向全棧努力。愁人的是沒有后臺,搜羅之后且學會了nodejs和express寫成本地的接口給前端頁面調用...

    noONE 評論0 收藏0

發表評論

0條評論

legendmohe

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<