当前位置: 首页 > 后端技术 > Node.js

Node.js爬取科技新闻网站cnBeta(附前端和服务端源码)

时间:2023-04-03 11:22:18 Node.js

前言一直很喜欢看科技新闻,在cnBeta上了很多年。以前西贝的评论区是匿名的,所以评论区很活跃,各种喷,各种段子,不过真的很好玩。可以说,那是西贝最火的时候。然而,自从国家网信办去年推出《互联网跟帖评论服务管理规定》,要求只有实名认证的用户才能留言评论,原本活跃的评论区一下子崩塌,人气一落千丈。其实说到底,西贝是没有跟上移动互联网的潮流。至此止步于PC互联网时代。网页广告太多,手机应用质量堪忧,体验极差。虽然第三方应用很多,但由于缺乏官方支持,在体验上还是不够好。比如官方发布一些改版,第三方应用基本就挂掉了。所以,为了方便日常阅读cnBeta的新闻,我打算通过爬虫把cnBeta的新闻爬下来,自己建一个m站,这样体验可控,没有广告(`?′)ψ。其实这个项目早就完成了,现在才有时间(闲)写一篇文章分享。概述本项目爬虫和服务器github地址:https://github.com/hudingyu/c...前端github地址:https://github.com/hudingyu/c...技术细节使用asyncawait用于异步逻辑处理。使用async库做循环遍历和并发请求操作。使用log4js进行日志处理,使用cheerio进行新闻详情页的分析和抓取。使用mongoose连接mongoDB进行数据存储和操作。目录结构目录结构├──bin//入口│├──article-list.js//抓取新闻列表逻辑├──content.js//抓取新闻内容逻辑│├──server.js//服务器程序entry│└──spider.js//爬虫程序入口├──config//配置文件├──dbhelper//数据库操作方法目录├──middleware//koa2中间件├──model//mongoDBCollection操作示例├──router//koa2路由文件├──utils//工具函数├──package.json方案分析首先看爬虫程序的入口文件。整体逻辑其实很简单。首先抓取新闻列表并存储在MongoDB数据库中,每十分钟抓取一次。抓取新闻列表后,在数据库查询列表中查询没有新闻内容的新闻,开始抓取新闻详情,然后更新到数据库。constarticleListInit=require('./article-list');constarticleContentInit=require('./content');constlogger=require('../config/log');conststart=async()=>{让articleListRes=awaitarticleListInit();if(!articleListRes){logger.warn('新闻列表更新失败...');}else{logger.info('新闻列表更新成功!');}让articleContentRes=awaitarticleContentInit();if(!articleContentRes){logger.warn('文章内容抓取错误...');}else{logger.info('文章内容抓取成功!');}};if(typeofarticleListInit==='function'){start();}setInterval(start,600000);再看抓取新闻列表的逻辑,因为可以获取新闻列表的Ajax接口,所以直接调用接口获取列表信息。但也有一个问题。cnBeta新闻列表的缩略图和文章中的图片都有防盗链,不能在自己的网站上直接使用它的图片,所以我直接爬取了cnBeta图片文件。将其保存在您自己的服务器上。/***抓取文章列表的初始化方法*@returns{Promise.<*>}*/constarticleListInit=async()=>{logger.info('抓取文章列表开始...');constpageUrlList=getPageUrlList(listBaseUrl,totalPage);如果(!pageUrlList){返回;}让res=awaitgetArticleList(pageUrlList);returnres;}/***使用分页接口获取文章列表*@parampageUrlList*@returns{Promise}*/constgetArticleList=(pageUrlList)=>{returnnewPromise((resolve,reject)=>{async.mapLimit(pageUrlList,1,(pageUrl,callback)=>{getCurPage(pageUrl,callback);},(err,result)=>{if(err){logger.error('获取文章列表错误...');logger.error(err);reject(false);return;}letarticleList=_.flatten(result);downloadThumbAndSave(articleList,resolve);})})};/***获取当前文章列表页面*@parampageUrl*@param回调*@returns{Promise.}*/constgetCurPage=async(pageUrl,callback)=>{letnum=Math.random()*1000+1000;等待睡眠(数量);request(pageUrl,(err,response,body)=>{if(err){logger.info('当前url出错,url地址:'+pageUrl);callback(null,null);return;}else{letresponseObj=JSON.parse(body);if(responseObj.result&&responseObj.result.list){letnewsList=parseObject(articleModel,responseObj.result.list,{pubTime:'inputtime',author:'aid',commentCount:'comments',});callback(null,newsList);return;}console.log("出错了");callback(null,null);}});};constdownloadThumbAndSave=(list,resolve)=>{consthost='https://static.cnbetacdn.com';constbasepath='./public/data';如果(list.indexOf(null)>-1){resolve(false);}else{try{async.eachSeries(list,(item,callback)=>{letthumb_url=item.thumb.replace(host,'');item.thumb=thumb_url;if(!fs.exists(thumb_url)){mkDirs(basepath+thumb_url.substring(0,thumb_url.lastIndexOf('/')),()=>{request.get({url:host+thumb_url,}).pipe(fs.createWriteStream(path.join(basepath,thumb_url))).on('error',(err)=>{console.log("pipeerror",err);});callback(null,null);});}},(err,结果)=>{如果(!错误){saveDB(列表,解析);}});}catch(err){console.log(err);}}};/***将文章列表保存到数据库*@paramresult*@paramcallback*@returns{Promise.}*/constsaveDB=async(result,callback)=>{//console.log(结果);letflag=awaitdbHelper.insertCollection(articleDbModel,result).catch(function(err){logger.error('数据插入失败');});if(!flag){logger.error('新闻列表保存失败');}else{logger.info('listsaved!total:'+result.length);}if(typeofcallback==='function'){回调(true);}};我们再看一下抓取新闻内容的逻辑,这里是直接根据新闻的sid获取新闻内容页面的html,然后使用cheerio库解析来获取我们需要的新闻内容,当然,这里也是把文章中的图片抓取下来存到服务器中,把数据库中存的新闻内容中的图片链接替换成自己服务器中的url。/***抓取文章内容入口*@returns{Promise.<*>}*/constarticleContentInit=async()=>{logger.info('抓取文章内容开始...');让uncachedArticleSidList=awaitgetUncachedArticleList(articleDbModel);//console.log('未缓存文章:'+uncachedArticleSidList.join(','));constres=awaitbatchCrawlArticleContent(uncachedArticleSidList);if(!res){logger.error('抓取文章内容出错...');}returnres;};/***查询新闻列表获取sid列表*@paramModel*@returns{Promise.}*/constgetUncachedArticleList=async(Model)=>{constselectedArticleList=awaitdbHelper.queryDocList(Model).catch(function(err){logger.error(err);});返回selectedArticleList.map(item=>item.sid);//returnselectedArticleList.map(item=>item._doc.sid);};/***批量抓取新闻详情*@paramlist*@returns{Promise}*/constbatchCrawlArticleContent=(list)=>{return新的承诺((解决,重新ject)=>{async.mapLimit(list,3,(sid,callback)=>{getArticleContent(sid,callback);},(err,result)=>{if(err){logger.error(err);拒绝(假);返回;}解决(真);});});};/***获取单篇文章内容*@paramsid*@paramcallback*@returns{Promise.}*/constgetArticleContent=async(sid,callback)=>{letnum=Math.random()*1000+1000;等待睡眠(数量);让url=contentBaseUrl+sid+'.htm';request(url,(err,response,body)=>{if(err){logger.error('抓取文章内容出错,文章url:'+url);callback(null,null);return;}const$=cheerio.load(body,{decodeEntities:false});constserverAssetPath=`${serverIp}:${serverPort}/data`;letdomainReg=newRegExp('https://static.cnbetacdn.com','g');让文章={sid,source:$('.article-bylinespana').html()||$('.article-bylinespan').html(),摘要:$('.article-summp').html(),内容:$('.articleCont').html().replace(styleReg.reg,styleReg.replace).replace(scriptReg.reg,scriptReg.replace).replace(domainReg,serverAssetPath),};saveContentToDB(文章);让imgList=[];$('.articleContimg').each((index,dom)=>{imgList.push(dom.attribs.src);});下载Imgs(imgList);回调(空,空);});};/***下载图片*@paramlist*/constdownloadImgs=(list)=>{consthost='https://static.cnbetacdn.com';constbasepath='./public/data';如果(!list.length){返回;}try{async.eachSeries(list,(item,callback)=>{letnum=Math.random()*500+500;sleep(num);if(item.indexOf(host)===-1)返回;让thumb_url=item.replace(host,'');item.thumb=thumb_url;如果(!fs.exists(thumb_url)){mkDirs(basepath+thumb_url.substring(0,thumb_url.lastIndexOf('/')),()=>{request.get({url:host+thumb_url,})。pipe(fs.createWriteStream(path.join(basepath,thumb_url))).on("error",(err)=>{console.log("pipeerror",err);});callback(null,null);});}});}catch(err){console.log(err);}};/***保存到文章内容到数据库*@paramarticle*/constsaveContentToDB=(item)=>{letflag=dbHelper.updateCollection(articleDbModel,item);if(flag){logger.info('抓取文章内容成功:'+item.sid);}};爬虫部分差不多就是这样。还有一点就是我们自己的服务器上每天都有上百张爬取的图片。久而久之,图片占用的存储空间会非常大,所以我们需要定期清理它们。有兴趣的可以看看项目中的clear-expire.js文件。其实虽然整个项目并不复杂,但是在搭建一套前后端系统的过程中,收获颇多,需要解决的问题也很多。自己去实践和思考,也是性能优化考量的一个重要方面。下面截图是我最后完成的m站。界面很清爽,体验确实比cnBeta官网好多了。这样一来,平时看科技新闻确实方便很多。多于