怎么使用NodeJs爬蟲抓取古代典籍

這篇文章主要講解了“怎么使用NodeJs爬蟲抓取古代典籍”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“怎么使用NodeJs爬蟲抓取古代典籍”吧!

為山東等地區(qū)用戶提供了全套網(wǎng)頁(yè)設(shè)計(jì)制作服務(wù),及山東網(wǎng)站建設(shè)行業(yè)解決方案。主營(yíng)業(yè)務(wù)為做網(wǎng)站、成都做網(wǎng)站、山東網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠(chéng)的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會(huì)得到認(rèn)可,從而選擇與我們長(zhǎng)期合作。這樣,我們也可以走得更遠(yuǎn)!

項(xiàng)目實(shí)現(xiàn)方案分析

項(xiàng)目是一個(gè)典型的多級(jí)抓取案例,目前只有三級(jí),即 書籍列表, 書籍項(xiàng)對(duì)應(yīng)的 章節(jié)列表,一個(gè)章節(jié)鏈接對(duì)應(yīng)的內(nèi)容。 抓取這樣的結(jié)構(gòu)可以采用兩種方式, 一是  直接從外層到內(nèi)層 內(nèi)層抓取完以后再執(zhí)行下一個(gè)外層的抓取,  還有一種就是先把外層抓取完成保存到數(shù)據(jù)庫(kù),然后根據(jù)外層抓取到所有內(nèi)層章節(jié)的鏈接,再次保存,然后從數(shù)據(jù)庫(kù)查詢到對(duì)應(yīng)的鏈接單元  對(duì)之進(jìn)行內(nèi)容抓取。這兩種方案各有利弊,其實(shí)兩種方式我都試過(guò), 后者有一個(gè)好處,因?yàn)閷?duì)三個(gè)層級(jí)是分開抓取的,  這樣就能夠更方便,盡可能多的保存到對(duì)應(yīng)章節(jié)的相關(guān)數(shù)據(jù)。 可以試想一下 ,如果采用前者 按照正常的邏輯

對(duì)一級(jí)目錄進(jìn)行遍歷抓取到對(duì)應(yīng)的二級(jí)章節(jié)目錄, 再對(duì)章節(jié)列表進(jìn)行遍歷 抓取內(nèi)容,到第三級(jí) 內(nèi)容單元抓取完成 需要保存時(shí),如果需要很多的一級(jí)目錄信息,就需要  這些分層的數(shù)據(jù)之間進(jìn)行數(shù)據(jù)傳遞 ,想想其實(shí)應(yīng)該是比較復(fù)雜的一件事情。所以分開保存數(shù)據(jù) 一定程度上避開了不必要的復(fù)雜的數(shù)據(jù)傳遞。

目前我們考慮到 其實(shí)我們要抓取到的古文書籍?dāng)?shù)量并不多,古文書籍大概只有180本囊括了各種經(jīng)史。其和章節(jié)內(nèi)容本身是一個(gè)很小的數(shù)據(jù)  ,即一個(gè)集合里面有180個(gè)文檔記錄。  這180本書所有章節(jié)抓取下來(lái)一共有一萬(wàn)六千個(gè)章節(jié),對(duì)應(yīng)需要訪問(wèn)一萬(wàn)六千個(gè)頁(yè)面爬取到對(duì)應(yīng)的內(nèi)容。所以選擇第二種應(yīng)該是合理的。

項(xiàng)目實(shí)現(xiàn)

主程有三個(gè)方法 bookListInit ,chapterListInit,contentListInit,  分別是抓取書籍目錄,章節(jié)列表,書籍內(nèi)容的方法對(duì)外公開暴露的初始化方法。通過(guò)async  可以實(shí)現(xiàn)對(duì)這三個(gè)方法的運(yùn)行流程進(jìn)行控制,書籍目錄抓取完成將數(shù)據(jù)保存到數(shù)據(jù)庫(kù),然后執(zhí)行結(jié)果返回到主程序,如果運(yùn)行成功  主程序則執(zhí)行根據(jù)書籍列表對(duì)章節(jié)列表的抓取,同理對(duì)書籍內(nèi)容進(jìn)行抓取。

