1. 程式人生 > >python爬蟲裡資訊提取的核心方法: Beautifulsoup、Xpath和正則表示式

python爬蟲裡資訊提取的核心方法: Beautifulsoup、Xpath和正則表示式

20170531

這幾天重新拾起了爬蟲,算起來有將近5個月不碰python爬蟲了。

對照著網上的程式和自己以前寫的抓圖的程式進行了重寫,發現了很多問題。總結和歸納和提高學習效果的有效手段,因此對於這些問題做個歸納和總結,一方面總結學習成果,使之成為自己的東西,另一方面希望能夠給其他初學爬蟲的人一些啟發。

爬蟲程式核心是對網頁進行解析,從中提取出自己想要的資訊資料。這些資料可能是網址(url、href)、圖片(image)、文字(text)、語音(MP3)、視訊(mp4、avi……),它們隱藏在網頁的html資料中,在各級等級分明的element裡面,通常是有跡可循的,否則就沒有爬取的必要了。提取的手段主要有三種:xpath、BeautifulSoup、正則表示式(Re)。下面分別進行介紹:

(一)BeautifulSoup

從本心來說,我更喜歡用BeautifulSoup。因為它更符合直觀語義特性,find()和find_all()函式已經基本上足夠提取出任何的資訊,對於身份證號、QQ號等特徵特別明顯的資料,頂多再加上一個正則表示式就完全OK了。

Beautiful Soup提供一些簡單的、python式的函式用來處理導航、搜尋、修改分析樹等功能。它是一個工具箱,通過解析文件為使用者提供需要抓取的資料,因為簡單,所以不需要多少程式碼就可以寫出一個完整的應用程式。

Beautiful Soup自動將輸入文件轉換為Unicode編碼,輸出文件轉換為utf-8編碼。你不需要考慮編碼方式,除非文件沒有指定一個編碼方式,這時,Beautiful Soup就不能自動識別編碼方式了。然後,你僅僅需要說明一下原始編碼方式就可以了。Beautiful Soup已成為和lxml、html6lib一樣出色的python直譯器,為使用者靈活地提供不同的解析策略或強勁的速度。

pip install beautifulsoup4                 #pip方式安裝bs4

pip install lxml                           #pip方式安裝 lxml 解析工具

具體介紹參見:

靜下心來看完這篇博文才瞭解到原來BeautifulSoup有這麼多的用法,特別是find_all()和find(),下面是一些例項:

#例項

import requests

from bs4 import BeautifulSoup

headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}

html = requests.get(url, headers=headers)

soup = BeautifulSoup(html.text, 'lxml')  #以上是網路獲取html

soup=BeautifulSoup(open('index.html')) # 讀取本地的html,加個open函式即可

print(soup.prettify())  # 用標準html 顯示方法列印html

#soup.find_all()方法介紹 ,soup.find()與之基本類似,只是返回的是第一個值

find_all( name , attrs , recursive , text , **kwargs )

soup.find_all('b')  #查詢所有的b標籤,返回列表

soup.find_all(re.compile("^b")) # 正則表示式

soup.find_all(["a", "b"])  #傳入列表引數,找到所有的a標籤和b標籤

soup.find_all(id='link2')  #傳入id是link2的引數,Beautiful Soup會搜尋每個tag的”id”屬性

soup.find_all(href=re.compile("elsie")) #傳入正則表示式,查詢所有的href標籤內容中含有 elsie 的內容

soup.find_all(href=re.compile("elsie"), id='link1') # 多層過濾,除了href進行限定之外,對id標籤的內容也做了限定

soup.find_all("div", class_="sister") #最常用的查詢技巧,這裡之所以加‘_=’是因為‘class’不僅是html中的tag,也是python語法的關鍵詞,其他的不用加下劃線

data_soup.find_all(attrs={"data-foo": "value"}) # 針對html5裡面的data- 進行的專項查詢

soup.find_all(text="Elsie") # 對text內容進行查詢

soup.find_all(text=["Tillie", "Elsie", "Lacie"]) # 列表形式進行查詢,與上面name類似

