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

这篇文章主要讲解了“怎么使用NodeJs爬虫抓取古代典籍”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么使用NodeJs爬虫抓取古代典籍”吧!

为山东等地区用户提供了全套网页设计制作服务,及山东网站建设行业解决方案。主营业务为做网站、成都做网站、山东网站设计,以传统方式定制建设网站,并提供域名空间备案等一条龙服务,秉承以专业、用心的态度为用户提供真诚的服务。我们深信只要达到每一位用户的要求,就会得到认可,从而选择与我们长期合作。这样,我们也可以走得更远!

项目实现方案分析

项目是一个典型的多级抓取案例,目前只有三级,即 书籍列表, 书籍项对应的 章节列表,一个章节链接对应的内容。 抓取这样的结构可以采用两种方式, 一是  直接从外层到内层 内层抓取完以后再执行下一个外层的抓取,  还有一种就是先把外层抓取完成保存到数据库,然后根据外层抓取到所有内层章节的链接,再次保存,然后从数据库查询到对应的链接单元  对之进行内容抓取。这两种方案各有利弊,其实两种方式我都试过, 后者有一个好处,因为对三个层级是分开抓取的,  这样就能够更方便,尽可能多的保存到对应章节的相关数据。 可以试想一下 ,如果采用前者 按照正常的逻辑

对一级目录进行遍历抓取到对应的二级章节目录, 再对章节列表进行遍历 抓取内容,到第三级 内容单元抓取完成 需要保存时,如果需要很多的一级目录信息,就需要  这些分层的数据之间进行数据传递 ,想想其实应该是比较复杂的一件事情。所以分开保存数据 一定程度上避开了不必要的复杂的数据传递。

目前我们考虑到 其实我们要抓取到的古文书籍数量并不多,古文书籍大概只有180本囊括了各种经史。其和章节内容本身是一个很小的数据  ,即一个集合里面有180个文档记录。  这180本书所有章节抓取下来一共有一万六千个章节,对应需要访问一万六千个页面爬取到对应的内容。所以选择第二种应该是合理的。

项目实现

主程有三个方法 bookListInit ,chapterListInit,contentListInit,  分别是抓取书籍目录,章节列表,书籍内容的方法对外公开暴露的初始化方法。通过async  可以实现对这三个方法的运行流程进行控制,书籍目录抓取完成将数据保存到数据库,然后执行结果返回到主程序,如果运行成功  主程序则执行根据书籍列表对章节列表的抓取,同理对书籍内容进行抓取。

项目主入口

/**  * 爬虫抓取主入口  */ const start = async() => {     let booklistRes = await bookListInit();     if (!booklistRes) {         logger.warn('书籍列表抓取出错,程序终止...');         return;     }     logger.info('书籍列表抓取成功,现在进行书籍章节抓取...');      let chapterlistRes = await chapterListInit();     if (!chapterlistRes) {         logger.warn('书籍章节列表抓取出错,程序终止...');         return;     }     logger.info('书籍章节列表抓取成功,现在进行书籍内容抓取...');      let contentListRes = await contentListInit();     if (!contentListRes) {         logger.warn('书籍章节内容抓取出错,程序终止...');         return;     }     logger.info('书籍内容抓取成功'); } // 开始入口 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('开始抓取书籍章节列表,书籍目录共:' + 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('抓取章节信息,调用 getCurBookSectionList() 进行串行遍历操作,执行完成回调出错,错误信息已打印,请查看日志!');         return;     }     return res; }

内容抓取的思考

书籍目录抓取其实逻辑非常简单,只需要使用async.mapLimit做一个遍历就可以保存数据了,但是我们在保存内容的时候 简化的逻辑其实就是 遍历章节列表  抓取链接里的内容。但是实际的情况是链接数量多达几万 我们从内存占用角度也不能全部保存到一个数组中,然后对其遍历,所以我们需要对内容抓取进行单元化。

普遍的遍历方式 是每次查询一定的数量,来做抓取,这样缺点是只是以一定数量做分类,数据之间没有关联,以批量方式进行插入,如果出错  则容错会有一些小问题,而且我们想一本书作为一个集合单独保存会遇到问题。因此我们采用第二种就是以一个书籍单元进行内容抓取和保存。

