您好,登錄后才能下訂單哦!
這篇文章主要講解了“怎么使用NodeJs爬蟲抓取古代典籍”,文中的講解內(nèi)容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“怎么使用NodeJs爬蟲抓取古代典籍”吧!
項目實現(xiàn)方案分析
項目是一個典型的多級抓取案例,目前只有三級,即 書籍列表, 書籍項對應(yīng)的 章節(jié)列表,一個章節(jié)鏈接對應(yīng)的內(nèi)容。 抓取這樣的結(jié)構(gòu)可以采用兩種方式, 一是 直接從外層到內(nèi)層 內(nèi)層抓取完以后再執(zhí)行下一個外層的抓取, 還有一種就是先把外層抓取完成保存到數(shù)據(jù)庫,然后根據(jù)外層抓取到所有內(nèi)層章節(jié)的鏈接,再次保存,然后從數(shù)據(jù)庫查詢到對應(yīng)的鏈接單元 對之進行內(nèi)容抓取。這兩種方案各有利弊,其實兩種方式我都試過, 后者有一個好處,因為對三個層級是分開抓取的, 這樣就能夠更方便,盡可能多的保存到對應(yīng)章節(jié)的相關(guān)數(shù)據(jù)。 可以試想一下 ,如果采用前者 按照正常的邏輯
對一級目錄進行遍歷抓取到對應(yīng)的二級章節(jié)目錄, 再對章節(jié)列表進行遍歷 抓取內(nèi)容,到第三級 內(nèi)容單元抓取完成 需要保存時,如果需要很多的一級目錄信息,就需要 這些分層的數(shù)據(jù)之間進行數(shù)據(jù)傳遞 ,想想其實應(yīng)該是比較復雜的一件事情。所以分開保存數(shù)據(jù) 一定程度上避開了不必要的復雜的數(shù)據(jù)傳遞。
目前我們考慮到 其實我們要抓取到的古文書籍數(shù)量并不多,古文書籍大概只有180本囊括了各種經(jīng)史。其和章節(jié)內(nèi)容本身是一個很小的數(shù)據(jù) ,即一個集合里面有180個文檔記錄。 這180本書所有章節(jié)抓取下來一共有一萬六千個章節(jié),對應(yīng)需要訪問一萬六千個頁面爬取到對應(yīng)的內(nèi)容。所以選擇第二種應(yīng)該是合理的。
項目實現(xiàn)
主程有三個方法 bookListInit ,chapterListInit,contentListInit, 分別是抓取書籍目錄,章節(jié)列表,書籍內(nèi)容的方法對外公開暴露的初始化方法。通過async 可以實現(xiàn)對這三個方法的運行流程進行控制,書籍目錄抓取完成將數(shù)據(jù)保存到數(shù)據(jù)庫,然后執(zhí)行結(jié)果返回到主程序,如果運行成功 主程序則執(zhí)行根據(jù)書籍列表對章節(jié)列表的抓取,同理對書籍內(nèi)容進行抓取。
項目主入口
/** * 爬蟲抓取主入口 */ const start = async() => { let booklistRes = await bookListInit(); if (!booklistRes) { logger.warn('書籍列表抓取出錯,程序終止...'); return; } logger.info('書籍列表抓取成功,現(xiàn)在進行書籍章節(jié)抓取...'); let chapterlistRes = await chapterListInit(); if (!chapterlistRes) { logger.warn('書籍章節(jié)列表抓取出錯,程序終止...'); return; } logger.info('書籍章節(jié)列表抓取成功,現(xiàn)在進行書籍內(nèi)容抓取...'); let contentListRes = await contentListInit(); if (!contentListRes) { logger.warn('書籍章節(jié)內(nèi)容抓取出錯,程序終止...'); return; } logger.info('書籍內(nèi)容抓取成功'); } // 開始入口 if (typeof bookListInit === 'function' && typeof chapterListInit === 'function') { // 開始抓取 start(); }
引入的 bookListInit ,chapterListInit,contentListInit, 三個方法
booklist.js
/** * 初始化入口 */ const chapterListInit = async() => { const list = await bookHelper.getBookList(bookListModel); if (!list) { logger.error('初始化查詢書籍目錄失敗'); } logger.info('開始抓取書籍章節(jié)列表,書籍目錄共:' + list.length + '條'); let res = await asyncGetChapter(list); return res; };
chapterlist.js
/** * 初始化入口 */ const contentListInit = async() => { //獲取書籍列表 const list = await bookHelper.getBookLi(bookListModel); if (!list) { logger.error('初始化查詢書籍目錄失敗'); return; } const res = await mapBookList(list); if (!res) { logger.error('抓取章節(jié)信息,調(diào)用 getCurBookSectionList() 進行串行遍歷操作,執(zhí)行完成回調(diào)出錯,錯誤信息已打印,請查看日志!'); return; } return res; }
內(nèi)容抓取的思考
書籍目錄抓取其實邏輯非常簡單,只需要使用async.mapLimit做一個遍歷就可以保存數(shù)據(jù)了,但是我們在保存內(nèi)容的時候 簡化的邏輯其實就是 遍歷章節(jié)列表 抓取鏈接里的內(nèi)容。但是實際的情況是鏈接數(shù)量多達幾萬 我們從內(nèi)存占用角度也不能全部保存到一個數(shù)組中,然后對其遍歷,所以我們需要對內(nèi)容抓取進行單元化。
普遍的遍歷方式 是每次查詢一定的數(shù)量,來做抓取,這樣缺點是只是以一定數(shù)量做分類,數(shù)據(jù)之間沒有關(guān)聯(lián),以批量方式進行插入,如果出錯 則容錯會有一些小問題,而且我們想一本書作為一個集合單獨保存會遇到問題。因此我們采用第二種就是以一個書籍單元進行內(nèi)容抓取和保存。
這里使用了 async.mapLimit(list, 1, (series, callback) => {}) 這個方法來進行遍歷,不可避免的用到了回調(diào),感覺很惡心。async.mapLimit()的第二個參數(shù)可以設(shè)置同時請求數(shù)量。
/* * 內(nèi)容抓取步驟: * ***步得到書籍列表, 通過書籍列表查到一條書籍記錄下 對應(yīng)的所有章節(jié)列表, * 第二步 對章節(jié)列表進行遍歷獲取內(nèi)容保存到數(shù)據(jù)庫中 * 第三步 保存完數(shù)據(jù)后 回到***步 進行下一步書籍的內(nèi)容抓取和保存 */ /** * 初始化入口 */ const contentListInit = async() => { //獲取書籍列表 const list = await bookHelper.getBookList(bookListModel); if (!list) { logger.error('初始化查詢書籍目錄失敗'); return; } const res = await mapBookList(list); if (!res) { logger.error('抓取章節(jié)信息,調(diào)用 getCurBookSectionList() 進行串行遍歷操作,執(zhí)行完成回調(diào)出錯,錯誤信息已打印,請查看日志!'); return; } return res; } /** * 遍歷書籍目錄下的章節(jié)列表 * @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('書籍目錄抓取異步執(zhí)行出錯!'); logger.error(err); reject(false); return; } resolve(true); }) }) } /** * 獲取單本書籍下章節(jié)列表 調(diào)用章節(jié)列表遍歷進行抓取內(nèi)容 * @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 + ' 章節(jié)內(nèi)容失敗,進入下一部書籍內(nèi)容抓取!'); callback(null, null); return; } //判斷當前數(shù)據(jù)是否已經(jīng)存在 const bookItemModel = getModel(key); const contentLength = await bookHelper.getCollectionLength(bookItemModel, {}); if (contentLength === res.length) { logger.info('當前書籍:' + series.bookName + '數(shù)據(jù)庫已經(jīng)抓取完成,進入下一條數(shù)據(jù)任務(wù)'); callback(null, null); return; } await mapSectionList(res); callback(null, null); }
數(shù)據(jù)抓取完了 怎么保存是個問題
這里我們通過key 來給數(shù)據(jù)做分類,每次按照key來獲取鏈接,進行遍歷,這樣的好處是保存的數(shù)據(jù)是一個整體,現(xiàn)在思考數(shù)據(jù)保存的問題
1、可以以整體的方式進行插入
優(yōu)點 : 速度快 數(shù)據(jù)庫操作不浪費時間。
缺點 : 有的書籍可能有幾百個章節(jié) 也就意味著要先保存幾百個頁面的內(nèi)容再進行插入,這樣做同樣很消耗內(nèi)存,有可能造成程序運行不穩(wěn)定。
2、可以以每一篇文章的形式插入數(shù)據(jù)庫。
優(yōu)點 : 頁面抓取即保存的方式 使得數(shù)據(jù)能夠及時保存,即使后續(xù)出錯也不需要重新保存前面的章節(jié),
缺點 : 也很明顯 就是慢 ,仔細想想如果要爬幾萬個頁面 做 幾萬次*N 數(shù)據(jù)庫的操作 這里還可以做一個緩存器一次性保存一定條數(shù) 當條數(shù)達到再做保存這樣也是一個不錯的選擇。
/** * 遍歷單條書籍下所有章節(jié) 調(diào)用內(nèi)容抓取方法 * @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('書籍目錄抓取異步執(zhí)行出錯!'); logger.error(err); reject(false); return; } const bookName = list[0].bookName; const key = list[0].key; // 以整體為單元進行保存 saveAllContentToDB(result, bookName, key, resolve); //以每篇文章作為單元進行保存 // logger.info(bookName + '數(shù)據(jù)抓取完成,進入下一部書籍抓取函數(shù)...'); // resolve(true); }) }) }
兩者各有利弊,這里我們都做了嘗試。 準備了兩個錯誤保存的集合,errContentModel, errorCollectionModel,在插入出錯時 分別保存信息到對應(yīng)的集合中,二者任選其一即可。增加集合來保存數(shù)據(jù)的原因是 便于一次性查看以及后續(xù)操作, 不用看日志。
(PS ,其實完全用 errorCollectionModel 這個集合就可以了 ,errContentModel這個集合可以完整保存章節(jié)信息)
//保存出錯的數(shù)據(jù)名稱 const errorSpider = mongoose.Schema({ chapter: String, section: String, url: String, key: String, bookName: String, author: String, }) // 保存出錯的數(shù)據(jù)名稱 只保留key 和 bookName信息 const errorCollection = mongoose.Schema({ key: String, bookName: String, })
我們將每一條書籍信息的內(nèi)容 放到一個新的集合中,集合以key來進行命名。
感謝各位的閱讀,以上就是“怎么使用NodeJs爬蟲抓取古代典籍”的內(nèi)容了,經(jīng)過本文的學習后,相信大家對怎么使用NodeJs爬蟲抓取古代典籍這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。