soup.find_all(text=re.compile("Dormouse")) # 正則表示式形式,與上面類似

soup.find_all("a", limit=2) # 找到前兩個a標籤, limit用來限定次數(

還有一個select()函式比較有用,基本用法如下:

# 我們在寫 CSS 時,標籤名不加任何修飾,類名前加點,id名前加 #,在這裡我們也可以利用類似的方法來篩選元素,用到的方法是soup.select(),返回型別是list

(1)通過標籤名查詢

soup.select('title')

(2)通過類名查詢

soup.select('.sister')

(3)通過 id 名查詢

soup.select('#link1')

(4)組合查詢

組合查詢即和寫 class 檔案時,標籤名與類名、id名進行的組合原理是一樣的,例如查詢 p 標籤中,id 等於 link1的內容,二者需要用空格分開

soup.select('p #link1')

(5)屬性查詢

查詢時還可以加入屬性元素,屬性需要用中括號括起來,注意屬性和標籤屬於同一節點,所以中間不能加空格,否則會無法匹配到。

soup.select('a[class="sister"]')

soup.select('a[href="http://example.com/elsie"]')

get_text()方法可以用來獲取內容,請看下面程式碼:

soup = BeautifulSoup(html.text, 'lxml')

print (type(soup.select('title')))

print (soup.select('title')[0].get_text())  # 獲取第一個title標籤的對應內容

for title in soup.select('title'):

           print (title.get_text()) # 獲取列表中的title對應內容

好了,BeautifulSoup的用法基本介紹到這裡,除了速度上比較雞肋之外,BeautifulSoup的查詢方法做到了堪稱人性化,給人以非常直觀的語義理解。

(二)Xpath的介紹和用法

XPath 是一門在 XML 文件中查詢資訊的語言。XPath 可用來在 XML 文件中對元素和屬性進行遍歷。結構關係包括 父、子、兄弟、先輩、後代等。

這裡主要參考這篇博文:

表示式                         功能描述

nodename       選取此節點的所有子節點。

/                        從根節點選取。

//                       從匹配選擇的當前節點選擇文件中的節點,而不考慮它們的位置。

.                        選取當前節點。

..                      選取當前節點的父節點。

@                     選取屬性。

例項

在下面的表格中,我們已列出了一些路徑表示式以及表示式的結果:

路徑表示式                                                     結果

bookstore                            選取 bookstore 元素的所有子節點。

/bookstore                           選取根元素 bookstore。註釋:假如路徑起始於正斜槓( / ),則此路徑始                                                終代表到某元素的絕對路徑!

bookstore/book                  選取屬於 bookstore 的子元素的所有 book 元素。

//book                                  選取所有 book 子元素,而不管它們在文件中的位置。

bookstore//book                選擇屬於 bookstore 元素的後代的所有 book 元素,而不管它們位於                                                      bookstore 之下的什麼位置。

//@lang                               選取名為 lang 的所有屬性。

謂語(Predicates)

謂語用來查詢某個特定的節點或者包含某個指定的值的節點。謂語被嵌在 方括號 中。

例項

在下面的表格中,我們列出了帶有謂語的一些路徑表示式,以及表示式的結果:

路徑表示式                                                                        結  果

/bookstore/book[1]                       選取屬於 bookstore 子元素的第一個 book 元素。

/bookstore/book[last()]                 選取屬於 bookstore 子元素的最後一個 book 元素。

/bookstore/book[last()-1]             選取屬於 bookstore 子元素的倒數第二個 book 元素。

/bookstore/book[position()<3]     選取最前面的兩個屬於 bookstore 元素的子元素的 book 元素。

//title[@lang]                                  選取所有擁有名為 lang 的屬性的 title 元素。

//title[@lang=’eng’]                        選取所有 title 元素,且這些元素擁有值為 eng 的 lang 屬性。

/bookstore/book[price>35.00]     選取 bookstore 元素的所有 book 元素,且其中的 price 元素的值                                                            須大於 35.00。

/bookstore/book[price>35.00]/title     選取 bookstore 元素中的 book 元素的所有 title 元素,且其中                                                                   的 price 元素的值須大於 35.00。

