文章來(lái)源:小青年原創(chuàng)
發(fā)布時(shí)間:2016-09-29
關(guān)鍵詞:JavaScript,nodejs嗡官,http寿羞,url ,Query String矫废,爬蟲(chóng)
轉(zhuǎn)載需標(biāo)注本文原始地址: http://zhaomenghuan.github.io/#!/blog/20160929
前言
一直以來(lái)想深入學(xué)習(xí)一下node盏缤,一來(lái)是自己目前也沒(méi)有什么時(shí)間去學(xué)習(xí)服務(wù)器端語(yǔ)言,但是有時(shí)候又想自己擼一下服務(wù)器端蓖扑,本著愛(ài)折騰的精神開(kāi)始寫(xiě)一寫(xiě)關(guān)于node的文章記錄學(xué)習(xí)心得唉铜。本系列文章不會(huì)過(guò)多去講解node安裝、基本API等內(nèi)容律杠,而是通過(guò)一些實(shí)例去總結(jié)常用用法潭流。本文主要講解node網(wǎng)絡(luò)操作的相關(guān)內(nèi)容,node中的網(wǎng)絡(luò)操作依賴(lài)于http模塊柜去,http模塊提供了兩種使用方式:
- 作為服務(wù)器端使用灰嫉,創(chuàng)建一個(gè)http服務(wù)器,監(jiān)聽(tīng)http客戶(hù)端請(qǐng)求并返回響應(yīng)诡蜓;
- 作為客戶(hù)端使用熬甫,發(fā)起一個(gè)http客戶(hù)端請(qǐng)求胰挑,獲取服務(wù)器端響應(yīng)蔓罚。
node http模塊創(chuàng)建服務(wù)器
node 處理 get 請(qǐng)求實(shí)例
畢竟作為一個(gè)前端椿肩,我們經(jīng)常需要自己搭建一個(gè)服務(wù)器做測(cè)試,這里我們先來(lái)講一下node http模塊作為服務(wù)器端使用豺谈。首先我們需要郑象,使用createServer創(chuàng)建一個(gè)服務(wù),然后通過(guò)listen監(jiān)聽(tīng)客服端http請(qǐng)求茬末。
我們可以創(chuàng)建一個(gè)最簡(jiǎn)單的服務(wù)器厂榛,在頁(yè)面輸出hello world
,我們可以創(chuàng)建helloworld.js丽惭,內(nèi)容如下:
var http = require('http');
http.createServer(function(request, response){
response.writeHead(200, { 'Content-Type': 'text-plain' });
response.end('hello world!')
}).listen(8888);
在命令行輸入node helloworld.js即可击奶,我們打開(kāi)在瀏覽器打開(kāi)http://127.0.0.1:8888/就可以看到頁(yè)面輸出hello world!。
下面我們?cè)诒镜貙?xiě)一個(gè)頁(yè)面责掏,通過(guò)jsonp訪問(wèn)我們創(chuàng)建的node服務(wù)器:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<div id="output"></div>
<script type="text/javascript">
// 創(chuàng)建script標(biāo)簽
function importScript(src){
var el = document.createElement('script');
el.src = src;
el.async = true;
el.defer = true;
document.body.appendChild(el);
}
// 響應(yīng)的方法
function jsonpcallback(rs) {
console.log(JSON.stringify(rs));
document.getElementById("output").innerHTML = JSON.stringify(rs);
}
// 發(fā)起get請(qǐng)求
importScript('http://127.0.0.1:8888?userid=xiaoqingnian&callback=jsonpcallback');
</script>
</body>
</html>
我們當(dāng)然需要將上述node服務(wù)器中的代碼稍作修改:
var http = require('http'); // 提供web服務(wù)
var url = require('url'); // 解析GET請(qǐng)求
var data = {
'name': 'zhaomenghuan',
'age': '22'
};
http.createServer(function(req, res){
// 將url字符串轉(zhuǎn)換成Url對(duì)象
var params = url.parse(req.url, true);
console.log(params);
// 查詢(xún)參數(shù)
if(params.query){
// 根據(jù)附件條件查詢(xún)
if(params.query.userid === 'xiaoqingnian'){
// 判斷是否為jsonp方式請(qǐng)求柜砾,若是則使用jsonp方式,否則為普通web方式
if (params.query.callback) {
var resurlt = params.query.callback + '(' + JSON.stringify(data) + ')';
res.end(resurlt);
} else {
res.end(JSON.stringify(data));
}
}
}
}).listen(8888);
我們?cè)诿钚锌梢钥吹剑?/p>
Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?userid=xiaoqingnian&callback=jsonpcallback',
query: { userid: 'xiaoqingnian', callback: 'jsonpcallback' },
pathname: '/',
path: '/?userid=xiaoqingnian&callback=jsonpcallback',
href: '/?userid=xiaoqingnian&callback=jsonpcallback' }
經(jīng)過(guò)服務(wù)器端jsonp處理换衬,然后返回一個(gè)函數(shù):
jsonpcallback({"name":"zhaomenghuan","age":"22"})
而我們?cè)陧?yè)面中定義了一個(gè)jsonpcallback()的方法痰驱,所以當(dāng)我們?cè)谡?qǐng)求頁(yè)面動(dòng)態(tài)生成script調(diào)用服務(wù)器地址,這樣相當(dāng)于在頁(yè)面執(zhí)行了下我們定義的函數(shù)瞳浦。jsonp的實(shí)現(xiàn)原理主要是script標(biāo)簽src可以跨域執(zhí)行代碼担映,類(lèi)似于你引用js庫(kù),然后調(diào)用這個(gè)js庫(kù)里面的方法叫潦;這是這里我們可以認(rèn)為反過(guò)來(lái)了蝇完,你是在本地定義函數(shù),調(diào)用的邏輯通過(guò)服務(wù)器返回的一個(gè)函數(shù)執(zhí)行了诅挑,所以jsonp并沒(méi)有什么神奇的四敞,和XMLHttpRequest、ajax半毛錢(qián)關(guān)系都沒(méi)有拔妥,而且JSONP需要服務(wù)器端支持忿危,始終是無(wú)狀態(tài)連接,不能獲悉連接狀態(tài)和錯(cuò)誤事件没龙,而且只能走GET的形式铺厨。
node 處理 post 請(qǐng)求實(shí)例
當(dāng)然這里我們可以直接在后臺(tái)設(shè)置響應(yīng)頭進(jìn)行跨域(CORS),如:
var http = require("http"); // 提供web服務(wù)
var query = require("querystring"); // 解析POST請(qǐng)求
http.createServer(function(req,res){
// 報(bào)頭添加Access-Control-Allow-Origin標(biāo)簽硬纤,值為特定的URL或"*"(表示允許所有域訪問(wèn)當(dāng)前域)
res.setHeader("Access-Control-Allow-Origin","*");
var postdata = '';
// 一旦監(jiān)聽(tīng)器被添加测秸,可讀流會(huì)觸發(fā) 'data' 事件
req.addListener("data",function(chunk){
postdata += chunk;
})
// 'end' 事件表明已經(jīng)得到了完整的 body
req.addListener("end",function(){
console.log(postdata); // 'appid=xiaoqingnian'
// 將接收到參數(shù)串轉(zhuǎn)換位為json對(duì)象
var params = query.parse(postdata);
if(params.userid == 'xiaoqingnian'){
res.end('{"name":"zhaomenghuan","age":"22"}');
}
})
}).listen(8080);
我們通過(guò)流的形式接收前端post傳遞的參數(shù),通過(guò)監(jiān)聽(tīng)data和end事件后裸,后面在講解event模塊的時(shí)候再深入探究序厉。
CORS默認(rèn)只支持GET/POST這兩種http請(qǐng)求類(lèi)型,如果要開(kāi)啟PUT/DELETE之類(lèi)的方式溪王,需要在服務(wù)端在添加一個(gè)"Access-Control-Allow-Methods"報(bào)頭標(biāo)簽:
res.setHeader(
"Access-Control-Allow-Methods",
"PUT, GET, POST, DELETE, HEAD, PATCH"
);
前端訪問(wèn)代碼如下:
var xhr = new XMLHttpRequest();
xhr.onload = function () {
console.log(this.responseText);
};
xhr.onreadystatechange = function() {
console.log(this.readyState);
};
xhr.open("post", "http://127.0.0.1:8080", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("userid=xiaoqingnian");
url 模塊API詳解
url.parse——解析url字符串
上述代碼中比較關(guān)鍵的是我們通過(guò)url.parse方法將url字符串轉(zhuǎn)成Url對(duì)象腮鞍,用法如下:
url.parse(urlStr, [parseQueryString], [slashesDenoteHost])
接收參數(shù):
- urlStr:url字符串
-
parseQueryString:參數(shù)為true時(shí)值骇,query會(huì)被解析為JSON格式,否則為普通字符串格式移国,默認(rèn)為false吱瘩;如:
- 參數(shù)為true:
query: { userid: 'xiaoqingnian', callback: 'jsonpcallback' }
- 參數(shù)為false:
query: 'userid=xiaoqingnian&callback=jsonpcallback'
- 參數(shù)為true:
- slashesDenoteHost:默認(rèn)為false,當(dāng)url是 ‘http://’ 或 ‘ftp://’ 等標(biāo)志的協(xié)議前綴打頭的迹缀,或直接以地址打頭使碾,如 ‘127.0.0.1’ 或 ‘localhost’ 時(shí)候是沒(méi)有區(qū)別的;當(dāng)且僅當(dāng)以2個(gè)斜杠打頭的時(shí)候祝懂,比如 ‘//127.0.0.1’ 才有區(qū)別票摇。這時(shí)候,如果其值為true砚蓬,則第一個(gè)單個(gè) ‘/’ 之前的部分被解析為 ‘host’ 和 ‘hostname’兄朋,如 ” host : ‘127.0.0.1’ “,如果為false怜械,包括2個(gè)反斜杠在內(nèi)的所有字符串被解析為pathname颅和。如:
> url.parse('//www.foo/bar',true,true)
Url {
protocol: null,
slashes: true,
auth: null,
host: 'www.foo',
port: null,
hostname: 'www.foo',
hash: null,
search: '',
query: {},
pathname: '/bar',
path: '/bar',
href: '//www.foo/bar' }
> url.parse('//www.foo/bar',true,false)
Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '',
query: {},
pathname: '//www.foo/bar',
path: '//www.foo/bar',
href: '//www.foo/bar' }
這里的URL對(duì)象和瀏覽器中的location對(duì)象類(lèi)似,location中如果我們需要使用類(lèi)似的方法缕允,我們需要自己構(gòu)造峡扩。
url.format——格式化URL對(duì)象
我們可以通過(guò)url.format方法將一個(gè)解析后的URL對(duì)象格式化成url字符串,用法為:
url.format(urlObj)
例子:
url.format({
protocol: 'http:',
slashes: true,
auth: 'user:pass',
host: 'host.com:8080',
port: '8080',
hostname: 'host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/p/a/t/h',
path: '/p/a/t/h?query=string',
href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'
})
結(jié)果為:
'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'
url.resolve——拼接url字符串
我們可以通過(guò)url.resolve為URL或 href 插入 或 替換原有的標(biāo)簽障本,接收參數(shù):
from源地址教届,to需要添加或替換的標(biāo)簽。
url.resolve(from, to)
例子為:
url.resolve('/one/two/three', 'four')
=> '/one/two/four'
url.resolve('http://example.com/', '/one')
=> 'http://example.com/one'
url.resolve('http://example.com/one', '/two')
=> 'http://example.com/two'
Query String 模塊Query String
querystring.escape——字符串編碼
querystring.escape('appkey=123&version=1.0.0+')
// 'appkey%3D123%26version%3D1.0.0%2B'
querystring.unescape——字符串解碼
querystring.unescape('appkey%3D123%26version%3D1.0.0%2B')
// 'appkey=123&version=1.0.0+'
querystring.stringify(querystring.encode)——序列化對(duì)象
querystring.stringify(obj[, sep][, eq][, options])
querystring.encode(obj[, sep][, eq][, options])
接收參數(shù):
- obj: 欲轉(zhuǎn)換的對(duì)象
- sep:設(shè)置分隔符驾霜,默認(rèn)為 ‘&'
- eq:設(shè)置賦值符案训,默認(rèn)為 ‘='
querystring.stringify({foo: 'bar', baz: ['qux', 'quux'], corge: ''})
// 'foo=bar&baz=qux&baz=quux&corge='
querystring.stringify({foo: 'bar', baz: ['qux', 'quux'], corge: ''},',',':')
// 'foo:bar,baz:qux,baz:quux,corge:'
querystring.parse(querystring.decode)——解析query字符串
querystring.parse(str[, sep][, eq][, options])
querystring.decode(str[, sep][, eq][, options])
接收參數(shù):
- str:欲轉(zhuǎn)換的字符串
- sep:設(shè)置分隔符,默認(rèn)為 ‘&'
- eq:設(shè)置賦值符粪糙,默認(rèn)為 ‘='
- [options] maxKeys 可接受字符串的最大長(zhǎng)度强霎,默認(rèn)為1000
querystring.parse('foo=bar&baz=qux&baz=quux&corge=')
// { foo: 'bar', baz: [ 'qux', 'quux' ], corge: '' }
querystring.parse('foo:bar,baz:qux,baz:quux,corge:',',',':')
{ foo: 'bar', baz: [ 'qux', 'quux' ], corge: '' }
node http模塊發(fā)起請(qǐng)求
平時(shí)喜歡看博客,畢竟買(mǎi)書(shū)要錢(qián)而且有時(shí)候沒(méi)有耐心讀完整本書(shū)蓉冈,所以很喜歡逛一些網(wǎng)站城舞,但是很多時(shí)候把所有的站逛一下又沒(méi)有那么多時(shí)間,哈哈寞酿,所以就準(zhǔn)備把常去的網(wǎng)站的文章爬出來(lái)做一個(gè)文章列表家夺,一來(lái)省去收集的時(shí)間,二來(lái)借此熟悉熟悉node相關(guān)的東西伐弹。這里我們首先看一個(gè)爬蟲(chóng)的小例子拉馋,下面以SF為例加以說(shuō)明(希望不要被封號(hào))。
http.request與http.get的區(qū)別
http.request(options, callback)
options可以是一個(gè)對(duì)象或一個(gè)字符串。如果options是一個(gè)字符串, 它將自動(dòng)使用url.parse()解析煌茴。http.request() 返回一個(gè) http.ClientRequest類(lèi)的實(shí)例柠逞。ClientRequest實(shí)例是一個(gè)可寫(xiě)流對(duì)象。如果需要用POST請(qǐng)求上傳一個(gè)文件的話景馁,就將其寫(xiě)入到ClientRequest對(duì)象。使用http.request()方法時(shí)都必須總是調(diào)用req.end()以表明這個(gè)請(qǐng)求已經(jīng)完成逗鸣,即使響應(yīng)body里沒(méi)有任何數(shù)據(jù)合住。如果在請(qǐng)求期間發(fā)生錯(cuò)誤(DNS解析、TCP級(jí)別的錯(cuò)誤或?qū)嶋HHTTP解析錯(cuò)誤)撒璧,在返回的請(qǐng)求對(duì)象會(huì)觸發(fā)一個(gè)'error'事件透葛。
Options配置說(shuō)明:
- host:請(qǐng)求發(fā)送到的服務(wù)器的域名或IP地址。默認(rèn)為'localhost'卿樱。
- hostname:用于支持url.parse()僚害。hostname比host更好一些
- port:遠(yuǎn)程服務(wù)器的端口。默認(rèn)值為80繁调。
- localAddress:用于綁定網(wǎng)絡(luò)連接的本地接口萨蚕。
- socketPath:Unix域套接字(使用host:port或socketPath)
- method:指定HTTP請(qǐng)求方法的字符串。默認(rèn)為'GET'蹄胰。
- path:請(qǐng)求路徑岳遥。默認(rèn)為'/'。如果有查詢(xún)字符串裕寨,則需要包含浩蓉。例如'/index.html?page=12'。請(qǐng)求路徑包含非法字符時(shí)拋出異常宾袜。目前捻艳,只否決空格,不過(guò)在未來(lái)可能改變庆猫。
- headers:包含請(qǐng)求頭的對(duì)象认轨。
- auth:用于計(jì)算認(rèn)證頭的基本認(rèn)證,即'user:password'
- agent:控制Agent的行為月培。當(dāng)使用了一個(gè)Agent的時(shí)候好渠,請(qǐng)求將默認(rèn)為Connection: keep-alive〗谑樱可能的值為:
- undefined(默認(rèn)):在這個(gè)主機(jī)和端口上使用[全局Agent][]拳锚。
- Agent對(duì)象:在Agent中顯式使用passed。
- false:在對(duì)Agent進(jìn)行資源池的時(shí)候寻行,選擇停用連接霍掺,默認(rèn)請(qǐng)求為:Connection: close。
- keepAlive:{Boolean} 保持資源池周?chē)奶捉幼衷谖磥?lái)被用于其它請(qǐng)求。默認(rèn)值為false
- keepAliveMsecs:{Integer} 當(dāng)使用HTTP KeepAlive的時(shí)候杆烁,通過(guò)正在保持活動(dòng)的套接字發(fā)送TCP KeepAlive包的頻繁程度牙丽。默認(rèn)值為1000。僅當(dāng)keepAlive被設(shè)置為true時(shí)才相關(guān)兔魂。
http.get(options, callback)
因?yàn)榇蟛糠值恼?qǐng)求是沒(méi)有報(bào)文體的GET請(qǐng)求烤芦,所以Node提供了這種便捷的方法。該方法與http.request()的唯一區(qū)別是它設(shè)置的是GET方法并自動(dòng)調(diào)用req.end()析校。
爬蟲(chóng)實(shí)例
這里我們使用es6的新特性寫(xiě):
const https = require('https');
https.get('https://segmentfault.com/blogs', (res) => {
console.log('statusCode: ', res.statusCode);
console.log('headers: ', res.headers);
var data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log(data);
})
}).on('error', (e) => {
console.error(e);
});
這樣一小段代碼我們就可以拿到segmentfault的博客頁(yè)面的源碼构罗,需要說(shuō)明的是因?yàn)檫@里請(qǐng)求的網(wǎng)站是https協(xié)議,所以我們需要引入https模塊智玻,用法同http一致遂唧。下面需要做的是解析html代碼,下面我們需要做的就是解析源碼吊奢,這里我們可以引入cheerio盖彭,一個(gè)node版的類(lèi)jQuery模塊,npm地址:https://www.npmjs.com/package/cheerio页滚。
首先第一步安裝:
npm install cheerio
然后就是將html代碼load進(jìn)來(lái)召边,如下:
var cheerio = require('cheerio'),
var $ = cheerio.load(html);
最后我們就是分析dom結(jié)構(gòu)咯,通過(guò)類(lèi)似于jQuery的方法獲取DOM元素的內(nèi)容裹驰,然后就將數(shù)據(jù)重新組裝成json結(jié)構(gòu)的數(shù)據(jù)掌实。這里就是分析源碼然后,這里我就不詳細(xì)分析了邦马,直接上代碼:
function htmlparser(html){
var baseUrl = 'https://segmentfault.com';
var $ = cheerio.load(html);
var bloglist = $('.stream-list__item');
var data = [];
bloglist.each(function(item){
var page = $(this);
var summary = page.find('.summary');
var blogrank = page.find('.blog-rank');
var title = summary.find('.title a').text();
var href = baseUrl + summary.find('.title a').attr('href');
var author = summary.find('.author li a').first().text().trim();
var origin = summary.find('.author li a').last().text().trim();
var time = summary.find('.author li span')[0].nextSibling.data.trim();
var excerpt = summary.find('p.excerpt').text().trim();
var votes = blogrank.find('.votes').text().trim();
var views = blogrank.find('.views').text().trim();
data.push({
title: title,
href: href,
author: author,
origin: origin,
time: time,
votes: votes,
views: views,
excerpt: excerpt
})
})
return data;
}
結(jié)果如下:
[{ title: '轉(zhuǎn)換流',
href: 'https://segmentfault.com/a/1190000007036273',
author: 'SwiftGG翻譯組',
origin: 'SwiftGG翻譯組',
time: '1 小時(shí)前',
votes: '0推薦',
views: '14瀏覽',
excerpt: '作者:Erica Sadun贱鼻,原文鏈接,原文日期:2016-08-29譯者:Darren滋将;校對(duì):shank
s邻悬;定稿:千葉知風(fēng) 我在很多地方都表達(dá)了我對(duì)流的喜愛(ài)。我在 Swift Cookbook 中介紹了一些∷婷觯現(xiàn)
在父丰,我將通過(guò) Pearson 的內(nèi)容更新計(jì)劃...' },
......
]
這里我們只是抓取了文章列表的一頁(yè),如果需要抓取多頁(yè)掘宪,只需要將內(nèi)容再次封裝一下蛾扇,傳入一個(gè)地址參數(shù)?page=2,如:https://segmentfault.com/blogs?page=2
另外我們也沒(méi)有將詳情頁(yè)進(jìn)一步爬蟲(chóng)魏滚,畢竟文章的目的只是學(xué)習(xí)镀首,同時(shí)方便自己查看列表,這里保留原始地址鼠次。
溫馨提示:大家不要都拿sf做測(cè)試哦更哄,不然玩壞了就不好芋齿。
模擬登陸
哈哈,寫(xiě)到這里已經(jīng)很晚了成翩,用node試了試模擬登陸SF觅捆,結(jié)果404,暫時(shí)沒(méi)有什么思路麻敌,等有時(shí)間再試試專(zhuān)門(mén)開(kāi)篇講解咯栅炒。這里推薦一篇之前看到的文章:記一次用 NodeJs 實(shí)現(xiàn)模擬登錄的思路。