此前在做項目的時候,一直用json文件用作模擬數據,后來發現了mock.js,於是就用了mock.js,再后來感覺這些數據再怎么模擬都是靜態數據。所以就想用nodejs實現一個數據轉發功能,在本地拉取服務端的數據。那時就簡易做出了一個針對那個項目的數據拉取功能。而在最近,在看一些博客的時候,想把幾個博客的頁面內容全部拉取到一個頁面來看。所以就把此前數據拉取功能稍作改造封裝了一下。
做出的一個簡易數據拉取demo:點我看效果
然后大概簡述一下demo的實現。當作學習記錄。
首先是數據轉發模塊,我將其封裝了一下,封裝成了transdata.js
"use strict"; var http = require("http"); var stream = require("stream"); var url = require("url"); var zlib = require("zlib"); var noop = function () {}; //兩種請求 var transdata = { post: function (opt) { opt.method = "post"; main(opt); }, get: function (opt) { if (arguments.length >= 2 && (typeof arguments[0] == "string") && (typeof arguments[1] == "function")) { opt = { url: arguments[0], success: arguments[1] }; if (arguments[2] && (typeof arguments[2] == "function")) { opt.error = arguments[2]; } } opt.method = "get"; main(opt); } };
先是頭部這段代碼,就是簡單的做了一點封裝,封裝成了兩個方法,一個是get,一個是post,但是其實兩個最終調用的都是main方法。其中,opt則是要傳入的參數。參數包括了url、請求對象,響應對象等。
main方法如下
//轉發請求主要邏輯 function main(opt) { var options, creq; // res可以為response對象,也可以為一個可寫流,success和error為請求成功或失敗后的回調 opt.res = ((opt.res instanceof http.ServerResponse) || (opt.res instanceof stream.Writable)) ? opt.res : null; opt.success = (typeof opt.success == "function") ? opt.success : noop; opt.error = (typeof opt.error == "function") ? opt.error : noop; try { opt.url = (typeof opt.url == "string") ? url.parse(opt.url) : null; } catch (e) { opt.url = null; } if (!opt.url) { opt.error(new Error("url is illegal")); return; } options = { hostname: opt.url.hostname, port: opt.url.port, path: opt.url.pathname, method: opt.method, headers: { 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.37 Safari/537.36' } }; // 如果req為可讀流則使用pipe連接,傳輸數據,如果不是則直接寫出字符串 if (opt.method == 'post') { if (opt.req instanceof stream.Readable) { if(opt.req instanceof http.IncomingMessage){ options.headers["Content-Type"] = opt.req.headers["content-type"]; options.headers["Content-Length"] = opt.req.headers["content-length"]; } process.nextTick(function () { opt.req.pipe(creq); }) } else { var str = ((typeof opt.req) == "string") ? opt.req : ""; process.nextTick(function () { creq.end(str); }) } } else { process.nextTick(function () { creq.end(); }) } creq = http.request(options, function (res) { reqCallback(opt.res, res, opt.success) }).on('error', function (e) { opt.error(e); }); }
首先將opt參數進行一些錯誤處理。其中res可以為響應對象,也可以為一個可寫流。而success和error就是請求成功和失敗后的回調,同時再將url轉成對象方面后面使用。因為要在后台發起一個請求,所以請求的參數options是必須的啦。
寫好options后再判斷要轉發的請求是post還是get,如果是post而且傳入的參數req是一個請求頭或者可讀流,則直接使用pipe連接res,進行數據傳輸。如果req是string,則直接寫入。發起請求獲得響應后則調用reqCallback方法,對數據進行處理。
reqCallback方法如下:
//請求成功后的回調 function reqCallback(ores, res, callback) { if (ores) { ores.on('finish', function () { callback(); }); if (ores instanceof http.ServerResponse) { var options = {}; //復制響應頭信息 if (res.headers) { for (var k in res.headers) { options[k] = res.headers[k]; } } ores.writeHead(200, options); } res.pipe(ores); } else { var size = 0; var chunks = []; res.on('data', function (chunk) { size += chunk.length; chunks.push(chunk); }).on('end', function () { var buffer = Buffer.concat(chunks, size); //如果數據用gzip或者deflate壓縮,則用zlib進行解壓縮 if (res.headers && res.headers['content-encoding'] && res.headers['content-encoding'].match(/(\bdeflate\b)|(\bgzip\b)/)) { zlib.unzip(buffer, function (err, buffer) { if (!err) { callback(buffer.toString()) } else { console.log(err); callback(""); } }); } else { callback(buffer.toString()) } }) } }
對數據的處理比較簡單,如果res是響應對象,則直接通過pipe連接,如果不是,則獲取到數據,如果數據用gzip壓縮了,則用zlib進行解壓,然后放在回調中即可。
transdata的調用比較簡單,像get直接:
transdata.get(url , function(result){})
而我項目中用到的數據轉發的是用到post請求,也很簡單,直接:
var transdata = require("transdata"); var http = require("http"); http.createServer(function(req , res){ transdata.post({ req:req, url:'http://XXX/XX:9000/getdata', res:res, success:function(){ console.log("success"); }, error:function(e){ console.log("error"); } }); })
transdata寫完,再回到上面那個demo的實現上來,既然有了transdata,獲取數據就很容易了。代碼如下:
var creeper = function(req , res , urlObj){ var header = fs.readFileSync(baseDir + "header.ejs").toString(); var contents = fs.readFileSync(baseDir + "contents.ejs").toString(); var foot = fs.readFileSync(baseDir + "foot.ejs").toString(); res.writeHead(200 , {'content-type':'text/html;charset=utf-8'}); res.write(ejs.render(header , {data:ids})); console.log("開始采集數據..."); var count = 0; for(var i=0;i<ids.length;i++){ (function(index){ var id = ids[index]; var nowSource = source[id]; transdata.get(nowSource.url , function(result){ count++; console.log(">【"+id+ "】get√"); var $ = cheerio.load(result); var $colum = $(nowSource.colum); result = []; $colum.each(function(){ result.push(nowSource.handle($(this))) }); if(typeof +nowSource.max == "number"){result = result.slice(0 , nowSource.max)} if(result.length){ var data = {}; data[id] = result; result.index = index; var html = ejs.render(contents , {data:data}); html = html.replace(/(\r|\n)\s*/g , '').replace(/'/g , "\\'"); res.write("<script>loadHtml("+index+" , 'dom_"+index+"' , '"+html+"')</script>"); } if(count == ids.length){ console.log("數據采集完成.."); res.end(foot); } }) }(i)) } };
獲取到數據,數據為html信息,而處理html信息的工具就是cheerio,用法跟jquery的選擇器一樣,就用cheerio對數據進行操作並且獲取自己需要的數據,這些就不進行贅述。相對比較簡單。
整個項目源代碼的github地址附上:
https://github.com/whxaxes/node-test/tree/master/server/creeper
同時附上transdata.js的github地址:
https://github.com/whxaxes/transdata
有興趣的可以一看。