国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

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

開發 前端
之前研究數據,零零散散的寫過一些數據抓取的爬蟲,不過寫的比較隨意。有很多地方現在看起來并不是很合理 這段時間比較閑,本來是想給之前的項目做重構的。后來 利用這個周末,索性重新寫了一個項目,就是本項目 guwen-spider。目前這個爬蟲還是比較簡單的類型的, 直接抓取頁面,然后在頁面中提取數據,保存數據到數據庫。

前言

之前研究數據,零零散散的寫過一些數據抓取的爬蟲,不過寫的比較隨意。有很多地方現在看起來并不是很合理 這段時間比較閑,本來是想給之前的項目做重構的。

后來 利用這個周末,索性重新寫了一個項目,就是本項目 guwen-spider。目前這個爬蟲還是比較簡單的類型的, 直接抓取頁面,然后在頁面中提取數據,保存數據到數據庫。

通過與之前寫的對比,我覺得難點在于整個程序的健壯性,以及相應的容錯機制。在昨天寫代碼的過程中其實也有反映, 真正的主體代碼其實很快就寫完了 ,花了大部分時間是在

做穩定性的調試, 以及尋求一種更合理的方式來處理數據與流程控制的關系。

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

背景

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

概述

本項目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 可以實現對這三個方法的運行流程進行控制,書籍目錄抓取完成將數據保存到數據庫,然后執行結果返回到主程序,如果運行成功 主程序則執行根據書籍列表對章節列表的抓取,同理對書籍內容進行抓取。

項目主入口

 

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

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

booklist.js

 

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

chapterlist.js

 

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

內容抓取的思考

書籍目錄抓取其實邏輯非常簡單,只需要使用async.mapLimit做一個遍歷就可以保存數據了,但是我們在保存內容的時候 簡化的邏輯其實就是 遍歷章節列表 抓取鏈接里的內容。但是實際的情況是鏈接數量多達幾萬 我們從內存占用角度也不能全部保存到一個數組中,然后對其遍歷,所以我們需要對內容抓取進行單元化。

普遍的遍歷方式 是每次查詢一定的數量,來做抓取,這樣缺點是只是以一定數量做分類,數據之間沒有關聯,以批量方式進行插入,如果出錯 則容錯會有一些小問題,而且我們想一本書作為一個集合單獨保存會遇到問題。因此我們采用第二種就是以一個書籍單元進行內容抓取和保存。