这里使用了 async.mapLimit(list, 1, (series, callback) => {})  这个方法来进行遍历,不可避免的用到了回调,感觉很恶心。async.mapLimit()的第二个参数可以设置同时请求数量。

/*   * 内容抓取步骤:  * ***步得到书籍列表, 通过书籍列表查到一条书籍记录下 对应的所有章节列表,   * 第二步 对章节列表进行遍历获取内容保存到数据库中   * 第三步 保存完数据后 回到***步 进行下一步书籍的内容抓取和保存  */  /**  * 初始化入口  */ const contentListInit = async() => {     //获取书籍列表     const list = await bookHelper.getBookList(bookListModel);     if (!list) {         logger.error('初始化查询书籍目录失败');         return;     }     const res = await mapBookList(list);     if (!res) {         logger.error('抓取章节信息,调用 getCurBookSectionList() 进行串行遍历操作,执行完成回调出错,错误信息已打印,请查看日志!');         return;     }     return res; } /**  * 遍历书籍目录下的章节列表  * @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('书籍目录抓取异步执行出错!');                 logger.error(err);                 reject(false);                 return;             }             resolve(true);         })     }) }  /**  * 获取单本书籍下章节列表 调用章节列表遍历进行抓取内容  * @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 + ' 章节内容失败,进入下一部书籍内容抓取!');         callback(null, null);         return;     }     //判断当前数据是否已经存在     const bookItemModel = getModel(key);     const contentLength = await bookHelper.getCollectionLength(bookItemModel, {});     if (contentLength === res.length) {         logger.info('当前书籍:' + series.bookName + '数据库已经抓取完成,进入下一条数据任务');         callback(null, null);         return;     }     await mapSectionList(res);     callback(null, null); }

数据抓取完了 怎么保存是个问题

这里我们通过key 来给数据做分类,每次按照key来获取链接,进行遍历,这样的好处是保存的数据是一个整体,现在思考数据保存的问题

1、可以以整体的方式进行插入

优点 :速度快 数据库操作不浪费时间。

缺点 :有的书籍可能有几百个章节 也就意味着要先保存几百个页面的内容再进行插入,这样做同样很消耗内存,有可能造成程序运行不稳定。

2、可以以每一篇文章的形式插入数据库。

优点 :页面抓取即保存的方式 使得数据能够及时保存,即使后续出错也不需要重新保存前面的章节,

缺点 :也很明显 就是慢 ,仔细想想如果要爬几万个页面 做 几万次*N 数据库的操作 这里还可以做一个缓存器一次性保存一定条数  当条数达到再做保存这样也是一个不错的选择。

/**  * 遍历单条书籍下所有章节 调用内容抓取方法  * @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('书籍目录抓取异步执行出错!');                 logger.error(err);                 reject(false);                 return;             }             const bookName = list[0].bookName;             const key = list[0].key;              // 以整体为单元进行保存             saveAllContentToDB(result, bookName, key, resolve);              //以每篇文章作为单元进行保存             // logger.info(bookName + '数据抓取完成,进入下一部书籍抓取函数...');             // resolve(true);          })     }) }

两者各有利弊,这里我们都做了尝试。 准备了两个错误保存的集合,errContentModel, errorCollectionModel,在插入出错时  分别保存信息到对应的集合中,二者任选其一即可。增加集合来保存数据的原因是 便于一次性查看以及后续操作, 不用看日志。

(PS ,其实完全用 errorCollectionModel 这个集合就可以了 ,errContentModel这个集合可以完整保存章节信息)

//保存出错的数据名称 const errorSpider = mongoose.Schema({     chapter: String,     section: String,     url: String,     key: String,     bookName: String,     author: String, }) // 保存出错的数据名称 只保留key 和 bookName信息 const errorCollection = mongoose.Schema({     key: String,     bookName: String, })

我们将每一条书籍信息的内容 放到一个新的集合中,集合以key来进行命名。

感谢各位的阅读,以上就是“怎么使用NodeJs爬虫抓取古代典籍”的内容了,经过本文的学习后,相信大家对怎么使用NodeJs爬虫抓取古代典籍这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是创新互联,小编将为大家推送更多相关知识点的文章,欢迎关注!


名称栏目:怎么使用NodeJs爬虫抓取古代典籍
当前地址:http://ybzwz.com/article/geijpc.html