c爬虫抓取网页数据(【】一个一级-spider页面())
优采云 发布时间: 2022-02-28 22:18c爬虫抓取网页数据(【】一个一级-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 提供支持,还可以为移动和应用程序提供支持。目前整套还是很简单的,但是可以满足基本的查询和浏览功能。希望以后有时间让项目更加丰富。
该项目非常简单,但有一个额外的环境用于学习和研究从前端到服务器端的开发。