成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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-04-27 16:04:47

Windows 7微軟操作系統

2009-09-09 18:07:49

CCNA考試資料

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

無線路由器

2022-05-24 15:55:37

避障小車華為

2009-01-19 20:16:23

Oracle心得體會

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技術棧公眾號

主站蜘蛛池模板: 一级免费看 | 色香婷婷| 日韩欧美一级片 | 国产一级毛片视频 | 日本h片在线观看 | 中文字幕免费在线观看 | 91一区二区三区在线观看 | 国产91久久久久 | 亚洲精品一区二区在线观看 | 中文字幕久久精品 | 狠狠干天天干 | 精精国产xxxx视频在线播放 | 亚洲视频在线播放 | av毛片 | 欧美极品一区二区 | 国产精品久久久亚洲 | 一区二区精品在线 | 伊人免费网 | 成人在线中文字幕 | 精品成人免费视频 | 四虎影院欧美 | 六月婷婷久久 | 国产精品日日摸夜夜添夜夜av | 色爽女| 在线国产中文字幕 | 在线观看www | 亚洲网站在线观看 | 欧美成人精品 | 国产一区二区视频在线观看 | 免费观看a级毛片在线播放 黄网站免费入口 | 日韩一区二区三区四区五区 | 免费观看一级特黄欧美大片 | 国产精品欧美一区二区三区 | 午夜激情网 | 在线日韩 | 午夜精品久久久久久不卡欧美一级 | 日韩亚洲欧美综合 | 在线一区观看 | 国产欧美日韩精品一区 | 一区二区三区四区视频 | 99热这里都是精品 |