x1 工具準(zhǔn)備
工欲善其事必先利其器,爬取語(yǔ)料的根基便是基于python。
我們基于python3進(jìn)行開(kāi)發(fā),主要使用以下幾個(gè)模塊:requests、lxml、json。
簡(jiǎn)單介紹一個(gè)各模塊的功能
01|requests
requests是一個(gè)Python第三方庫(kù),處理URL資源特別方便。它的官方文檔上寫(xiě)著大大口號(hào):HTTP for Humans(為人類使用HTTP而生)。相比python自帶的urllib使用體驗(yàn),筆者認(rèn)為requests的使用體驗(yàn)比urllib高了一個(gè)數(shù)量級(jí)。
我們簡(jiǎn)單的比較一下:
urllib:
1import urllib2 2import urllib 3 4URL_GET = "https://api.douban.com/v2/event/list" 5#構(gòu)建請(qǐng)求參數(shù) 6params = urllib.urlencode({'loc':'108288','day_type':'weekend','type':'exhibition'}) 7 8#發(fā)送請(qǐng)求 9response = urllib2.urlopen('?'.join([URL_GET,'%s'])%params) 10#Response Headers 11print(response.info()) 12#Response Code 13print(response.getcode()) 14#Response Body 15print(response.read()) 復(fù)制代碼
requests:
1import requests 2 3URL_GET = "https://api.douban.com/v2/event/list" 4#構(gòu)建請(qǐng)求參數(shù) 5params = {'loc':'108288','day_type':'weekend','type':'exhibition'} 6 7#發(fā)送請(qǐng)求 8response = requests.get(URL_GET,params=params) 9#Response Headers 10print(response.headers) 11#Response Code 12print(response.status_code) 13#Response Body 14print(response.text)復(fù)制代碼
我們可以發(fā)現(xiàn),這兩種庫(kù)還是有一些區(qū)別的:
1. 參數(shù)的構(gòu)建:urllib需要對(duì)參數(shù)進(jìn)行urlencode編碼處理,比較麻煩;requests無(wú)需額外編碼處理,十分簡(jiǎn)潔。
2. 請(qǐng)求發(fā)送:urllib需要額外對(duì)url參數(shù)進(jìn)行構(gòu)造,變?yōu)榉弦蟮男问剑籸equests則簡(jiǎn)明很多,直接get對(duì)應(yīng)鏈接與參數(shù)。
3. 連接方式:看一下返回?cái)?shù)據(jù)的頭信息的“connection”,使用urllib庫(kù)時(shí),"connection":"close",說(shuō)明每次請(qǐng)求結(jié)束關(guān)掉socket通道,而使用requests庫(kù)使用了urllib3,多次請(qǐng)求重復(fù)使用一個(gè)socket,"connection":"keep-alive",說(shuō)明多次請(qǐng)求使用一個(gè)連接,消耗更少的資源
4. 編碼方式:requests庫(kù)的編碼方式Accept-Encoding更全,在此不做舉例
綜上所訴,使用requests更為簡(jiǎn)明、易懂,極大的方便我們開(kāi)發(fā)。
02|lxml
BeautifulSoup是一個(gè)庫(kù),而XPath是一種技術(shù),python中最常用的XPath庫(kù)是lxml。
當(dāng)我們拿到requests返回的頁(yè)面后,我們?cè)趺茨玫较胍臄?shù)據(jù)呢?這個(gè)時(shí)候祭出lxml這強(qiáng)大的HTML/XML解析工具。python從不缺解析庫(kù),那么我們?yōu)槭裁匆诒姸鄮?kù)里選擇lxml呢?我們選擇另一款出名的HTML解析庫(kù)BeautifulSoup來(lái)進(jìn)行對(duì)比。
我們簡(jiǎn)單的比較一下:
BeautifulSoup:
1from bs4 import BeautifulSoup #導(dǎo)入庫(kù) 2# 假設(shè)html是需要被解析的html 3 4#將html傳入BeautifulSoup 的構(gòu)造方法,得到一個(gè)文檔的對(duì)象 5soup = BeautifulSoup(html,'html.parser',from_encoding='utf-8') 6#查找所有的h4標(biāo)簽 7links = soup.find_all("h4") 復(fù)制代碼
lxml:
1from lxml import etree 2# 假設(shè)html是需要被解析的html 3 4#將html傳入etree 的構(gòu)造方法,得到一個(gè)文檔的對(duì)象 5root = etree.HTML(html) 6#查找所有的h4標(biāo)簽 7links = root.xpath("http://h4") 復(fù)制代碼
我們可以發(fā)現(xiàn),這兩種庫(kù)還是有一些區(qū)別的:
1. 解析html: BeautifulSoup的解析方式和JQ的寫(xiě)法類似,API非常人性化,支持css選擇器;lxml的語(yǔ)法有一定的學(xué)習(xí)成本
2. 性能:BeautifulSoup是基于DOM的,會(huì)載入整個(gè)文檔,解析整個(gè)DOM樹(shù),因此時(shí)間和內(nèi)存開(kāi)銷都會(huì)大很多;而lxml只會(huì)局部遍歷,另外lxml是用c寫(xiě)的,而B(niǎo)eautifulSoup是用python寫(xiě)的,明顯的性能上lxml>>BeautifulSoup。
綜上所訴,使用BeautifulSoup更為簡(jiǎn)明、易用,lxml雖然有一定學(xué)習(xí)成本,但總體也很簡(jiǎn)明易懂,最重要的是它基于C編寫(xiě),速度快很多,對(duì)于筆者這種強(qiáng)迫癥,自然而然就選lxml啦。
03|json
python自帶json庫(kù),對(duì)于基礎(chǔ)的json的處理,自帶庫(kù)完全足夠。但是如果你想更偷懶,可以使用第三方j(luò)son庫(kù),常見(jiàn)的有demjson、simplejson。
這兩種庫(kù),無(wú)論是import模塊速度,還是編碼、解碼速度,都是simplejson更勝一籌,再加上兼容性 simplejson 更好。所以大家如果想使用方庫(kù),可以使用simplejson。
0x2 確定語(yǔ)料源
將武器準(zhǔn)備好之后,接下來(lái)就需要確定爬取方向。
以電競(jìng)類語(yǔ)料為例,現(xiàn)在我們要爬電競(jìng)類相關(guān)語(yǔ)料。大家熟悉的電競(jìng)平臺(tái)有企鵝電競(jìng)、企鵝電競(jìng)和企鵝電競(jìng)(斜眼),所以我們以企鵝電競(jìng)上直播的游戲作為數(shù)據(jù)源進(jìn)行爬取。
我們登陸企鵝電競(jìng)官網(wǎng),進(jìn)入游戲列表頁(yè),可以發(fā)現(xiàn)頁(yè)面上有很多游戲,通過(guò)人工去寫(xiě)這些游戲名收益明顯不高,于是我們就開(kāi)始我們爬蟲(chóng)的第一步:游戲列表爬取。
1import requests 2from lxml import etree 3 4# 更新游戲列表 5def _updateGameList(): 6 # 發(fā)送HTTP請(qǐng)求時(shí)的HEAD信息,用于偽裝為瀏覽器 7 heads = { 8 'Connection': 'Keep-Alive', 9 'Accept': 'text/html, application/xhtml+xml, */*', 10 'Accept-Language': 'en-US,en;q=0.8,zh-Hans-CN;q=0.5,zh-Hans;q=0.3', 11 'Accept-Encoding': 'gzip, deflate', 12 'User-Agent': 'Mozilla/6.1 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko' 13 } 14 # 需要爬取的游戲列表頁(yè) 15 url = 'https://egame.qq.com/gamelist' 16 17 # 不壓縮html,最大鏈接時(shí)間為10妙 18 res = requests.get(url, headers=heads, verify=False, timeout=10) 19 # 為防止出錯(cuò),編碼utf-8 20 res.encoding = 'utf-8' 21 # 將html構(gòu)建為Xpath模式 22 root = etree.HTML(res.content) 23 # 使用Xpath語(yǔ)法,獲取游戲名 24 gameList = root.xpath("http://ul[@class='livelist-mod']//li//p//text()") 25 # 輸出爬到的游戲名 26 print(gameList) 復(fù)制代碼
當(dāng)我們拿到這幾十個(gè)游戲名后,下一步就是對(duì)這幾十款游戲進(jìn)行語(yǔ)料爬取,這時(shí)候問(wèn)題就來(lái)了,我們要從哪個(gè)網(wǎng)站來(lái)爬這幾十個(gè)游戲的攻略呢,taptap?多玩?17173?在對(duì)這幾個(gè)網(wǎng)站進(jìn)行分析后,發(fā)現(xiàn)這些網(wǎng)站僅有一些熱門(mén)游戲的文章語(yǔ)料,一些冷門(mén)或者低熱度的游戲,例如“靈魂籌碼”、“奇跡:覺(jué)醒”、“死神來(lái)了”等,很難在這些網(wǎng)站上找到大量文章語(yǔ)料,如圖所示:
我們可以發(fā)現(xiàn),“ 奇跡:覺(jué)醒”、“靈魂籌碼”的文章語(yǔ)料特別少,數(shù)量上不符合我們的要求。 那么有沒(méi)有一個(gè)比較通用的資源站,它擁有著無(wú)比豐富的文章語(yǔ)料,可以滿足我們的需求。
其實(shí)靜下心來(lái)想想,這個(gè)資源站我們天天都有用到,那就是百度。我們?cè)诎俣刃侣勊阉飨嚓P(guān)游戲,拿到搜索結(jié)果列表,這些列表的鏈接的網(wǎng)頁(yè)內(nèi)容幾乎都與搜索結(jié)果強(qiáng)相關(guān),這樣我們數(shù)據(jù)源不夠豐富的問(wèn)題便輕松解決了。但是此時(shí)出現(xiàn)了一個(gè)新的問(wèn)題,并且是一個(gè)比較難解決的問(wèn)題——如何抓取到任意網(wǎng)頁(yè)的文章內(nèi)容?
因?yàn)椴煌木W(wǎng)站都有不同的頁(yè)面結(jié)構(gòu),我們無(wú)法與預(yù)知將會(huì)爬到哪個(gè)網(wǎng)站的數(shù)據(jù),并且我們也不可能針對(duì)每一個(gè)網(wǎng)站都去寫(xiě)一套爬蟲(chóng),那樣的工作量簡(jiǎn)直難以想象!但是我們也不能簡(jiǎn)單粗暴的將頁(yè)面中的所有文字都爬下來(lái),用那樣的語(yǔ)料來(lái)進(jìn)行訓(xùn)練無(wú)疑是噩夢(mèng)!
經(jīng)過(guò)與各個(gè)網(wǎng)站斗智斗勇、查詢資料與思索之后,終于找到一條比較通用的方案,下面為大家講一講筆者的思路。
0x3 任意網(wǎng)站的文章語(yǔ)料爬取
01|提取方法
1)基于Dom樹(shù)正文提取
2)基于網(wǎng)頁(yè)分割找正文塊
3)基于標(biāo)記窗的正文提取
4)基于數(shù)據(jù)挖掘或機(jī)器學(xué)習(xí)
5)基于行塊分布函數(shù)正文提取
02|提取原理
大家看到這幾種是不是都有點(diǎn)疑惑了,它們到底是怎么提取的呢?讓筆者慢慢道來(lái)。
1)基于Dom樹(shù)的正文提取:
這一種方法主要是通過(guò)比較規(guī)范的HTML建立Dom樹(shù),然后地柜遍歷Dom,比較并識(shí)別各種非正文信息,包括廣告、鏈接和非重要節(jié)點(diǎn)信息,將非正文信息抽離之后,余下來(lái)的自然就是正文信息。
但是這種方法有兩個(gè)問(wèn)題
① 特別依賴于HTML的良好結(jié)構(gòu),如果我們爬取到一個(gè)不按W3c規(guī)范的編寫(xiě)的網(wǎng)頁(yè)時(shí),這種方法便不是很適用。
② 樹(shù)的建立和遍歷時(shí)間復(fù)雜度、空間復(fù)雜度都較高,樹(shù)的遍歷方法也因HTML標(biāo)簽會(huì)有不同的差異。
2) 基于網(wǎng)頁(yè)分割找正文塊 :
這一種方法是利用HTML標(biāo)簽中的分割線以及一些視覺(jué)信息(如文字顏色、字體大小、文字信息等)。
這種方法存在一個(gè)問(wèn)題:
① 不同的網(wǎng)站HTML風(fēng)格迥異,分割沒(méi)有辦法統(tǒng)一,無(wú)法保證通用性。
3) 基于標(biāo)記窗的正文提取:
先科普一個(gè)概念——標(biāo)記窗,我們將兩個(gè)標(biāo)簽以及其內(nèi)部包含的文本合在一起成為一個(gè)標(biāo)記窗(比如 <h1>我是h1</h1> 中的“我是h1”就是標(biāo)記窗內(nèi)容),取出標(biāo)記窗的文字。
這種方法先取文章標(biāo)題、HTML中所有的標(biāo)記窗,在對(duì)其進(jìn)行分詞。然后計(jì)算標(biāo)題的序列與標(biāo)記窗文本序列的詞語(yǔ)距離L,如果L小于一個(gè)閾值,則認(rèn)為此標(biāo)記窗內(nèi)的文本是正文。
這種方法雖然看上去挺好,但其實(shí)也是存在問(wèn)題的:
① 需要對(duì)頁(yè)面中的所有文本進(jìn)行分詞,效率不高。
② 詞語(yǔ)距離的閾值難以確定,不同的文章?lián)碛胁煌拈撝怠?/p>
4)基于數(shù)據(jù)挖掘或機(jī)器學(xué)習(xí)
使用大數(shù)據(jù)進(jìn)行訓(xùn)練,讓機(jī)器提取主文本。
這種方法肯定是極好的,但是它需要先有html與正文數(shù)據(jù),然后進(jìn)行訓(xùn)練。我們?cè)诖瞬贿M(jìn)行探討。
5)基于行塊分布函數(shù)正文提取
對(duì)于任意一個(gè)網(wǎng)頁(yè),它的正文和標(biāo)簽總是雜糅在一起。此方法的核心有亮點(diǎn):① 正文區(qū)的密度;② 行塊的長(zhǎng)度;一個(gè)網(wǎng)頁(yè)的正文區(qū)域肯定是文字信息分布最密集的區(qū)域之一,這個(gè)區(qū)域可能最大(評(píng)論信息長(zhǎng)、正文較短),所以同時(shí)引進(jìn)行塊長(zhǎng)度進(jìn)行判斷。
實(shí)現(xiàn)思路:
① 我們先將HTML去標(biāo)簽,只留所有正文,同時(shí)留下標(biāo)簽取出后的所有空白位置信息,我們稱其為Ctext;
② 對(duì)每一個(gè)Ctext取周圍k行(k<5),合起來(lái)稱為Cblock;
③ 對(duì)Cblock去掉所有空白符,其文字總長(zhǎng)度稱為Clen;
④ 以Ctext為橫坐標(biāo)軸,以各行的Clen為縱軸,建立坐標(biāo)系。
以這個(gè)網(wǎng)頁(yè)為例: http://www.gov.cn/ldhd/2009-11/08/content_1459564.htm 該網(wǎng)頁(yè)的正文區(qū)域?yàn)?45行至182行。
由上圖可知,正確的文本區(qū)域全都是分布函數(shù)圖上含有最值且連續(xù)的一個(gè)區(qū)域,這個(gè)區(qū)域往往含有一個(gè)驟升點(diǎn)和一個(gè)驟降點(diǎn)。因此,網(wǎng)頁(yè)正文抽取問(wèn)題轉(zhuǎn)化為了求行塊分布函數(shù)上的驟升點(diǎn)和驟降點(diǎn)兩個(gè)邊界點(diǎn),這兩個(gè)邊界點(diǎn)所含的區(qū)域包含了當(dāng)前網(wǎng)頁(yè)的行塊長(zhǎng)度最大值并且是連續(xù)的。
經(jīng)過(guò)大量實(shí)驗(yàn),證明此方法對(duì)于中文網(wǎng)頁(yè)的正文提取有較高的準(zhǔn)確度,此算法的優(yōu)點(diǎn)在于,行塊函數(shù)不依賴與HTML代碼,與HTML標(biāo)簽無(wú)關(guān),實(shí)現(xiàn)簡(jiǎn)單,準(zhǔn)確率較高。
主要邏輯代碼如下:
1# 假設(shè)content為已經(jīng)拿到的html 2 3# Ctext取周圍k行(k<5),定為3 4blocksWidth = 3 5# 每一個(gè)Cblock的長(zhǎng)度 6Ctext_len = [] 7# Ctext 8lines = content.split('n') 9# 去空格 10for i in range(len(lines)): 11 if lines[i] == ' ' or lines[i] == 'n': 12 lines[i] = '' 13# 計(jì)算縱坐標(biāo),每一個(gè)Ctext的長(zhǎng)度 14for i in range(0, len(lines) - blocksWidth): 15 wordsNum = 0 16 for j in range(i, i + blocksWidth): 17 lines[j] = lines[j].replace("\s", "") 18 wordsNum += len(lines[j]) 19 Ctext_len.append(wordsNum) 20# 開(kāi)始標(biāo)識(shí) 21start = -1 22# 結(jié)束標(biāo)識(shí) 23end = -1 24# 是否開(kāi)始標(biāo)識(shí) 25boolstart = False 26# 是否結(jié)束標(biāo)識(shí) 27boolend = False 28# 行塊的長(zhǎng)度閾值 29max_text_len = 88 30# 文章主內(nèi)容 31main_text = [] 32# 沒(méi)有分割出Ctext 33if len(Ctext_len) < 3: 34 return '沒(méi)有正文' 35for i in range(len(Ctext_len) - 3): 36 # 如果高于這個(gè)閾值 37 if(Ctext_len[i] > max_text_len and (not boolstart)): 38 # Cblock下面3個(gè)都不為0,認(rèn)為是正文 39 if (Ctext_len[i + 1] != 0 or Ctext_len[i + 2] != 0 or Ctext_len[i + 3] != 0): 40 boolstart = True 41 start = i 42 continue 43 if (boolstart): 44 # Cblock下面3個(gè)中有0,則結(jié)束 45 if (Ctext_len[i] == 0 or Ctext_len[i + 1] == 0): 46 end = i 47 boolend = True 48 tmp = [] 49 50 # 判斷下面還有沒(méi)有正文 51 if(boolend): 52 for ii in range(start, end + 1): 53 if(len(lines[ii]) < 5): 54 continue 55 tmp.append(lines[ii] + "n") 56 str = "".join(list(tmp)) 57 # 去掉版權(quán)信息 58 if ("Copyright" in str or "版權(quán)所有" in str): 59 continue 60 main_text.append(str) 61 boolstart = boolend = False 62# 返回主內(nèi)容 63result = "".join(list(main_text)) 復(fù)制代碼
0x4 結(jié)語(yǔ)
至此我們就可以獲取任意內(nèi)容的文章語(yǔ)料了,但這僅僅是開(kāi)始,獲取到了這些語(yǔ)料后我們還需要在一次進(jìn)行清洗、分詞、詞性標(biāo)注等,才能獲得真正可以使用的語(yǔ)料。
瀏覽器根據(jù)訪問(wèn)的域名找到其IP地址。DNS查找過(guò)程如下:
瀏覽器緩存:首先搜索瀏覽器自身的DNS緩存(緩存的時(shí)間比較短,大概只有1分鐘,且只能容納1000條緩存),看自身的緩存中是否是有域名對(duì)應(yīng)的條目,而且沒(méi)有過(guò)期,如果有且沒(méi)有過(guò)期則解析到此結(jié)束。
系統(tǒng)緩存:如果瀏覽器自身的緩存里面沒(méi)有找到對(duì)應(yīng)的條目,那么瀏覽器會(huì)搜索操作系統(tǒng)自身的DNS緩存,如果找到且沒(méi)有過(guò)期則停止搜索解析到此結(jié)束。
路由器緩存:如果系統(tǒng)緩存也沒(méi)有找到,則會(huì)向路由器發(fā)送查詢請(qǐng)求。
ISP(互聯(lián)網(wǎng)服務(wù)提供商) DNS緩存:如果在路由緩存也沒(méi)找到,最后要查的就是ISP緩存DNS的服務(wù)器。
TCP的3次握手。
一個(gè)HTTP請(qǐng)求報(bào)文由請(qǐng)求行(request line)、請(qǐng)求頭部(headers)、空行(blank line)和請(qǐng)求數(shù)據(jù)(request body)4個(gè)部分組成。
圖1 HTTP請(qǐng)求格式
1.3.1 請(qǐng)求行
請(qǐng)求行分為三個(gè)部分:請(qǐng)求方法、請(qǐng)求地址URL和HTTP協(xié)議版本,它們之間用空格分割。例如,GET /index.html HTTP/1.1。
1.請(qǐng)求方法
HTTP/1.1 定義的請(qǐng)求方法有8種:GET(完整請(qǐng)求一個(gè)資源)、POST(提交表單)、PUT(上傳文件)、DELETE(刪除)、PATCH、HEAD(僅請(qǐng)求響應(yīng)首部)、OPTIONS(返回請(qǐng)求的資源所支持的方法)、TRACE(追求一個(gè)資源請(qǐng)求中間所經(jīng)過(guò)的代理)。最常的兩種GET和POST,如果是RESTful接口的話一般會(huì)用到GET、POST、DELETE、PUT。
(1)GET
當(dāng)客戶端要從服務(wù)器中讀取文檔時(shí),當(dāng)點(diǎn)擊網(wǎng)頁(yè)上的鏈接或者通過(guò)在瀏覽器的地址欄輸入網(wǎng)址來(lái)瀏覽網(wǎng)頁(yè)的,使用的都是GET方式。GET方法要求服務(wù)器將URL定位的資源放在響應(yīng)報(bào)文的數(shù)據(jù)部分,會(huì)送給客戶端。
使用GET方法時(shí),請(qǐng)求參數(shù)和對(duì)應(yīng)的值附加在URL后面,利用一個(gè)問(wèn)號(hào)‘?’代表URL的結(jié)尾與請(qǐng)求參數(shù)的開(kāi)始,傳遞參數(shù)長(zhǎng)度受限制。例如,/index.jsp?id=100&op=bind。通過(guò)GET方式傳遞的數(shù)據(jù)直接放在地址中,所以GET方式的請(qǐng)求一般不包含“請(qǐng)求內(nèi)容”部分,請(qǐng)求數(shù)據(jù)以地址的形式表現(xiàn)在請(qǐng)求行。
地址中‘?’之后的部分就是通過(guò)GET發(fā)送的請(qǐng)求數(shù)據(jù),各個(gè)數(shù)據(jù)之間用‘&’符號(hào)隔開(kāi)。顯然這種方式不適合傳送私密數(shù)據(jù)。另外,由于不同的瀏覽器對(duì)地址的字符限制也有所不同,一般最多只能識(shí)別1024個(gè)字符,所以如果需要傳送大量數(shù)據(jù)的時(shí)候,也不適合使用GET方式。如果數(shù)據(jù)是英文字母/數(shù)字,原樣發(fā)送;如果是空格,轉(zhuǎn)換為+;如果是中文/其他字符,則直接把字符串用BASE64加密,得出:%E4%BD%A0%E5%A5%BD,其中%XX中的XX為該符號(hào)以16進(jìn)制表示的ASCII。
(2)POST
允許客戶端給服務(wù)器提供信息較多。POST方法將請(qǐng)求參數(shù)封裝在HTTP請(qǐng)求數(shù)據(jù)中,以名稱/值的形式出現(xiàn),可以傳輸大量數(shù)據(jù),這樣POST方式對(duì)傳送的數(shù)據(jù)大小沒(méi)有限制,而且也不會(huì)顯示在URL中。POST方式請(qǐng)求行中不包含數(shù)據(jù)字符串,這些數(shù)據(jù)保存在“請(qǐng)求內(nèi)容”部分,各數(shù)據(jù)之間也是使用‘&’符號(hào)隔開(kāi)。POST方式大多用于頁(yè)面的表單中。因?yàn)镻OST也能完成GET的功能,因此多數(shù)人在設(shè)計(jì)表單的時(shí)候一律都使用POST方式,其實(shí)這是一個(gè)誤區(qū)。GET方式也有自己的特點(diǎn)和優(yōu)勢(shì),我們應(yīng)該根據(jù)不同的情況來(lái)選擇是使用GET還是使用POST。
圖2 HTTP請(qǐng)求方法
2.URL
URL:統(tǒng)一資源定位符,是一種資源位置的抽象唯一識(shí)別方法。
組成:<協(xié)議>://<主機(jī)>:<端口>/<路徑>
端口和路徑有事可以省略(HTTP默認(rèn)端口號(hào)是80)
3.協(xié)議版本
協(xié)議版本的格式為:HTTP/主版本號(hào).次版本號(hào),常用的有HTTP/1.0和HTTP/1.1
1.3.2 請(qǐng)求頭部
請(qǐng)求頭部為請(qǐng)求報(bào)文添加了一些附加信息,由“名/值”對(duì)組成,每行一對(duì),名和值之間使用冒號(hào)分隔。
請(qǐng)求頭部的最后會(huì)有一個(gè)空行,表示請(qǐng)求頭部結(jié)束,接下來(lái)為請(qǐng)求數(shù)據(jù)。
1.3.3 請(qǐng)求數(shù)據(jù)
請(qǐng)求數(shù)據(jù)不在GET方法中使用,而在POST方法中使用。POST方法適用于需要客戶填寫(xiě)表單的場(chǎng)合。與請(qǐng)求數(shù)據(jù)相關(guān)的最長(zhǎng)使用的請(qǐng)求頭部是Cntent-Type和Content-Length。下面是一個(gè)POST方法的請(qǐng)求報(bào)文:
POST /index.php HTTP/1.1 請(qǐng)求行
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:10.0.2) Gecko/20100101 Firefox/10.0.2請(qǐng)求頭
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language: zh-cn,zh;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://localhost/
Content-Length:25
Content-Type:application/x-www-form-urlencoded
空行
username=aa&password=1234 請(qǐng)求數(shù)據(jù)
HTTP響應(yīng)報(bào)文由狀態(tài)行(status line)、相應(yīng)頭部(headers)、空行(blank line)和響應(yīng)數(shù)據(jù)(response body)4個(gè)部分組成。
1.4.1 狀態(tài)行
狀態(tài)行由3部分組成,分別為:協(xié)議版本、狀態(tài)碼、狀態(tài)碼掃描。其中協(xié)議版本與請(qǐng)求報(bào)文一致,狀態(tài)碼描述是對(duì)狀態(tài)碼的簡(jiǎn)單描述。
1.4.2 響應(yīng)頭部
1.4.3 響應(yīng)數(shù)據(jù)
用于存放需要返回給客戶端的數(shù)據(jù)信息。
HTTP/1.1 200 OK 狀態(tài)行
Date: Sun, 17 Mar 2013 08:12:54 GMT 響應(yīng)頭部
Server: Apache/2.2.8 (Win32) PHP/5.2.5
X-Powered-By: PHP/5.2.5
Set-Cookie: PHPSESSID=c0huq7pdkmm5gg6osoe3mgjmm3; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 4393
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=utf-8
空行
<html> 響應(yīng)數(shù)據(jù)
<head>
<title>HTTP響應(yīng)示例<title>
</head>
<body>
Hello HTTP!
</body>
</html>
瀏覽器拿到HTML文件后,開(kāi)始解析HTML代碼,遇到靜態(tài)資源時(shí),就向服務(wù)器端去請(qǐng)求下載。
瀏覽器利用自己內(nèi)部的工作機(jī)制,把請(qǐng)求到的靜態(tài)資源和HTML代碼進(jìn)行渲染,呈現(xiàn)給用戶。
來(lái)源:CSDN
我們這個(gè)Web服務(wù)器有了一個(gè)基本的門(mén)面以后,我們是時(shí)候來(lái)用它做點(diǎn)實(shí)際的事情了。還記得我們最早提到HTTP協(xié)議的用途是什么嗎?它叫超文本傳輸協(xié)議啊,所以我們必須考慮讓我們的服務(wù)器能夠接收到客戶端傳來(lái)的數(shù)據(jù)。因?yàn)槲覀兡壳巴瓿闪舜蟛糠值墓ぷ鳎詫?duì)數(shù)據(jù)傳輸這個(gè)問(wèn)題我們這里選擇以最簡(jiǎn)單的GET和POST為例來(lái)實(shí)現(xiàn),這樣我們今天的重點(diǎn)就落實(shí)在Get和Post的實(shí)現(xiàn)這個(gè)問(wèn)題上來(lái)。而從原理上來(lái)講,無(wú)論Get方式請(qǐng)求還是Post方式請(qǐng)求,我們都可以在請(qǐng)求報(bào)文中獲得其請(qǐng)求參數(shù),不同的是前者出現(xiàn)在請(qǐng)求行中,而后者出現(xiàn)在消息體中。例如我們傳遞的兩個(gè)參數(shù)num1和num2對(duì)應(yīng)的數(shù)值分別是12和24,那么在具體的請(qǐng)求報(bào)文中我們都能找到類似“num1=12&num2=24”這樣的字符結(jié)構(gòu),所以只要針對(duì)這個(gè)字符結(jié)構(gòu)進(jìn)行解析,就可以獲得客戶端傳遞給服務(wù)器的參數(shù)啦。
首先我們來(lái)實(shí)現(xiàn)Get請(qǐng)求,Get是HTTP協(xié)議中默認(rèn)的請(qǐng)求類型,我們平時(shí)訪問(wèn)網(wǎng)頁(yè)、請(qǐng)求資源實(shí)際上都是通過(guò)Get方式實(shí)現(xiàn)的。Get方式請(qǐng)求需要通過(guò)類似“?id=001&option=10”這樣的形式附加在URL上,因此Get方式對(duì)瀏覽器來(lái)說(shuō)是透明的,即用戶可以通過(guò)瀏覽器地址欄知道,這個(gè)過(guò)程中傳遞了哪些參數(shù)以及這些參數(shù)的值分別是什么。而由于瀏覽器的限制,我們通過(guò)這種方式請(qǐng)求的時(shí)候能夠傳遞的參數(shù)數(shù)目和長(zhǎng)度都是有限的,而且當(dāng)參數(shù)中存在中文數(shù)值的時(shí)候還需要對(duì)其進(jìn)行編碼。Get方式請(qǐng)求相對(duì)簡(jiǎn)單,我們下面來(lái)看看它的請(qǐng)求報(bào)文:
GET /?num1=23&num2=12 HTTP/1.1 Accept: text/html, application/xhtml+xml, image/jxr, */* Accept-Language: zh-Hans-CN,zh-Hans;q=0.5 User-Agent: Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586 Accept-Encoding: gzip, deflate Host: localhost:4040 Connection: Keep-Alive Cookie: _ga=GA1.1.1181222800.1463541781 1 2 3 4 5 6 7 8
此時(shí)我們可以注意到在請(qǐng)求報(bào)文第一行,即請(qǐng)求行中出現(xiàn)了“/?num1=23&num2=12”這樣的字樣,這就是客戶端傳遞給服務(wù)器的參數(shù),我們很容易想到只需要將這個(gè)字段串中的“鍵”和“值”都解析出來(lái),服務(wù)器就可以對(duì)這些數(shù)據(jù)進(jìn)行處理然后返回給客戶端了。所以下面我們通過(guò)這樣的方式來(lái)實(shí)現(xiàn),我們?yōu)镠tttpRequest類增加了一個(gè)Parms屬性,它是一個(gè)鍵和值均為字符串類型的字典,我們使用這個(gè)字典來(lái)存儲(chǔ)和管理客戶端傳遞來(lái)的參數(shù)。
//獲取請(qǐng)求參數(shù) if(this.Method == "GET" && this.URL.Contains('?')) this.Params = GetRequestParams(lines[0].Split(' ')[1].Split('?')[1]); 1 2 3
顯然我們首先需要判斷請(qǐng)求類型是否為GET以及請(qǐng)求中是否帶有參數(shù),其方法是判斷請(qǐng)求地址中是否含有“?”字符。這里的lines是指將報(bào)文信息按行分割以后的數(shù)組,顯然請(qǐng)求地址在第一行,所以我們根據(jù)“?”分割該行數(shù)據(jù)以后就可以得到“num1=23&num2=12”這樣的結(jié)果,這里我們使用一個(gè)方法GetRequestParms來(lái)返回參數(shù)字典,這樣作做是為了復(fù)用方法,因?yàn)樵谔幚鞵ost請(qǐng)求的時(shí)候我們會(huì)繼續(xù)使用這個(gè)方法。該方法定義如下:
/// <summary> /// 從內(nèi)容中解析請(qǐng)求參數(shù)并返回一個(gè)字典 /// </summary> /// <param name="content">使用&連接的參數(shù)字符串</param> /// <returns>如果存在參數(shù)則返回參數(shù)否則返回null</returns> protected Dictionary<string, string> GetRequestParams(string content) { //防御編程 if(string.IsNullOrEmpty(content)) return null; //按照&對(duì)字符進(jìn)行分割 string[] reval = content.Split('&'); if(reval.Length <= 0) return null; //將結(jié)果添加至字典 Dictionary<string, string> dict = new Dictionary<string, string>(); foreach(string val in reval) { string[] kv = val.Split('='); if(kv.Length <= 1) dict.Add(kv[0], ""); dict.Add(kv[0],kv[1]); } //返回字典 return dict; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
Post請(qǐng)求相對(duì)Get請(qǐng)求比較安全,因?yàn)樗朔薌et請(qǐng)求參數(shù)長(zhǎng)度的限制問(wèn)題,而且由于它的參數(shù)是存放在消息體中的,所以在傳遞參數(shù)的時(shí)候?qū)τ脩舳允遣豢梢?jiàn)的,我們平時(shí)接觸到的網(wǎng)站登錄都是這種類型,而復(fù)雜點(diǎn)的網(wǎng)站會(huì)通過(guò)驗(yàn)證碼、Cookie等形式來(lái)避免爬蟲(chóng)程序模擬登錄,在Web開(kāi)發(fā)中Post請(qǐng)求可以由一個(gè)表單發(fā)起,可以由爬蟲(chóng)程序如HttpWebRequest、WebClient等發(fā)起,下面我們重點(diǎn)來(lái)分析它的請(qǐng)求報(bào)文:
POST / HTTP/1.1 Accept: text/html, application/xhtml+xml, image/jxr, */* Accept-Language: zh-Hans-CN,zh-Hans;q=0.5 User-Agent: Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586 Accept-Encoding: gzip, deflate Host: localhost:4040 Connection: Keep-Alive Cookie: _ga=GA1.1.1181222800.1463541781 num1=23&num2=12 1 2 3 4 5 6 7 8 9 10
我們可以注意到此時(shí)請(qǐng)求行的請(qǐng)求方法變成了POST,而在報(bào)文結(jié)尾增加了一行內(nèi)容,我們稱其為“消息體”,這是一個(gè)可選的內(nèi)容,請(qǐng)注意它前面有一個(gè)空行。所以,當(dāng)我們處理一個(gè)Posst請(qǐng)求的時(shí)候,通過(guò)最后一行就可以解析出客戶端傳遞過(guò)來(lái)的參數(shù),和Get請(qǐng)求相同,我們這里繼續(xù)使用GetRequestParams來(lái)完成解析。
if(this.Method == "POST") this.Params = GetRequestParams(lines[lines.Length-1]); 1 2
現(xiàn)在我們來(lái)完成一個(gè)簡(jiǎn)單地實(shí)例,服務(wù)器自然由我們這里設(shè)計(jì)的這個(gè)服務(wù)器來(lái)完成咯,而客戶端則由Unity來(lái)完成因?yàn)閁nity有簡(jiǎn)單的WWW可以使用。首先來(lái)編寫(xiě)服務(wù)端,這個(gè)繼承HttpServer就好了,我們主要來(lái)寫(xiě)這里的方法:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using HttpServerLib; using System.IO; namespace HttpServer { public class ExampleServer : HttpServerLib.HttpServer { /// <summary> /// 構(gòu)造函數(shù) /// </summary> /// <param name="ipAddress">IP地址</param> /// <param name="port">端口號(hào)</param> public ExampleServer(string ipAddress, int port) : base(ipAddress, port) { } public override void OnPost(HttpRequest request) { //獲取客戶端傳遞的參數(shù) int num1 = int.Parse(request.Params["num1"]); int num2 = int.Parse(request.Params["num2"]); //設(shè)置返回信息 string content = string.Format("這是通過(guò)Post方式返回的數(shù)據(jù):num1={0},num2={1}",num1,num2); //構(gòu)造響應(yīng)報(bào)文 HttpResponse response = new HttpResponse(content, Encoding.UTF8); response.StatusCode = "200"; response.Content_Type = "text/html; charset=UTF-8"; response.Server = "ExampleServer"; //發(fā)送響應(yīng) ProcessResponse(request.Handler, response); } public override void OnGet(HttpRequest request) { //獲取客戶端傳遞的參數(shù) int num1 = int.Parse(request.Params["num1"]); int num2 = int.Parse(request.Params["num2"]); //設(shè)置返回信息 string content = string.Format("這是通過(guò)Get方式返回的數(shù)據(jù):num1={0},num2={1}",num1,num2); //構(gòu)造響應(yīng)報(bào)文 HttpResponse response = new HttpResponse(content, Encoding.UTF8); response.StatusCode = "200"; response.Content_Type = "text/html; charset=UTF-8"; response.Server = "ExampleServer"; //發(fā)送響應(yīng) ProcessResponse(request.Handler, response); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
因?yàn)檫@里需要對(duì)Get和Post進(jìn)行響應(yīng),所以我們這里對(duì)OnGet和OnPost兩個(gè)方法進(jìn)行了重寫(xiě),這里的處理方式非常簡(jiǎn)單,按照一定格式返回?cái)?shù)據(jù)即可。下面我們來(lái)說(shuō)說(shuō)Unity作為客戶端這邊要做的工作。WWW是Unity3D中提供的一個(gè)簡(jiǎn)單的HTTP協(xié)議的封裝類,它和.NET平臺(tái)下的WebClient、HttpWebRequest/HttpWebResponse類似,都可以處理常見(jiàn)的HTTP請(qǐng)求如Get和Post這兩種請(qǐng)求方式。
WWW的優(yōu)勢(shì)主要是簡(jiǎn)單易用和支持協(xié)程,尤其是Unity3D中的協(xié)程(Coroutine)這個(gè)特性,如果能夠得到良好的使用,常常能夠起到事倍功半的效果。因?yàn)閃WW強(qiáng)調(diào)的是以HTTP短鏈接為主的易用性,所以相應(yīng)地在超時(shí)、Cookie等HTTP頭部字段支持的完整性上無(wú)法和WebClient、HttpWebRequest/HttpWebRespons相提并論,當(dāng)我們需要更復(fù)雜的HTTP協(xié)議支持的時(shí)候,選擇在WebClient、HttpWebRequest/HttpWebResponse上進(jìn)行深度定制將會(huì)是一個(gè)不錯(cuò)的選擇。我們這里需要的是發(fā)起一個(gè)簡(jiǎn)單的HTTP請(qǐng)求,所以使用WWW完全可以滿足我們的要求,首先我們來(lái)看在Unity3D中如何發(fā)起一個(gè)Get請(qǐng)求,這里給出一個(gè)簡(jiǎn)單的代碼示例:
//采用GET方式請(qǐng)求數(shù)據(jù) IEnumerator Get() { WWW www = new WWW ("http://127.0.0.1:4040/?num1=12&num2=23"); yield return www; Debug.Log(www.text); } 1 2 3 4 5 6 7
現(xiàn)在我們是需要使用StartCoroutine調(diào)用這個(gè)方法就可以啦!同樣地,對(duì)于Post請(qǐng)求,我們這里采用一個(gè)WWWForm來(lái)封裝參數(shù),而在網(wǎng)頁(yè)開(kāi)發(fā)中我們通常都是借助表單來(lái)向服務(wù)器傳遞參數(shù)的,這里給出同樣簡(jiǎn)單的代碼示例:
//采用POST方式請(qǐng)求數(shù)據(jù) IEnumerator Post() { WWWForm form = new WWWForm (); form.AddField ("num1", 12); form.AddField ("num2", 23); WWW www = new WWW ("http://127.0.0.1:4040/", form); yield return www; Debug.Log (www.text); } 1 2 3 4 5 6 7 8 9 10
而運(yùn)行這個(gè)實(shí)例,我們可以得到下面的結(jié)果:
都是誰(shuí)告訴你做服務(wù)器開(kāi)發(fā)一定要用Java的啊,現(xiàn)在我們可以寫(xiě)出自己的服務(wù)器了,本篇結(jié)束
*請(qǐng)認(rèn)真填寫(xiě)需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。