項(xiàng)目主入口

/**  * 爬蟲抓取主入口  */ const start = async() => {     let booklistRes = await bookListInit();     if (!booklistRes) {         logger.warn('書籍列表抓取出錯(cuò),程序終止...');         return;     }     logger.info('書籍列表抓取成功,現(xiàn)在進(jìn)行書籍章節(jié)抓取...');      let chapterlistRes = await chapterListInit();     if (!chapterlistRes) {         logger.warn('書籍章節(jié)列表抓取出錯(cuò),程序終止...');         return;     }     logger.info('書籍章節(jié)列表抓取成功,現(xiàn)在進(jìn)行書籍內(nèi)容抓取...');      let contentListRes = await contentListInit();     if (!contentListRes) {         logger.warn('書籍章節(jié)內(nèi)容抓取出錯(cuò),程序終止...');         return;     }     logger.info('書籍內(nèi)容抓取成功'); } // 開始入口 if (typeof bookListInit === 'function' && typeof chapterListInit === 'function') {     // 開始抓取     start(); }

引入的 bookListInit ,chapterListInit,contentListInit, 三個(gè)方法

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() 進(jìn)行串行遍歷操作,執(zhí)行完成回調(diào)出錯(cuò),錯(cuò)誤信息已打印,請(qǐng)查看日志!');         return;     }     return res; }

內(nèi)容抓取的思考

書籍目錄抓取其實(shí)邏輯非常簡(jiǎn)單,只需要使用async.mapLimit做一個(gè)遍歷就可以保存數(shù)據(jù)了,但是我們?cè)诒4鎯?nèi)容的時(shí)候 簡(jiǎn)化的邏輯其實(shí)就是 遍歷章節(jié)列表  抓取鏈接里的內(nèi)容。但是實(shí)際的情況是鏈接數(shù)量多達(dá)幾萬(wàn) 我們從內(nèi)存占用角度也不能全部保存到一個(gè)數(shù)組中,然后對(duì)其遍歷,所以我們需要對(duì)內(nèi)容抓取進(jìn)行單元化。

普遍的遍歷方式 是每次查詢一定的數(shù)量,來(lái)做抓取,這樣缺點(diǎn)是只是以一定數(shù)量做分類,數(shù)據(jù)之間沒(méi)有關(guān)聯(lián),以批量方式進(jìn)行插入,如果出錯(cuò)  則容錯(cuò)會(huì)有一些小問(wèn)題,而且我們想一本書作為一個(gè)集合單獨(dú)保存會(huì)遇到問(wèn)題。因此我們采用第二種就是以一個(gè)書籍單元進(jìn)行內(nèi)容抓取和保存。

這里使用了 async.mapLimit(list, 1, (series, callback) => {})  這個(gè)方法來(lái)進(jìn)行遍歷,不可避免的用到了回調(diào),感覺(jué)很惡心。async.mapLimit()的第二個(gè)參數(shù)可以設(shè)置同時(shí)請(qǐng)求數(shù)量。

