筆者上一篇文章咱們講到了如何「從零開始造一個智障機器人」,若是感興趣的朋友能夠看一看。本文能夠說是上一篇文章的前傳,爲造聊天機器人而打基礎。html
上篇文章中的對話機器人,其一問一答仍是挺流暢的,那麼咱們不由思考,爲何「機器人」能根據咱們的問句,回答出符合邏輯、語義的答案呢?其實這一切的一切都是源於天然語言訓練的基礎——語料。python
本文將和你們分享一些從互聯網上爬取語料的經驗。算法
0x1 工具準備json
工欲善其事必先利其器,爬取語料的根基即是基於python。咱們基於python3進行開發,主要使用如下幾個模塊:requests、lxml、json。api
簡單介紹一個各模塊的功能瀏覽器
01|requests微信
requests是一個Python第三方庫,處理URL資源特別方便。它的官方文檔上寫着大大口號:HTTP for Humans(爲人類使用HTTP而生)。相比python自帶的urllib使用體驗,筆者認爲requests的使用體驗比urllib高了一個數量級。app
咱們簡單的比較一下:機器學習
urllib:
1import urllib2
2import urllib
3
4URL_GET = "https://api.douban.com/v2/event/list"
5#構建請求參數
6params = urllib.urlencode({'loc':'108288','day_type':'weekend','type':'exhibition'})
7
8#發送請求
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())
複製代碼
requests:
1import requests
2
3URL_GET = "https://api.douban.com/v2/event/list"
4#構建請求參數
5params = {'loc':'108288','day_type':'weekend','type':'exhibition'}
6
7#發送請求
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)複製代碼
咱們能夠發現,這兩種庫仍是有一些區別的:
1. 參數的構建:urllib須要對參數進行urlencode編碼處理,比較麻煩;requests無需額外編碼處理,十分簡潔。
2. 請求發送:urllib須要額外對url參數進行構造,變爲符合要求的形式;requests則簡明不少,直接get對應連接與參數。
3. 鏈接方式:看一下返回數據的頭信息的「connection」,使用urllib庫時,"connection":"close",說明每次請求結束關掉socket通道,而使用requests庫使用了urllib3,屢次請求重複使用一個socket,"connection":"keep-alive",說明屢次請求使用一個鏈接,消耗更少的資源
4. 編碼方式:requests庫的編碼方式Accept-Encoding更全,在此不作舉例
綜上所訴,使用requests更爲簡明、易懂,極大的方便咱們開發。
02|lxml
BeautifulSoup是一個庫,而XPath是一種技術,python中最經常使用的XPath庫是lxml。
當咱們拿到requests返回的頁面後,咱們怎麼拿到想要的數據呢?這個時候祭出lxml這強大的HTML/XML解析工具。python從不缺解析庫,那麼咱們爲何要在衆多庫裏選擇lxml呢?咱們選擇另外一款出名的HTML解析庫BeautifulSoup來進行對比。
咱們簡單的比較一下:
BeautifulSoup:
1from bs4 import BeautifulSoup #導入庫
2# 假設html是須要被解析的html
3
4#將html傳入BeautifulSoup 的構造方法,獲得一個文檔的對象
5soup = BeautifulSoup(html,'html.parser',from_encoding='utf-8')
6#查找全部的h4標籤
7links = soup.find_all("h4")
複製代碼
lxml:
1from lxml import etree
2# 假設html是須要被解析的html
3
4#將html傳入etree 的構造方法,獲得一個文檔的對象
5root = etree.HTML(html)
6#查找全部的h4標籤
7links = root.xpath("//h4")
複製代碼
咱們能夠發現,這兩種庫仍是有一些區別的:
1. 解析html: BeautifulSoup的解析方式和JQ的寫法相似,API很是人性化,支持css選擇器;lxml的語法有必定的學習成本
2. 性能:BeautifulSoup是基於DOM的,會載入整個文檔,解析整個DOM樹,所以時間和內存開銷都會大不少;而lxml只會局部遍歷,另外lxml是用c寫的,而BeautifulSoup是用python寫的,明顯的性能上lxml>>BeautifulSoup。
綜上所訴,使用BeautifulSoup更爲簡明、易用,lxml雖然有必定學習成本,但整體也很簡明易懂,最重要的是它基於C編寫,速度快不少,對於筆者這種強迫症,天然而然就選lxml啦。
03|json
python自帶json庫,對於基礎的json的處理,自帶庫徹底足夠。可是若是你想更偷懶,可使用第三方json庫,常見的有demjson、simplejson。
這兩種庫,不管是import模塊速度,仍是編碼、解碼速度,都是simplejson更勝一籌,再加上兼容性simplejson更好。因此你們若是想使用方庫,可使用simplejson。
0x2 肯定語料源
將武器準備好以後,接下來就須要肯定爬取方向。
以電競類語料爲例,如今咱們要爬電競類相關語料。你們熟悉的電競平臺有企鵝電競、企鵝電競和企鵝電競(斜眼),因此咱們以企鵝電競上直播的遊戲做爲數據源進行爬取。
咱們登錄企鵝電競官網,進入遊戲列表頁,能夠發現頁面上有不少遊戲,經過人工去寫這些遊戲名收益明顯不高,因而咱們就開始咱們爬蟲的第一步:遊戲列表爬取。
1import requests
2from lxml import etree
3
4# 更新遊戲列表
5def _updateGameList():
6 # 發送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構建爲Xpath模式
22 root = etree.HTML(res.content)
23 # 使用Xpath語法,獲取遊戲名
24 gameList = root.xpath("//ul[@class='livelist-mod']//li//p//text()")
25 # 輸出爬到的遊戲名
26 print(gameList)
複製代碼
當咱們拿到這幾十個遊戲名後,下一步就是對這幾十款遊戲進行語料爬取,這時候問題就來了,咱們要從哪一個網站來爬這幾十個遊戲的攻略呢,taptap?多玩?17173?在對這幾個網站進行分析後,發現這些網站僅有一些熱門遊戲的文章語料,一些冷門或者低熱度的遊戲,例如「靈魂籌碼」、「奇蹟:覺醒」、「死神來了」等,很難在這些網站上找到大量文章語料,如圖所示:
咱們能夠發現,「奇蹟:覺醒」、「靈魂籌碼」的文章語料特別少,數量上不符合咱們的要求。那麼有沒有一個比較通用的資源站,它擁有着無比豐富的文章語料,能夠知足咱們的需求。
其實靜下心來想一想,這個資源站咱們每天都有用到,那就是百度。咱們在百度新聞搜索相關遊戲,拿到搜索結果列表,這些列表的連接的網頁內容幾乎都與搜索結果強相關,這樣咱們數據源不夠豐富的問題便輕鬆解決了。可是此時出現了一個新的問題,而且是一個比較難解決的問題——如何抓取到任意網頁的文章內容?
由於不一樣的網站都有不一樣的頁面結構,咱們沒法與預知將會爬到哪一個網站的數據,而且咱們也不可能針對每個網站都去寫一套爬蟲,那樣的工做量簡直不可思議!可是咱們也不能簡單粗暴的將頁面中的全部文字都爬下來,用那樣的語料來進行訓練無疑是噩夢!
通過與各個網站鬥智鬥勇、查詢資料與思索以後,終於找到一條比較通用的方案,下面爲你們講一講筆者的思路。
0x3 任意網站的文章語料爬取
01|提取方法
1)基於Dom樹正文提取2)基於網頁分割找正文塊
3)基於標記窗的正文提取
4)基於數據挖掘或機器學習
5)基於行塊分佈函數正文提取
02|提取原理
你們看到這幾種是否是都有點疑惑了,它們究竟是怎麼提取的呢?讓筆者慢慢道來。1)基於Dom樹的正文提取:
這一種方法主要是經過比較規範的HTML創建Dom樹,而後地櫃遍歷Dom,比較並識別各類非正文信息,包括廣告、連接和非重要節點信息,將非正文信息抽離以後,餘下來的天然就是正文信息。
可是這種方法有兩個問題
① 特別依賴於HTML的良好結構,若是咱們爬取到一個不按W3c規範的編寫的網頁時,這種方法便不是很適用。
② 樹的創建和遍歷時間複雜度、空間複雜度都較高,樹的遍歷方法也因HTML標籤會有不一樣的差別。
2) 基於網頁分割找正文塊:
這一種方法是利用HTML標籤中的分割線以及一些視覺信息(如文字顏色、字體大小、文字信息等)。
這種方法存在一個問題:
① 不一樣的網站HTML風格迥異,分割沒有辦法統一,沒法保證通用性。
3) 基於標記窗的正文提取:
先科普一個概念——標記窗,咱們將兩個標籤以及其內部包含的文本合在一塊兒成爲一個標記窗(好比<h1>我是h1</h1>中的「我是h1」就是標記窗內容),取出標記窗的文字。
這種方法先取文章標題、HTML中全部的標記窗,在對其進行分詞。而後計算標題的序列與標記窗文本序列的詞語距離L,若是L小於一個閾值,則認爲此標記窗內的文本是正文。這種方法雖然看上去挺好,但其實也是存在問題的:
① 須要對頁面中的全部文本進行分詞,效率不高。
② 詞語距離的閾值難以肯定,不一樣的文章擁有不一樣的閾值。
4)基於數據挖掘或機器學習
使用大數據進行訓練,讓機器提取主文本。
這種方法確定是極好的,可是它須要先有html與正文數據,而後進行訓練。咱們在此不進行探討。
5)基於行塊分佈函數正文提取
對於任意一個網頁,它的正文和標籤老是雜糅在一塊兒。此方法的核心有亮點:① 正文區的密度;② 行塊的長度;一個網頁的正文區域確定是文字信息分佈最密集的區域之一,這個區域可能最大(評論信息長、正文較短),因此同時引進行塊長度進行判斷。
實現思路:
① 咱們先將HTML去標籤,只留全部正文,同時留下標籤取出後的全部空白位置信息,咱們稱其爲Ctext;
② 對每個Ctext取周圍k行(k<5),合起來稱爲Cblock;
③ 對Cblock去掉全部空白符,其文字總長度稱爲Clen;
④ 以Ctext爲橫座標軸,以各行的Clen爲縱軸,創建座標系。
以這個網頁爲例: http://www.gov.cn/ldhd/2009-11/08/content_1459564.htm 該網頁的正文區域爲145行至182行。
由上圖可知,正確的文本區域全都是分佈函數圖上含有最值且連續的一個區域,這個區域每每含有一個驟升點和一個驟降點。所以,網頁正文抽取問題轉化爲了求行塊分佈函數上的驟升點和驟降點兩個邊界點,這兩個邊界點所含的區域包含了當前網頁的行塊長度最大值而且是連續的。
通過大量實驗,證實此方法對於中文網頁的正文提取有較高的準確度,此算法的優勢在於,行塊函數不依賴與HTML代碼,與HTML標籤無關,實現簡單,準確率較高。
主要邏輯代碼以下:
1# 假設content爲已經拿到的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# 計算縱座標,每個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# 開始標識
21start = -1
22# 結束標識
23end = -1
24# 是否開始標識
25boolstart = False
26# 是否結束標識
27boolend = False
28# 行塊的長度閾值
29max_text_len = 88
30# 文章主內容
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,認爲是正文
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,則結束
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 # 去掉版權信息
58 if ("Copyright" in str or "版權全部" in str):
59 continue
60 main_text.append(str)
61 boolstart = boolend = False
62# 返回主內容
63result = "".join(list(main_text))
複製代碼
0x4 結語
至此咱們就能夠獲取任意內容的文章語料了,但這僅僅是開始,獲取到了這些語料後咱們還須要在一次進行清洗、分詞、詞性標註等,才能得到真正可使用的語料。
後續有機會再和你們分享語料清洗這一塊。這裏有一個可愛的二維碼,你們記得關注喲~
始發於微信公衆號: 騰訊DeepOcean