路徑表示式                                    結果

/bookstore/*                   選取 bookstore 元素的所有子元素。

//*                                    選取文件中的所有元素。

//title[@*]                        選取所有帶有屬性的 title 元素。

選取若干路徑

通過在路徑表示式中使用“|”運算子,您可以選取若干個路徑。

例項

在下面的表格中,我們列出了一些路徑表示式,以及這些表示式的結果:

路徑表示式                                                                        結果

//book/title | //book/price                        選取 book 元素的所有 title 和 price 元素。

//title | //price                                            選取文件中的所有 title 和 price 元素。

/bookstore/book/title | //price                 選取屬於 bookstore 元素的 book 元素的所有 title 元素,以及                                                                  文件中所有的 price 元素。

用法簡介:



通過以上例項的練習,相信大家對 XPath 的基本用法有了基本的瞭解。

也可以利用 text 方法來獲取元素的內容。

下面是一些在程式中見到的表示式,大家有興趣的話可以解析一下:

response.xpath('//div[@class="moco-course-wrap"]/a[@target="_blank"]')

#找到所有 div 標籤下 class 名為moco-course-wrap的類,並且具有target="_blank"的子標籤 a標籤

box.xpath('.//@href')

#找到當前節點下,選取href屬性

box.xpath('.//img/@alt').extract()[0]

#找到當前節點下,所有的img標籤(不一定是子標籤),選取alt屬性,提取文字內容

box.xpath('.//@src').extract()[0]

#找到當前節點下,所有的src屬性,提取內容

box.xpath('.//span/text()').extract()[0].strip()[:-3]

#找到當前節點下,所有的span標籤下的文字內容

box.xpath('.//p/text()').extract()[0].strip()

#當前節點下,p標籤的文字內容

url = response.xpath("//a[contains(text(),'下一頁')]/@href").extract()

#所有包含有 “下一頁” 這個文字內容的a標籤,提取這個a標籤中的href屬性值

#爬取糗百CR版的名稱和圖片地址,果然非常好用。

nodes_title = page.xpath('//div[@class="mtitle"]//text()')

print(len(nodes_title))

#8

print(nodes_title[0])

#老闆,出個價吧

nodes_imgurl = page.xpath('//div[@style="text-align: center;"]//img/@src')

#http://wx2.sinaimg.cn/mw690/8903e1f3gy1ffq18eskojg20b4069e81.gif

(三)正則表示式

最為頭痛,最不直觀的正則表示式,下次再寫吧。

參考博文:

今天又鼓搗了幾個小時的正則表示式,從基礎到應用都看了半天,哎,正則表示式,能少用盡量少用吧,容錯率太低了,一點點錯了位置,可能都獲取不到正確的資料。先來看看正則表示式的基礎吧:

正則表示式(regular expression)描述了一種字串匹配的模式(pattern),可以用來檢查一個串是否含有某種子串、將匹配的子串替換或者從某個串中取出符合某個條件的子串等。

構造正則表示式的方法和建立數學表示式的方法一樣。也就是用多種元字元與運算子可以將小的表示式結合在一起來建立更大的表示式。正則表示式的元件可以是單個的字元、字元集合、字元範圍、字元間的選擇或者所有這些元件的任意組合。

正則表示式是由普通字元(例如字元 a 到 z)以及特殊字元(稱為"元字元")組成的文字模式。模式描述在搜尋文字時要匹配的一個或多個字串。正則表示式作為一個模板,將某個字元模式與所搜尋的字串進行匹配。

普通字元

普通字元包括沒有顯式指定為元字元的所有可列印和不可列印字元。這包括所有大寫和小寫字母、所有數字、所有標點符號和一些其他符號。

非列印字元

非列印字元也可以是正則表示式的組成部分。下表列出了表示非列印字元的轉義序列:

字元                                               描  述

\cx         匹配由x指明的控制字元。例如, \cM 匹配一個 Control-M 或回車符。x 的值必須為 A-Z 或                 a-z 之一。否則,將 c 視為一個原義的 'c' 字元。

\f            匹配一個換頁符。等價於 \x0c 和 \cL。

\n           匹配一個換行符。等價於 \x0a 和 \cJ。

\r            匹配一個回車符。等價於 \x0d 和 \cM。

\s            匹配任何空白字元,包括空格、製表符、換頁符等等。等價於 [ \f\n\r\t\v]。

\S            匹配任何非空白字元。等價於 [^ \f\n\r\t\v]。

\t              匹配一個製表符。等價於 \x09 和 \cI。

\v             匹配一個垂直製表符。等價於 \x0b 和 \cK。

特殊字元

所謂特殊字元,就是一些有特殊含義的字元。若要匹配這些特殊字元,必須首先使字元"轉義",即,將反斜槓字元\放在它們前面。下表列出了正則表示式中的特殊字元:

特別字元描述

$                      匹配輸入字串的結尾位置。如果設定了 RegExp 物件的 Multiline 屬性,則 $ 也匹配 '\n' 或 '\r'。要匹配 $ 字元本身,請使用 \$。

( )                     標記一個子表示式的開始和結束位置。子表示式可以獲取供以後使用。要匹配這些字元,請使用 \( 和 \)。

*                        匹配前面的子表示式零次或多次。要匹配 * 字元,請使用 \*。

+                       匹配前面的子表示式一次或多次。要匹配 + 字元,請使用 \+。

.                         匹配除換行符 \n 之外的任何單字元。要匹配 . ,請使用 \. 。

[                         標記一箇中括號表示式的開始。要匹配 [,請使用 \[。

?                        匹配前面的子表示式零次或一次,或指明一個非貪婪限定符。要匹配 ? 字元,請使                           用 \?。

\                          將下一個字元標記為或特殊字元、或原義字元、或向後引用、或八進位制轉義符。例                            如, 'n' 匹配字元 'n'。'\n' 匹配換行符。序列 '\\' 匹配 "\",而 '\(' 則匹配 "("。

^                          匹配輸入字串的開始位置,除非在方括號表示式中使用,此時它表示不接受該字                            符集合。要匹配 ^ 字元本身,請使用 \^。

{                          標記限定符表示式的開始。要匹配 {,請使用 \{。

|                           指明兩項之間的一個選擇。要匹配 |,請使用 \|。

限定符

限定符用來指定正則表示式的一個給定元件必須要出現多少次才能滿足匹配。有*或+或?或{n}或{n,}或{n,m}共6種。

正則表示式的限定符有:

字元                                                                          描述

*                        匹配前面的子表示式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等價於{0,}。

+                       匹配前面的子表示式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等價於 {1,}。

?                        匹配前面的子表示式零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 中的"do" 。? 等價於 {0,1}。

{n}                      n 是一個非負整數。匹配確定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的兩個 o。

{n,}                     n 是一個非負整數。至少匹配n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等價於 'o+'。'o{0,}' 則等價於 'o*'。

{n,m}                  m 和 n 均為非負整數,其中n <= m。最少匹配 n 次且最多匹配 m 次。例                 如,"o{1,3}" 將匹配 "fooooood" 中的前三個 o。'o{0,1}' 等價於 'o?'。請注意在逗號和兩個數之間不能有空格。

定位符

定位符使您能夠將正則表示式固定到行首或行尾。它們還使您能夠建立這樣的正則表示式,這些正則表示式出現在一個單詞內、在一個單詞的開頭或者一個單詞的結尾。

定位符用來描述字串或單詞的邊界,^和$分別指字串的開始與結束,span class="marked">\b 描述單詞的前或後邊界,span class="marked">\B 表示非單詞邊界。

正則表示式的限定符有:

字元描述

^                         匹配輸入字串開始的位置。如果設定了 RegExp 物件的 Multiline 屬性,^ 還會與                              \n 或 \r 之後的位置匹配。

$                        匹配輸入字串結尾的位置。如果設定了 RegExp 物件的 Multiline 屬性,$ 還會與                              \n 或 \r 之前的位置匹配。

\b                        匹配一個字邊界,即字與空格間的位置。

\B                        非字邊界匹配。

反義字元

有時需要查詢不屬於某個能簡單定義的字元類的字元。比如想查詢除了數字以外,其它任意字元都行的情況,這時需要用到反義:

常用的反義程式碼

程式碼/語法                           說明

\W                        匹配任意不是字母,數字,下劃線,漢字的字元

\S                         匹配任意不是空白符的字元

\D                         匹配任意非數字的字元

\B                          匹配不是單詞開頭或結束的位置

[^x]                        匹配除了x以外的任意字元

[^aeiou]                匹配除了aeiou這幾個字母以外的任意字元

例子:\S+匹配不包含空白符的字串。

]+>匹配用尖括號括起來的以a開頭的字串。

舉個例子,學習一下:

var str = "https://www.runoob.com:80/html/html-tutorial.html";

var patt1 = /(\w+):\/\/([^/:]+)(:\d*)?([^# ]*)/;

arr = str.match(patt1);

for (var i = 0; i < arr.length ; i++)

{    document.write(arr[i]);    

              document.write("  br  ");

}

第一個括號子表示式捕獲 Web 地址的協議部分。該子表示式匹配在冒號和兩個正斜槓前面的任何單詞。

第二個括號子表示式捕獲地址的域地址部分。子表示式匹配 / 和 : 之外的一個或多個字元。

第三個括號子表示式捕獲埠號(如果指定了的話)。該子表示式匹配冒號後面的零個或多個數字。只能重複一次該子表示式。

最後,第四個括號子表示式捕獲 Web 地址指定的路徑和 / 或頁資訊。該子表示式能匹配不包括 # 或空格字元的任何字元序列。

將正則表示式應用到上面的 URI,各子匹配項包含下面的內容:

第一個括號子表示式包含"http"

第二個括號子表示式包含"www.runoob.com"

第三個括號子表示式包含":80"

第四個括號子表示式包含"/html/html-tutorial.html"

在html中查詢,在每一行的結尾,都要加上一個\s*進行匹配,\s匹配非空白字元,從上一行末尾到下一行的起始中間,有可能是換行符,有可能是製表符,有可能一個或者多個,都可以用\s*進行匹配。

來上幾個例子吧,大家可以試著破解一下,我覺得儘量還是不要用正則,或者是配合著BeautifulSoup用正則,的確是比較坑。



這個是好不容易測試成功的第一個自己寫的正則表示式,發現用正則的時候,不敢越級查詢,哪怕子節點裡面沒有我們需要的資料,也要用.*?來把這個內容表示出來,上面這個式子這麼一大堆,其實只是提取了我們需要的兩個資料而已, 發帖作者和圖片地址,如果用soup去查詢,那是相當清楚明白。

後來又試了一下,好像也不要,關鍵是後面的re.S,連同‘\n’這個換行字元考慮進去,要不然在html.text裡面,各種換行符,確實是無從查詢。



上面這句話是用來提取每個糗事的內容和圖片相對地址的,與上面的正則表示式比較,進步的地方是,不需要再一層一層地寫了,可以越級了

result 本身是list型別的,而它的分量 result[0]是元組(tuple)型別的,即不可更改的列表。這些提取出來的資訊,被儲存或者重組,然後進行下一步的處理。或者item代替result比較好,因為更加貼近scrapy的用法。(item是scrapy裡面的一個類,用來儲存提取出的資訊,結構型別為字典)

參考文獻:

(四)總結

通過寫這篇博文,對於html網頁資料的三種查詢方法進行了重溫和了解,其中正則表示式耗時最多,總是出現這樣那樣的問題,當解決之後,還是比較有成就感的。xpath也並不太難用,介於soup和正則表示式之間吧,甚至某些時候比soup還要方便一些。

tips:

(1)在Xpath查詢中,//代表所有 /代表子節點,  如果越級查詢的話,要用// ,如果沒有越級的話,/比較合適

(2)正則表示式中,可以分開寫每個要提取的資訊,最後再合在一起,要不然太長了。如果非要寫在一起,建議每一個(.*?)佔據一行,做好標註,要不出錯了很難排錯的。