/*   * 內(nèi)容抓取步驟:  * ***步得到書籍列表, 通過(guò)書籍列表查到一條書籍記錄下 對(duì)應(yīng)的所有章節(jié)列表,   * 第二步 對(duì)章節(jié)列表進(jìn)行遍歷獲取內(nèi)容保存到數(shù)據(jù)庫(kù)中   * 第三步 保存完數(shù)據(jù)后 回到***步 進(jìn)行下一步書籍的內(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() 進(jìn)行串行遍歷操作,執(zhí)行完成回調(diào)出錯(cuò),錯(cuò)誤信息已打印,請(qǐng)查看日志!');         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í)行出錯(cuò)!');                 logger.error(err);                 reject(false);                 return;             }             resolve(true);         })     }) }  /**  * 獲取單本書籍下章節(jié)列表 調(diào)用章節(jié)列表遍歷進(jìn)行抓取內(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('獲取當(dāng)前書籍: ' + series.bookName + ' 章節(jié)內(nèi)容失敗,進(jìn)入下一部書籍內(nèi)容抓取!');         callback(null, null);         return;     }     //判斷當(dāng)前數(shù)據(jù)是否已經(jīng)存在     const bookItemModel = getModel(key);     const contentLength = await bookHelper.getCollectionLength(bookItemModel, {});     if (contentLength === res.length) {         logger.info('當(dāng)前書籍:' + series.bookName + '數(shù)據(jù)庫(kù)已經(jīng)抓取完成,進(jìn)入下一條數(shù)據(jù)任務(wù)');         callback(null, null);         return;     }     await mapSectionList(res);     callback(null, null); }

數(shù)據(jù)抓取完了 怎么保存是個(gè)問(wèn)題

這里我們通過(guò)key 來(lái)給數(shù)據(jù)做分類,每次按照key來(lái)獲取鏈接,進(jìn)行遍歷,這樣的好處是保存的數(shù)據(jù)是一個(gè)整體,現(xiàn)在思考數(shù)據(jù)保存的問(wèn)題

1、可以以整體的方式進(jìn)行插入

優(yōu)點(diǎn) :速度快 數(shù)據(jù)庫(kù)操作不浪費(fèi)時(shí)間。

缺點(diǎn) :有的書籍可能有幾百個(gè)章節(jié) 也就意味著要先保存幾百個(gè)頁(yè)面的內(nèi)容再進(jìn)行插入,這樣做同樣很消耗內(nèi)存,有可能造成程序運(yùn)行不穩(wěn)定。

2、可以以每一篇文章的形式插入數(shù)據(jù)庫(kù)。

優(yōu)點(diǎn) :頁(yè)面抓取即保存的方式 使得數(shù)據(jù)能夠及時(shí)保存,即使后續(xù)出錯(cuò)也不需要重新保存前面的章節(jié),

缺點(diǎn) :也很明顯 就是慢 ,仔細(xì)想想如果要爬幾萬(wàn)個(gè)頁(yè)面 做 幾萬(wàn)次*N 數(shù)據(jù)庫(kù)的操作 這里還可以做一個(gè)緩存器一次性保存一定條數(shù)  當(dāng)條數(shù)達(dá)到再做保存這樣也是一個(gè)不錯(cuò)的選擇。

/**  * 遍歷單條書籍下所有章節(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í)行出錯(cuò)!');                 logger.error(err);                 reject(false);                 return;             }             const bookName = list[0].bookName;             const key = list[0].key;              // 以整體為單元進(jìn)行保存             saveAllContentToDB(result, bookName, key, resolve);              //以每篇文章作為單元進(jìn)行保存             // logger.info(bookName + '數(shù)據(jù)抓取完成,進(jìn)入下一部書籍抓取函數(shù)...');             // resolve(true);          })     }) }

兩者各有利弊,這里我們都做了嘗試。 準(zhǔn)備了兩個(gè)錯(cuò)誤保存的集合,errContentModel, errorCollectionModel,在插入出錯(cuò)時(shí)  分別保存信息到對(duì)應(yīng)的集合中,二者任選其一即可。增加集合來(lái)保存數(shù)據(jù)的原因是 便于一次性查看以及后續(xù)操作, 不用看日志。

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

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

我們將每一條書籍信息的內(nèi)容 放到一個(gè)新的集合中,集合以key來(lái)進(jìn)行命名。

感謝各位的閱讀,以上就是“怎么使用NodeJs爬蟲抓取古代典籍”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)怎么使用NodeJs爬蟲抓取古代典籍這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

網(wǎng)站欄目:怎么使用NodeJs爬蟲抓取古代典籍
文章路徑:http://bm7419.com/article32/geijpc.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)頁(yè)設(shè)計(jì)公司、網(wǎng)站收錄、面包屑導(dǎo)航、虛擬主機(jī)、外貿(mào)建站、標(biāo)簽優(yōu)化

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

成都app開發(fā)公司