自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

NodeJs爬蟲(chóng)抓取古代典籍,共計(jì)16000個(gè)頁(yè)面心得體會(huì)總結(jié)及項(xiàng)目分享

開(kāi)發(fā) 前端
之前研究數(shù)據(jù),零零散散的寫(xiě)過(guò)一些數(shù)據(jù)抓取的爬蟲(chóng),不過(guò)寫(xiě)的比較隨意。有很多地方現(xiàn)在看起來(lái)并不是很合理 這段時(shí)間比較閑,本來(lái)是想給之前的項(xiàng)目做重構(gòu)的。后來(lái) 利用這個(gè)周末,索性重新寫(xiě)了一個(gè)項(xiàng)目,就是本項(xiàng)目 guwen-spider。目前這個(gè)爬蟲(chóng)還是比較簡(jiǎn)單的類(lèi)型的, 直接抓取頁(yè)面,然后在頁(yè)面中提取數(shù)據(jù),保存數(shù)據(jù)到數(shù)據(jù)庫(kù)。

前言

之前研究數(shù)據(jù),零零散散的寫(xiě)過(guò)一些數(shù)據(jù)抓取的爬蟲(chóng),不過(guò)寫(xiě)的比較隨意。有很多地方現(xiàn)在看起來(lái)并不是很合理 這段時(shí)間比較閑,本來(lái)是想給之前的項(xiàng)目做重構(gòu)的。

后來(lái) 利用這個(gè)周末,索性重新寫(xiě)了一個(gè)項(xiàng)目,就是本項(xiàng)目 guwen-spider。目前這個(gè)爬蟲(chóng)還是比較簡(jiǎn)單的類(lèi)型的, 直接抓取頁(yè)面,然后在頁(yè)面中提取數(shù)據(jù),保存數(shù)據(jù)到數(shù)據(jù)庫(kù)。

通過(guò)與之前寫(xiě)的對(duì)比,我覺(jué)得難點(diǎn)在于整個(gè)程序的健壯性,以及相應(yīng)的容錯(cuò)機(jī)制。在昨天寫(xiě)代碼的過(guò)程中其實(shí)也有反映, 真正的主體代碼其實(shí)很快就寫(xiě)完了 ,花了大部分時(shí)間是在

做穩(wěn)定性的調(diào)試, 以及尋求一種更合理的方式來(lái)處理數(shù)據(jù)與流程控制的關(guān)系。

NodeJs爬蟲(chóng)抓取古代典籍,共計(jì)16000個(gè)頁(yè)面心得體會(huì)總結(jié)及項(xiàng)目分享

背景

項(xiàng)目的背景是抓取一個(gè)一級(jí)頁(yè)面是目錄列表 ,點(diǎn)擊一個(gè)目錄進(jìn)去 是一個(gè)章節(jié) 及篇幅列表 ,點(diǎn)擊章節(jié)或篇幅進(jìn)入具體的內(nèi)容頁(yè)面。

概述

本項(xiàng)目github地址 : guwen-spider (PS:***面還有彩蛋 ~~逃

項(xiàng)目技術(shù)細(xì)節(jié)

項(xiàng)目大量用到了 ES7 的async 函數(shù), 更直觀的反應(yīng)程序了的流程。為了方便,在對(duì)數(shù)據(jù)遍歷的過(guò)程中直接使用了著名的async這個(gè)庫(kù),所以不可避免的還是用到了回調(diào)promise ,因?yàn)閿?shù)據(jù)的處理發(fā)生在回調(diào)函數(shù)中,不可避免的會(huì)遇到一些數(shù)據(jù)傳遞的問(wèn)題,其實(shí)也可以直接用ES7的async await 寫(xiě)一個(gè)方法來(lái)實(shí)現(xiàn)相同的功能。這里其實(shí)最贊的一個(gè)地方是使用了 Class 的 static 方法封裝對(duì)數(shù)據(jù)庫(kù)的操作, static 顧名思義 靜態(tài)方法 就跟 prototype 一樣 ,不會(huì)占用額外空間。

項(xiàng)目主要用到了

  1. ES7的 async await 協(xié)程做異步有關(guān)的邏輯處理。
  2. 使用 npm的 async庫(kù) 來(lái)做循環(huán)遍歷,以及并發(fā)請(qǐng)求操作。
  3. 使用 log4js 來(lái)做日志處理
  4. 使用 cheerio 來(lái)處理dom的操作。
  5. 使用 mongoose 來(lái)連接mongoDB 做數(shù)據(jù)的保存以及操作。

