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