先來看一下結果: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即可查看結果。