目錄結(jié)構(gòu)

  • ├── bin // 入口
  • │ ├── booklist.js // 抓取書(shū)籍邏輯
  • │ ├── chapterlist.js // 抓取章節(jié)邏輯
  • │ ├── content.js // 抓取內(nèi)容邏輯
  • │ └── index.js // 程序入口
  • ├── config // 配置文件
  • ├── dbhelper // 數(shù)據(jù)庫(kù)操作方法目錄
  • ├── logs // 項(xiàng)目日志目錄
  • ├── model // mongoDB 集合操作實(shí)例
  • ├── node_modules
  • ├── utils // 工具函數(shù)
  • ├── package.json

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

項(xiàng)目是一個(gè)典型的多級(jí)抓取案例,目前只有三級(jí),即 書(shū)籍列表, 書(shū)籍項(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ù)查詢(xún)到對(duì)應(yīng)的鏈接單元 對(duì)之進(jìn)行內(nèi)容抓取。這兩種方案各有利弊,其實(shí)兩種方式我都試過(guò), 后者有一個(gè)好處,因?yàn)閷?duì)三個(gè)層級(jí)是分開(kāi)抓取的, 這樣就能夠更方便,盡可能多的保存到對(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ù)雜的一件事情。所以分開(kāi)保存數(shù)據(jù) 一定程度上避開(kāi)了不必要的復(fù)雜的數(shù)據(jù)傳遞。

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

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

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