這里使用了 async.mapLimit(list, 1, (series, callback) => {}) 這個方法來進行遍歷,不可避免的用到了回調,感覺很惡心。async.mapLimit()的第二個參數可以設置同時請求數量。

 

  1. /*  
  2.  * 內容抓取步驟: 
  3.  * ***步得到書籍列表, 通過書籍列表查到一條書籍記錄下 對應的所有章節列表,  
  4.  * 第二步 對章節列表進行遍歷獲取內容保存到數據庫中  
  5.  * 第三步 保存完數據后 回到***步 進行下一步書籍的內容抓取和保存 
  6.  */ 
  7.  
  8. /** 
  9.  * 初始化入口 
  10.  */ 
  11. const contentListInit = async() => { 
  12.     //獲取書籍列表 
  13.     const list = await bookHelper.getBookList(bookListModel); 
  14.     if (!list) { 
  15.         logger.error('初始化查詢書籍目錄失敗'); 
  16.         return
  17.     } 
  18.     const res = await mapBookList(list); 
  19.     if (!res) { 
  20.         logger.error('抓取章節信息,調用 getCurBookSectionList() 進行串行遍歷操作,執行完成回調出錯,錯誤信息已打印,請查看日志!'); 
  21.         return
  22.     } 
  23.     return res; 
  24. /** 
  25.  * 遍歷書籍目錄下的章節列表 
  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('書籍目錄抓取異步執行出錯!'); 
  36.                 logger.error(err); 
  37.                 reject(false); 
  38.                 return
  39.             } 
  40.             resolve(true); 
  41.         }) 
  42.     }) 
  43.  
  44. /** 
  45.  * 獲取單本書籍下章節列表 調用章節列表遍歷進行抓取內容 
  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('獲取當前書籍: ' + series.bookName + ' 章節內容失敗,進入下一部書籍內容抓取!'); 
  59.         callback(nullnull); 
  60.         return
  61.     } 
  62.     //判斷當前數據是否已經存在 
  63.     const bookItemModel = getModel(key); 
  64.     const contentLength = await bookHelper.getCollectionLength(bookItemModel, {}); 
  65.     if (contentLength === res.length) { 
  66.         logger.info('當前書籍:' + series.bookName + '數據庫已經抓取完成,進入下一條數據任務'); 
  67.         callback(nullnull); 
  68.         return
  69.     } 
  70.     await mapSectionList(res); 
  71.     callback(nullnull); 

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

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

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

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

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

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

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

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

 

  1. /** 
  2.  * 遍歷單條書籍下所有章節 調用內容抓取方法 
  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('書籍目錄抓取異步執行出錯!'); 
  13.                 logger.error(err); 
  14.                 reject(false); 
  15.                 return
  16.             } 
  17.             const bookName = list[0].bookName; 
  18.             const key = list[0].key
  19.  
  20.             // 以整體為單元進行保存 
  21.             saveAllContentToDB(result, bookName, key, resolve); 
  22.  
  23.             //以每篇文章作為單元進行保存 
  24.             // logger.info(bookName + '數據抓取完成,進入下一部書籍抓取函數...'); 
  25.             // resolve(true); 
  26.  
  27.         }) 
  28.     }) 

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

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

 

  1. //保存出錯的數據名稱 
  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. // 保存出錯的數據名稱 只保留key 和 bookName信息 
  11. const errorCollection = mongoose.Schema({ 
  12.     key: String, 
  13.     bookName: String, 
  14. }) 

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

總結

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

彩蛋

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

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

責任編輯:未麗燕 來源: SegmentFault
相關推薦

2010-04-07 11:36:56

JNCIP

2009-07-03 18:49:07

綜合布線工程實施

2011-04-01 15:22:12

Zabbix配置安裝

2009-11-04 14:45:18

接入網優化

2009-09-09 18:07:49

CCNA考試資料

2009-04-27 16:04:47

Windows 7微軟操作系統

2019-11-11 09:33:09

戴爾

2009-08-28 14:49:19

DHCP服務器管理維護

2011-09-28 13:21:16

軟件項目

2009-08-25 13:57:09

C#泛型集合類型

2009-11-25 17:24:42

無線路由器

2009-01-19 20:16:23

Oracle心得體會

2022-05-24 15:55:37

避障小車華為

2009-07-01 14:28:20

cisco1700路由器配置

2022-11-16 09:57:23

優化接口

2009-07-28 12:52:50

ASP.NET coo

2021-10-27 16:28:55

鴻蒙開發者大會華為

2009-09-03 09:39:42

思科CCIE認證考試心得

2017-02-16 13:46:27

可視化工具數據庫
點贊
收藏

51CTO技術棧公眾號

国产aⅴ精品一区二区四区| 精品成人影院| 最新久久zyz资源站| 7777在线视频| 成人激情视频| 97精品国产aⅴ7777| 国产精品黄色片| 亚洲高清av在线| eeuss影院在线观看| 亚洲一区二区成人在线观看| av动漫在线观看| 国产一区二区在线看| 欧美亚洲另类在线一区二区三区| 亚洲最新av| 黄色日韩网站视频| 色一情一区二区三区四区| 国产欧美一级| 玖玖玖精品中文字幕| 亚洲三级电影在线观看| 51国偷自产一区二区三区| 99精品综合| 成人网在线免费看| 国产精品激情电影| 成人网在线免费观看| 中文在线手机av| 99精品久久只有精品| 成人自拍性视频| 91中文在线| 国产精品久久久久久久久搜平片| 久久精品ww人人做人人爽| 2023国产精华国产精品| 亚洲精品一区二三区不卡| 狠狠色噜噜狠狠色综合久| 精品动漫av| 欧美人与物videos另类| 久久99久久99精品免视看婷婷 | 国产精品极品美女在线观看免费| 真实国产乱子伦对白视频| 美女精品一区二区| 可以在线看的av网站| 国产精品免费观看视频| 亚州色图欧美色图| 免费久久久一本精品久久区| 美美哒免费高清在线观看视频一区二区| 亚洲精品国产精品国自产观看| av资源久久| 日韩亚洲第一页| 日本aa大片在线播放免费看| 日本在线一区二区三区| 欧美黑人巨大xxx极品| 欧美巨大xxxx| 亚洲永久免费观看| 久久97超碰国产精品超碰| 国产成a人亚洲精v品在线观看| 国产精品亚洲视频| www.日本在线播放| 中文字幕一区二区三区在线观看| 日韩欧美亚洲| 亚洲视频一区二区免费在线观看| 先锋影音资源999| 久久久亚洲精品一区二区三区| 日本韩国福利视频| 国产乱妇无码大片在线观看| 精品国产乱码久久久久久108| 亚洲一区二区三区免费在线观看 | 极品一区美女高清| 欧美精品久久久久久久久| 亚洲欧美在线专区| 国产欧美一区二区精品久导航| 一级片在线视频| 欧美精品三级在线观看| 久久久久黄色| 欧美在线视频观看| 国产亚洲一卡2卡3卡4卡新区| 亚洲影视九九影院在线观看| 日韩综合小视频| 最新视频 - x88av| 日韩美女啊v在线免费观看| 在线成人动漫| 日韩免费在线观看| 尤物tv在线精品| 裸体大乳女做爰69| 天天综合日日夜夜精品| 欧美成人免费全部网站| 久久99精品国产.久久久久久| 欧美日本视频在线观看| 国产精一区二区三区| 国产精品字幕| 在线精品亚洲一区二区不卡| av成人 com a| 91高清免费视频| 国产在线精品一区二区三区不卡| 最新中文字幕2018| 亚洲精品不卡在线| 日韩激情毛片| 韩国无码av片在线观看网站| 亚洲成a天堂v人片| 国产成年精品| 国产传媒一区| 亚洲欧美在线高清| av观看在线| 爱福利视频一区| 色资源网在线观看| 日韩欧美三级在线| 制服丝袜日韩| 青春草在线视频免费观看| 欧美性大战久久久久久久蜜臀| 亚洲精品555| 日本一区二区在线视频观看| 中文字幕亚洲欧美在线不卡| 91福利精品在线观看| 91精品国自产在线观看| 成人免费av| 国产91对白刺激露脸在线观看| 91精品国产综合久久精品麻豆 | 欧美va亚洲va日韩∨a综合色| 久久这里只有精品18| 色播久久人人爽人人爽人人片视av| 99久久激情| 在线视频你懂| 最近2019免费中文字幕视频三| 国产一区不卡视频| 久久久久久蜜桃一区二区| 日韩限制级电影在线观看| 精品欧美日韩精品| 精品免费在线观看| 免费av网站在线看| 成人免费视频网址| 欧美唯美清纯偷拍| 欧美另类激情| 欧美无砖专区免费| 日韩在线播放视频| 国产精品久久久久久模特| 国产一区二区三区国产精品| 在线视频成人| 欧美日韩中文| 在线观看免费av网| 欧美在线观看网站| 亚洲三级视频在线观看| 国产亚洲精aa在线看| 蜜臀av午夜一区二区三区| 国产一区二区三区视频在线观看| 丁香婷婷综合激情五月色| 国产蜜臀av在线播放| 亚洲一区3d动漫同人无遮挡 | 亚洲成色www久久网站| 色94色欧美sute亚洲线路二| 你懂的一区二区三区| 最近中文字幕2019第二页视频| 久久久成人精品视频| 国产一区在线不卡| 少妇淫片在线影院| 椎名由奈jux491在线播放| 亚洲欧美另类中文字幕| 国产在线视频精品一区| av免费看在线| 久久精品99| 亚洲第一精品夜夜躁人人躁| 国产精品资源站在线| 亚洲三级欧美| 邪恶网站在线观看| 欧亚精品在线观看| 欧美日韩免费一区二区三区| 久久国产精品亚洲77777| 国产成人77亚洲精品www| 欧美成人三级在线视频| 久久露脸国产精品| 亚洲精品视频观看| 亚洲深爱激情| 极品在线视频| av电影免费| 91美女高潮出水| 精品无人区乱码1区2区3区在线| 久久99精品久久久久婷婷| 亚洲国产中文在线| 日本在线免费观看视频| 日本一区二区在线视频| 亚洲人成网7777777国产| 亚洲精品一二三| 亚洲欧美久久久| 日韩欧美中文在线观看| 国产毛片在线看| 精品久久久久久久久久中文字幕| 国产精品一区=区| 亚洲级视频在线观看免费1级| 国产精品色哟哟| 麻豆精品网站| 911亚洲精品| 午夜视频在线看| 美女黄色片视频| 日本一区二区久久精品| 555www成人网| 亚洲精品福利视频| 欧美日韩亚洲网| 91看片淫黄大片一级在线观看| 98精品久久久久久久| 高清久久精品| 欧美人与牲禽动交com| 九七影院理论片| 国产精品三级一区二区|