node.js入门(express和superagent的使用)爬博客园和知乎数据,并实时显示到前端
先来看一下结果:http://39.105.101.122:81/html/home.html
用到的东西:前端(H5盒子模型+vue+jquery)后端node.js(express+superagent),这里没有用到数据库和其他存储数据的文件。前端发送ajax请求,后端用express接收请求,然后通过superagent去请求博客园或者知乎的网站,拿到数据之后看情况对数据进行处理,然后再将数据返回给前端。
之前做过快半年的前端实习,主要是做H5方面的。毕业之后在新的公司里面主要是写java的爬虫,用的是htmlunit。这个东西相当于是用代码模拟浏览器来访问网站,可以模拟点击按钮的操作,用起来还是非常方便的。后来就想学一下node.js,看看用它是怎么来写后端,怎么写爬虫的。
首先是下载并安装node.js,新建一个文件夹作为一个新项目,进入这个文件夹,按住Shift键然后点击鼠标右键,点击“在此处打开命令窗口”。这样省去了切换目录的麻烦。然后在命令窗口里面输入npm init,回车,看情况填写信息,也可以一路回车默认。然后安装这里用到的几个模块:express、superagent、querystring、cheerio、eventproxy,比如在命令窗口输入npm install express --save,然后回车就可以在当前项目内安装express模块。安装完这几个模块之后,在当前目录下新建一个js文件:server.js,里面存放后台的代码,然后在当前目录下新建一个文件夹webapp,里面存放前端的东西:html、css、js、图片、还有其他用到的vue、jquery。
express的作用是可以创建一个后台的服务器,接收来自前端的请求,并且发送数据给前端。
用法如下:
var express = require('express'); var app = express(); app.use(express.static('webapp')); var server = app.listen(8081, function () { var host = server.address().address var port = server.address().port console.log("应用实例,访问地址为 http://%s:%s", host, port) })
app.use(express.static('webapp'));加上这句代码,浏览器里面就可以访问webapp这个文件夹里面的内容了。在命令窗口里面输入node server.js然后回车,在浏览器里面输入http://localhost:8081/webapp里面的内容就可以访问了。
接下来看一下前后端通信的代码是怎样的:
前端的请求如下所示:
$.ajax({ url: 'http://你的ip地址:8081/getData', type: 'POST', data: { num: $this.num, }, success: function(data){ var tempData = data.ret; for( var i = 0; i < tempData.length; i++ ){ var tempItem = { img: tempData[i].itemCover, hasImg: tempData[i].itemCover==undefined?false:true, text: tempData[i].itemText, num: "139", title: tempData[i].itemTitle, url: tempData[i].itemLink } $this.items.push(tempItem); } $this.num++; $this.working = false; $this.loading = false; // localStorage.items=JSON.stringify($this.items); // localStorage.num=$this.num; } })
server.js里面对应接收这个请求的方法如下所示:
app.post('/getData', function (req, res) { req.on('data', function(data) { var currentData = ""+data; var tempData = qs.parse(currentData); var index = tempData.num; var postData = []; var tempData = { value: "s" }; postData.push(tempData); res.header("Access-Control-Allow-Origin", "*"); res.json({ success: 1, ret: postData }) }); })
使用req.on('data',function(data){...})就可以接收到前端post过来的数据,然后用querystring的parse方法可以将数据解析成json格式,这样就能读取相应的数据。然后用res.json({...})就可以将数据返回给前端。res.header("Access-Control-Allow-Origin", "*")这句代码可以解决跨域的问题,比如说如果没有这句代码的话,在浏览器里面用localhost访问页面,前端的ajax请求是不会成功的,因为他会提示跨域的错误,加上这句代码之后就可以访问成功。
接下来看一下怎样用superagent来爬数据:
superagent.get("https://www.cnblogs.com/").end(function(err,pre){ var $ = cheerio.load(pre.text); for( var i = 0; i < $(".post_item_body").length; i++ ){ ... } })
用superagent来请求博客园首页的网址,然后pre.text就是博客园首页的html内容了,但是由于它是字符串,所以这里需要用到cheerio.load方法。然后这个$就和jquery里面的$一样了,可以直接用jquery里面的方法来获取页面的元素。在这里呢,推荐看https://www.cnblogs.com/coco1s/p/4954063.html这篇博客,这里面详细的描述了怎样用superagent来爬取博客园的数据。
爬取博客园的数据是成功了,但是我还想试一下其他的网站,平时看知乎比较多,就爬知乎吧。但是我发现用之前爬取博客园的方法去爬知乎的话,返回的是一个登陆的页面。所以这里就需要把cookie添加进去了。
superagent.get("https://www.zhihu.com/") .set('Cookie': '...') .end(function(err,obj){ if(err){return; } var tempData = { value: obj.text, }; res.header("Access-Control-Allow-Origin", "*"); res.json({ success: 1, ret: tempData }) })
这样的话,首页是进去了,但是在浏览器里面下滑的话还会有很多条目出来的,要想爬到这部分后来加载出来的条目,就需要分析网页的请求了。在浏览器里面打开知乎,然后按F12进入控制台,点击network,然后在网页里面下滑,就会发现network里面有很多请求,其中有一条请求是这样的:
可以看到这条请求返回的数据是很多的,它应该就是我们需要的请求了。点击上面的Headers,然后找到请求头,如下所示:
将请求头里面的这些信息塞到superagent的set函数里面,并且将Headers下面的Request URL替换之前的url,如下所示:
superagent.get("https://www.zhihu.com/api/v3/feed/topstory?action_feed=True&limit=10&session_token=a28a2fdc4537d7d14cbd46bcaf16912a&action=down&after_id=9&desktop=true"") .set({ 'Cookie': '_zap=67cf877b-b4c4-4798-9e23-d3ef6553d2f9; q_c1=fae7f35742874cd2b0356a5c321d085f|1501321325000|1501321325000; aliyungf_tc=AQAAAPsawBFU6QgABtBbcXShs11WbLol; q_c1=fae7f35742874cd2b0356a5c321d085f|1512744706000|1501321325000; _xsrf=51039ae2b79b7ba81f1da8eba41f2b62; r_cap_id="MDc0MTM0MWZlNWY2NDc2Mzk1YTdhOWE0MWFkZDQzYjA=|1512744706|b5b2a49f82637c6408347fe8941ea3315f5f5be3"; cap_id="NjI5ZDM3YTIzY2ZjNDhhOWJlZWM2MjIxNjQ0MjNmOTE=|1512744706|210d6f4e2c44900b25afebff79b79d9aac13dda0"; d_c0="ABBCbcS6zQyPTk28d91f51hdLtLQ1xQPe3s=|1512744707"; l_n_c=1; z_c0=Mi4xRDVobkFRQUFBQUFBRUVKdHhMck5EQmNBQUFCaEFsVk5JUFVYV3dBaC1ZVktoUjFoRExNMEI1Mkp5a1BWSHpUaERn|1512744736|1d19657b025b92339fecc550c2fc8606f0f5ba48; __utma=155987696.856620421.1512833230.1512833230.1512833230.1; __utmc=155987696; __utmz=155987696.1512833230.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _xsrf=51039ae2b79b7ba81f1da8eba41f2b62', 'Host': 'www.zhihu.com', 'Referer': 'https://www.zhihu.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', 'x-api-version': '3.0.53', 'x-udid': 'ABBCbcS6zQyPTk28d91f51hdLtLQ1xQPe3s=', 'Connection': 'keep-alive', 'authorization': 'Bearer Mi4xRDVobkFRQUFBQUFBRUVKdHhMck5EQmNBQUFCaEFsVk5JUFVYV3dBaC1ZVktoUjFoRExNMEI1Mkp5a1BWSHpUaERn|1512744736|1d19657b025b92339fecc550c2fc8606f0f5ba48' }) .end(function(err,obj){ if(err){ return; } var tempData = { value: obj.text, }; res.header("Access-Control-Allow-Origin", "*"); res.json({ success: 1, ret: tempData }) })
其实看代码可以发现我并没有把accept、Accept-Encoding、Accept-Language这三项加进去,因为我发现把请求头里面所有的信息塞进去之后,会返回错误信息,然后我试着删除掉一些请求信息,结果发现把这三项删除之后就可以请求成功。不清楚原因是什么。
这样的请求返回的结果和浏览器里面看到的返回的结果是一样的,中文字符是是经过转义的,使用JSON.parse函数可以将这个经过转义的字符串变为可用的json对象。然后分析这个json对象,从里面取我们需要的数据。
完整的server.js代码如下所示:
var express = require('express'); var superagent = require('superagent'); var qs = require("querystring"); var cheerio = require('cheerio'); var eventproxy = require('eventproxy'); var ep = new eventproxy(); var app = express(); app.use(express.static('webapp')); app.post('/getData', function (req, res) { req.on('data', function(data) { var currentData = ""+data; var tempData = qs.parse(currentData); var index = tempData.num; console.log(tempData.num); var urlArr = []; for( var i = index*2-1; i <= index*2; i++ ){ var url = "http://www.cnblogs.com/?CategoryId=808&CategoryType=%22SiteHome%22&ItemListActionName=%22PostList%22&PageIndex="+i+"&ParentCategoryId=0"; urlArr.push(url); } var postData = []; var startTime = new Date().getTime(); for( var j = 0; j < urlArr.length; j++ ){ superagent.get(urlArr[j]).end(function(err,pre){ var $ = cheerio.load(pre.text); for( var i = 0; i < $(".post_item_body").length; i++ ){ var titleElement = $(".post_item_body>h3>a").eq(i); var title = titleElement.text(); var sourceUrl = titleElement.attr("href"); var textElement = $(".post_item_summary").eq(i); var text = textElement.text(); var Element = $(".post_item_summary").eq(i); var coverElement = Element.find('img'); var cover = ""; if( coverElement!=undefined&&coverElement!=null ){ if( coverElement.attr("src")==undefined ){ cover = undefined; }else{ cover = "https:"+coverElement.attr("src"); } } var tempData = { itemTitle: title, itemLink: sourceUrl, itemText: text, itemCover: cover } postData.push(tempData); } ep.emit("dataEvent"); }) } ep.after('dataEvent',2,function(){ res.header("Access-Control-Allow-Origin", "*"); res.json({ success: 1, ret: postData }) var endTime = new Date().getTime(); var time = endTime-startTime; console.log(time); }); }); }) app.post('/getZhihuData', function (req, res) { req.on('data', function(data) { var currentData = ""+data; var tempData = qs.parse(currentData); var index = tempData.num; console.log(tempData.num); var postData = []; var startTime = new Date().getTime(); var afterId = index*10-1; var url = "https://www.zhihu.com/api/v3/feed/topstory?action_feed=True&limit=10&session_token=a28a2fdc4537d7d14cbd46bcaf16912a&action=down&after_id="+afterId+"&desktop=true"; superagent.get(url) .set({ 'Cookie': '_zap=67cf877b-b4c4-4798-9e23-d3ef6553d2f9; q_c1=fae7f35742874cd2b0356a5c321d085f|1501321325000|1501321325000; aliyungf_tc=AQAAAPsawBFU6QgABtBbcXShs11WbLol; q_c1=fae7f35742874cd2b0356a5c321d085f|1512744706000|1501321325000; _xsrf=51039ae2b79b7ba81f1da8eba41f2b62; r_cap_id="MDc0MTM0MWZlNWY2NDc2Mzk1YTdhOWE0MWFkZDQzYjA=|1512744706|b5b2a49f82637c6408347fe8941ea3315f5f5be3"; cap_id="NjI5ZDM3YTIzY2ZjNDhhOWJlZWM2MjIxNjQ0MjNmOTE=|1512744706|210d6f4e2c44900b25afebff79b79d9aac13dda0"; d_c0="ABBCbcS6zQyPTk28d91f51hdLtLQ1xQPe3s=|1512744707"; l_n_c=1; z_c0=Mi4xRDVobkFRQUFBQUFBRUVKdHhMck5EQmNBQUFCaEFsVk5JUFVYV3dBaC1ZVktoUjFoRExNMEI1Mkp5a1BWSHpUaERn|1512744736|1d19657b025b92339fecc550c2fc8606f0f5ba48; __utma=155987696.856620421.1512833230.1512833230.1512833230.1; __utmc=155987696; __utmz=155987696.1512833230.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _xsrf=51039ae2b79b7ba81f1da8eba41f2b62', 'Host': 'www.zhihu.com', 'Referer': 'https://www.zhihu.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', 'x-api-version': '3.0.53', 'x-udid': 'ABBCbcS6zQyPTk28d91f51hdLtLQ1xQPe3s=', 'Connection': 'keep-alive', 'authorization': 'Bearer Mi4xRDVobkFRQUFBQUFBRUVKdHhMck5EQmNBQUFCaEFsVk5JUFVYV3dBaC1ZVktoUjFoRExNMEI1Mkp5a1BWSHpUaERn|1512744736|1d19657b025b92339fecc550c2fc8606f0f5ba48' }) .end(function(err,obj){ if(err){ var X = JSON.parse(err); var Y = JSON.stringify(X); console.log(Y); console.log("err"); return; } var tempData = { value: obj.text, }; res.header("Access-Control-Allow-Origin", "*"); res.json({ success: 1, ret: tempData }) }) }); }) var server = app.listen(8081, function () { var host = server.address().address var port = server.address().port console.log("应用实例,访问地址为 http://%s:%s", host, port) })
在这里面还用到了eventproxy这个东西,因为node.js可以异步执行,所以在一个循环里面使用的superagent也是异步执行的,它不会等当前循环执行完之后再执行下一个循环,所以在这种情况下,我们不知道这个for循环里面的所有的superagent什么时候可以执行完。所以就有了eventproxy这个东西。它相当于是一个监听器,在一个循环的末尾使用ep.emit("dataEvent");这个函数,然后就可以触发ep.after函数,在触发的次数达到设定的次数之后,after函数里面的代码就会执行。
这里前端的细节就不再赘述了,主要实现的功能就是前端请求数据的时候,后端就去爬取数据并返回给前端,没有用到数据库或者是存储数据的文件。前端也实现了上划加载,loading动画的功能是我之前写过的一个loading动画,突然想到可以用到这里就搬来了。loading动画可以看这里:http://www.cnblogs.com/huizit1/p/5470587.html
前端除了vue和jquery就没有使用其他的插件或者库了。完整的代码在这里:https://github.com/swang23/NodeWebParser
项目代码克隆到本地之后,要想跑起来需要在home.js里面把ajax请求的url里面的ip地址换成自己电脑的ip地址(命令窗口里面:ipconfig回车查看ipv4地址),然后node server.js,在浏览器里面输入http://localhost:8081/html/home.html即可查看结果。