項(xiàng)目主入口

 

  1. /** 
  2.  * 爬蟲(chóng)抓取主入口 
  3.  */ 
  4. const start = async() => { 
  5.     let booklistRes = await bookListInit(); 
  6.     if (!booklistRes) { 
  7.         logger.warn('書(shū)籍列表抓取出錯(cuò),程序終止...'); 
  8.         return
  9.     } 
  10.     logger.info('書(shū)籍列表抓取成功,現(xiàn)在進(jìn)行書(shū)籍章節(jié)抓取...'); 
  11.  
  12.     let chapterlistRes = await chapterListInit(); 
  13.     if (!chapterlistRes) { 
  14.         logger.warn('書(shū)籍章節(jié)列表抓取出錯(cuò),程序終止...'); 
  15.         return
  16.     } 
  17.     logger.info('書(shū)籍章節(jié)列表抓取成功,現(xiàn)在進(jìn)行書(shū)籍內(nèi)容抓取...'); 
  18.  
  19.     let contentListRes = await contentListInit(); 
  20.     if (!contentListRes) { 
  21.         logger.warn('書(shū)籍章節(jié)內(nèi)容抓取出錯(cuò),程序終止...'); 
  22.         return
  23.     } 
  24.     logger.info('書(shū)籍內(nèi)容抓取成功'); 
  25. // 開(kāi)始入口 
  26. if (typeof bookListInit === 'function' && typeof chapterListInit === 'function') { 
  27.     // 開(kāi)始抓取 
  28.     start(); 

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

booklist.js

 

  1. /** 
  2.  * 初始化入口 
  3.  */ 
  4. const chapterListInit = async() => { 
  5.     const list = await bookHelper.getBookList(bookListModel); 
  6.     if (!list) { 
  7.         logger.error('初始化查詢(xún)書(shū)籍目錄失敗'); 
  8.     } 
  9.     logger.info('開(kāi)始抓取書(shū)籍章節(jié)列表,書(shū)籍目錄共:' + list.length + '條'); 
  10.     let res = await asyncGetChapter(list); 
  11.     return res; 
  12. }; 

chapterlist.js

 

  1. /** 
  2.  * 初始化入口 
  3.  */ 
  4. const contentListInit = async() => { 
  5.     //獲取書(shū)籍列表 
  6.     const list = await bookHelper.getBookLi(bookListModel); 
  7.     if (!list) { 
  8.         logger.error('初始化查詢(xún)書(shū)籍目錄失敗'); 
  9.         return
  10.     } 
  11.     const res = await mapBookList(list); 
  12.     if (!res) { 
  13.         logger.error('抓取章節(jié)信息,調(diào)用 getCurBookSectionList() 進(jìn)行串行遍歷操作,執(zhí)行完成回調(diào)出錯(cuò),錯(cuò)誤信息已打印,請(qǐng)查看日志!'); 
  14.         return
  15.     } 
  16.     return res; 

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

書(shū)籍目錄抓取其實(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)行單元化。

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

 

  1. /*  
  2.  * 內(nèi)容抓取步驟: 
  3.  * ***步得到書(shū)籍列表, 通過(guò)書(shū)籍列表查到一條書(shū)籍記錄下 對(duì)應(yīng)的所有章節(jié)列表,  
  4.  * 第二步 對(duì)章節(jié)列表進(jìn)行遍歷獲取內(nèi)容保存到數(shù)據(jù)庫(kù)中  
  5.  * 第三步 保存完數(shù)據(jù)后 回到***步 進(jìn)行下一步書(shū)籍的內(nèi)容抓取和保存 
  6.  */ 
  7.  
  8. /** 
  9.  * 初始化入口 
  10.  */ 
  11. const contentListInit = async() => { 
  12.     //獲取書(shū)籍列表 
  13.     const list = await bookHelper.getBookList(bookListModel); 
  14.     if (!list) { 
  15.         logger.error('初始化查詢(xún)書(shū)籍目錄失敗'); 
  16.         return
  17.     } 
  18.     const res = await mapBookList(list); 
  19.     if (!res) { 
  20.         logger.error('抓取章節(jié)信息,調(diào)用 getCurBookSectionList() 進(jìn)行串行遍歷操作,執(zhí)行完成回調(diào)出錯(cuò),錯(cuò)誤信息已打印,請(qǐng)查看日志!'); 
  21.         return
  22.     } 
  23.     return res; 
  24. /** 
  25.  * 遍歷書(shū)籍目錄下的章節(jié)列表 
  26.  * @param {*} list  
  27.  */ 
  28. const mapBookList = (list) => { 
  29.     return new Promise((resolve, reject) => { 
  30.         async.mapLimit(list, 1, (series, callback) => { 
  31.             let doc = series._doc; 
  32.             getCurBookSectionList(doc, callback); 
  33.         }, (err, result) => { 
  34.             if (err) { 
  35.                 logger.error('書(shū)籍目錄抓取異步執(zhí)行出錯(cuò)!'); 
  36.                 logger.error(err); 
  37.                 reject(false); 
  38.                 return
  39.             } 
  40.             resolve(true); 
  41.         }) 
  42.     }) 
  43.  
  44. /** 
  45.  * 獲取單本書(shū)籍下章節(jié)列表 調(diào)用章節(jié)列表遍歷進(jìn)行抓取內(nèi)容 
  46.  * @param {*} series  
  47.  * @param {*} callback  
  48.  */ 
  49. const getCurBookSectionList = async(series, callback) => { 
  50.  
  51.     let num = Math.random() * 1000 + 1000; 
  52.     await sleep(num); 
  53.     let key = series.key
  54.     const res = await bookHelper.querySectionList(chapterListModel, { 
  55.         keykey 
  56.     }); 
  57.     if (!res) { 
  58.         logger.error('獲取當(dāng)前書(shū)籍: ' + series.bookName + ' 章節(jié)內(nèi)容失敗,進(jìn)入下一部書(shū)籍內(nèi)容抓取!'); 
  59.         callback(nullnull); 
  60.         return
  61.     } 
  62.     //判斷當(dāng)前數(shù)據(jù)是否已經(jīng)存在 
  63.     const bookItemModel = getModel(key); 
  64.     const contentLength = await bookHelper.getCollectionLength(bookItemModel, {}); 
  65.     if (contentLength === res.length) { 
  66.         logger.info('當(dāng)前書(shū)籍:' + series.bookName + '數(shù)據(jù)庫(kù)已經(jīng)抓取完成,進(jìn)入下一條數(shù)據(jù)任務(wù)'); 
  67.         callback(nullnull); 
  68.         return
  69.     } 
  70.     await mapSectionList(res); 
  71.     callback(nullnull); 

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

這里我們通過(guò)key 來(lái)給數(shù)據(jù)做分類(lèi),每次按照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) : 有的書(shū)籍可能有幾百個(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ò)的選擇。

 

  1. /** 
  2.  * 遍歷單條書(shū)籍下所有章節(jié) 調(diào)用內(nèi)容抓取方法 
  3.  * @param {*} list  
  4.  */ 
  5. const mapSectionList = (list) => { 
  6.     return new Promise((resolve, reject) => { 
  7.         async.mapLimit(list, 1, (series, callback) => { 
  8.             let doc = series._doc; 
  9.             getContent(doc, callback) 
  10.         }, (err, result) => { 
  11.             if (err) { 
  12.                 logger.error('書(shū)籍目錄抓取異步執(zhí)行出錯(cuò)!'); 
  13.                 logger.error(err); 
  14.                 reject(false); 
  15.                 return
  16.             } 
  17.             const bookName = list[0].bookName; 
  18.             const key = list[0].key
  19.  
  20.             // 以整體為單元進(jìn)行保存 
  21.             saveAllContentToDB(result, bookName, key, resolve); 
  22.  
  23.             //以每篇文章作為單元進(jìn)行保存 
  24.             // logger.info(bookName + '數(shù)據(jù)抓取完成,進(jìn)入下一部書(shū)籍抓取函數(shù)...'); 
  25.             // resolve(true); 
  26.  
  27.         }) 
  28.     }) 

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

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

 

  1. //保存出錯(cuò)的數(shù)據(jù)名稱(chēng) 
  2. const errorSpider = mongoose.Schema({ 
  3.     chapter: String, 
  4.     section: String, 
  5.     url: String, 
  6.     key: String, 
  7.     bookName: String, 
  8.     author: String, 
  9. }) 
  10. // 保存出錯(cuò)的數(shù)據(jù)名稱(chēng) 只保留key 和 bookName信息 
  11. const errorCollection = mongoose.Schema({ 
  12.     key: String, 
  13.     bookName: String, 
  14. }) 

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

總結(jié)

寫(xiě)這個(gè)項(xiàng)目 其實(shí)主要的難點(diǎn)在于程序穩(wěn)定性的控制,容錯(cuò)機(jī)制的設(shè)置,以及錯(cuò)誤的記錄,目前這個(gè)項(xiàng)目基本能夠?qū)崿F(xiàn)直接運(yùn)行 一次性跑通整個(gè)流程。 但是程序設(shè)計(jì)也肯定還存在許多問(wèn)題 ,歡迎指正和交流。

彩蛋

寫(xiě)完這個(gè)項(xiàng)目 做了一個(gè)基于React開(kāi)的前端網(wǎng)站用于頁(yè)面瀏覽 和一個(gè)基于koa2.x開(kāi)發(fā)的服務(wù)端, 整體技術(shù)棧相當(dāng)于是 React + Redux + Koa2 ,前后端服務(wù)是分開(kāi)部署的,各自獨(dú)立可以更好的去除前后端服務(wù)的耦合性,比如同一套服務(wù)端代碼,不僅可以給web端 還可以給 移動(dòng)端 ,app 提供支持。目前整個(gè)一套還很簡(jiǎn)陋,但是可以滿(mǎn)足基本的查詢(xún)?yōu)g覽功能。希望后期有時(shí)間可以把項(xiàng)目變得更加豐富。

  • 本項(xiàng)目地址 地址 : guwen-spider
  • 對(duì)應(yīng)前端 React + Redux + semantic-ui 地址 : guwen-react
  • 對(duì)應(yīng)Node端 Koa2.2 + mongoose 地址 : guwen-node

項(xiàng)目挺簡(jiǎn)單的 ,但是多了一個(gè)學(xué)習(xí)和研究 從前端到服務(wù)端的開(kāi)發(fā)的環(huán)境。

責(zé)任編輯:未麗燕 來(lái)源: SegmentFault
相關(guān)推薦

2010-04-07 11:36:56

JNCIP

2009-07-03 18:49:07

綜合布線(xiàn)工程實(shí)施

2011-04-01 15:22:12

Zabbix配置安裝

2009-11-04 14:45:18

接入網(wǎng)優(yōu)化

2009-04-27 16:04:47

Windows 7微軟操作系統(tǒng)

2009-09-09 18:07:49

CCNA考試資料

2019-11-11 09:33:09

戴爾

2009-08-28 14:49:19

DHCP服務(wù)器管理維護(hù)

2011-09-28 13:21:16

軟件項(xiàng)目

2009-11-25 17:24:42

無(wú)線(xiàn)路由器

2009-08-25 13:57:09

C#泛型集合類(lèi)型

2022-05-24 15:55:37

避障小車(chē)華為

2009-01-19 20:16:23

Oracle心得體會(huì)

2009-07-01 14:28:20

cisco1700路由器配置

2022-11-16 09:57:23

優(yōu)化接口

2021-10-27 16:28:55

鴻蒙開(kāi)發(fā)者大會(huì)華為

2009-07-28 12:52:50

ASP.NET coo

2009-09-03 09:39:42

思科CCIE認(rèn)證考試心得

2017-02-16 13:46:27

可視化工具數(shù)據(jù)庫(kù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)