千家信息网

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

发表于:2024-11-11 作者:千家信息网编辑
千家信息网最后更新 2024年11月11日,这篇文章主要讲解了"怎么使用NodeJs爬虫抓取古代典籍",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"怎么使用NodeJs爬虫抓取古代典籍"吧!项目实
千家信息网最后更新 2024年11月11日怎么使用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爬虫抓取古代典籍这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

书籍 数据 章节 内容 目录 信息 数据库 方法 入口 单元 方式 链接 爬虫 数量 问题 查询 典籍 成功 三个 外层 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 ice服务器迷你被告上法庭 数据库的毕业论文题目 对数据库这门课程的感想 游戏软件开发工程师退休待遇 解说生存战争服务器 上海威力网络技术服务 怎么通过服务器管理办公电脑 网络安全大拿被判刑 最简单的amd服务器 局开展自查保障网络安全 世界各国网络安全排名 服务器型号有哪些 oracle数据库财务软件 排位为什么一直无法连接服务器 数据库实验百度电子商务 计算机网络技术的大学课程 服务器系统用火绒安全 反电诈网络安全教育简讯 防止数据库出现意外的方法 全国普法网络安全知识竞赛 房地一体不动产数据库建设 蓟州区信息网络技术诚信合作 服务器是64还是32查看 c 连接hive数据库 与服务器的网络通信出现问题 服务器一般都有几个网口 数据库产品安装检查包括 静安区网络技术咨询服务哪个好 数据库新技术笔记 为什么国内云服务器带宽低
0