c爬虫抓取网页数据(【】一个一级-spider页面())

优采云 发布时间: 2022-02-28 22:18

  c爬虫抓取网页数据(【】一个一级-spider页面())

  前言

  在研究数据之前,我零零散散地写了一些数据爬虫,但我写的比较随意。很多地方现在看起来不太合理。这次比较空闲,本来想重构之前的项目。

  后来利用这个周末简单的重新写了一个项目,就是这个项目guwen-spider。目前这种爬虫还是比较简单的类型,直接爬取页面,然后从页面中提取数据,将数据保存到数据库中。

  通过和我之前写的比较,我认为难点在于整个程序的健壮性和相应的容错机制。其实昨天在写代码的过程中就体现出来了。真正的主代码其实写得很快,大部分时间都花在了

  做稳定性调试,寻求更合理的方式处理数据与过程控制的关系。

  

  背景

  项目背景是抓取一级页面,即目录列表,点击目录进入章节和长度列表,点击章节或长度进入具体内容页面。

  概述

  本项目github地址:guwen-spider(PS:***脸上有彩蛋~~逃跑

  项目技术细节

  项目中大量使用了 ES7 的 async 函数,更直观的反映了程序的流程。为方便起见,在遍历数据的过程中直接使用了众所周知的 async 库,所以难免会用到回调 promise。因为数据处理发生在回调函数中,难免会遇到一些数据传输问题。,其实可以直接用ES7的async await写一个方法来实现同样的功能。其实这里最好的部分是使用Class的静态方法来封装数据库的操作。顾名思义,静态方法就像原型一样,不占用额外空间。

  该项目主要使用

  ES7 的 async await 协程做异步逻辑处理。使用 npm 的异步库进行循环遍历和并发请求操作。使用log4js做日志处理,使用cheerio处理dom操作。使用 mongoose 连接 mongoDB 进行数据存储和操作。

  目录结构

  项目实现计划分析

  条目是典型的多级爬取案例,目前只有书单、图书条目对应的章节列表、章节链接对应的内容三个等级。有两种方法可以获取这样的结构。一种是直接从外层抓取到内层,然后在内层抓取后执行下一个外层,另一种是先将外层保存到数据库中。,然后根据外层爬取所有内章的链接,再次保存,然后从数据库中查询对应的链接单元爬取内容。这两种方案各有优缺点。其实这两种方法我都试过了。后者有一个优势,因为三个层次是分开捕获的,以便尽可能多地保存相关章节。数据。你可以想象,如果按照正常逻辑使用前者

  遍历一级目录抓取对应的二级章节目录,然后遍历章节列表抓取内容。当三级内容单元被爬取,需要保存的时候,如果需要大量的一级目录信息,那么这些分层数据之间的数据传输,想想应该是比较复杂的事情。因此,单独保存数据在一定程度上避免了不必要的复杂数据传输。

  目前我们已经考虑到,我们想要采集的古籍数量并不是很多,涵盖各种经文和历史的古籍也只有大约180本。它和章节内容本身是一个很小的数据,即一个集合中有180条文档记录。这180本书的所有章节共有16000个章节,对应的需要访问16000个页面才能爬取到相应的内容。所以选择第二个应该是合理的。

  项目实现

  主流程有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 三个方法

  书单.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; }; 

  章节列表.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() 的第二个参数可以设置同时请求的数量。

  提取后如何保存数据是个问题

  这里我们通过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 和 error采集Model。插入错误时,将信息分别保存到相应的集合中。您可以选择两者之一。之所以添加集合保存数据,是为了方便一次性查看和后续操作,无需查看日志。

  (PS,其实error采集Model集合可以完全使用,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命名。

  总结

  其实这个项目写的主要难点在于程序稳定性的控制,容错机制的设置,以及错误的记录。目前这个项目基本上可以一次性直接运行整个流程。但是,程序设计上肯定还存在很多问题。欢迎指正和交流。

  复活节彩蛋

  写完这个项目,我做了一个基于React网站的页面浏览前端和一个基于koa2.x开发的服务器。整体技术栈相当于 React + Redux + Koa2,前后端服务分别部署,各自能够更好的去除前后端服务的耦合。例如,同一组服务器端代码不仅可以为 Web 提供支持,还可以为移动和应用程序提供支持。目前整套还是很简单的,但是可以满足基本的查询和浏览功能。希望以后有时间让项目更加丰富。

  该项目非常简单,但有一个额外的环境用于学习和研究从前端到服务器端的开发。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线