【Python3網路爬蟲開發實戰】4-解析庫的使用-1 使用XPath

崔慶才丨靜覓發表於2019-02-26

上一章中,我們實現了一個最基本的爬蟲,但提取頁面資訊時使用的是正規表示式,這還是比較煩瑣,而且萬一有地方寫錯了,可能導致匹配失敗,所以使用正規表示式提取頁面資訊多多少少還是有些不方便。

對於網頁的節點來說,它可以定義idclass或其他屬性。而且節點之間還有層次關係,在網頁中可以通過XPath或CSS選擇器來定位一個或多個節點。那麼,在頁面解析時,利用XPath或CSS選擇器來提取某個節點,然後再呼叫相應方法獲取它的正文內容或者屬性,不就可以提取我們想要的任意資訊了嗎?

在Python中,怎樣實現這個操作呢?不用擔心,這種解析庫已經非常多,其中比較強大的庫有lxml、Beautiful Soup、pyquery等,本章就來介紹這3個解析庫的用法。有了它們,我們就不用再為正規表示式發愁,而且解析效率也會大大提高。

XPath,全稱XML Path Language,即XML路徑語言,它是一門在XML文件中查詢資訊的語言。它最初是用來搜尋XML文件的,但是它同樣適用於HTML文件的搜尋。

所以在做爬蟲時,我們完全可以使用XPath來做相應的資訊抽取。本節中,我們就來介紹XPath的基本用法。

1. XPath概覽

XPath的選擇功能十分強大,它提供了非常簡潔明瞭的路徑選擇表示式。另外,它還提供了超過100個內建函式,用於字串、數值、時間的匹配以及節點、序列的處理等。幾乎所有我們想要定位的節點,都可以用XPath來選擇。

XPath於1999年11月16日成為W3C標準,它被設計為供XSLT、XPointer以及其他XML解析軟體使用,更多的文件可以訪問其官方網站:www.w3.org/TR/xpath/

2. XPath常用規則

表4-1列舉了XPath的幾個常用規則。

表4-1 XPath常用規則

【Python3網路爬蟲開發實戰】4-解析庫的使用-1 使用XPath

這裡列出了XPath的常用匹配規則,示例如下:

//title[@lang=`eng`]
複製程式碼

這就是一個XPath規則,它代表選擇所有名稱為title,同時屬性lang的值為eng的節點。

後面會通過Python的lxml庫,利用XPath進行HTML的解析。

3. 準備工作

使用之前,首先要確保安裝好lxml庫,若沒有安裝,可以參考第1章的安裝過程。

4. 例項引入

現在通過例項來感受一下使用XPath來對網頁進行解析的過程,相關程式碼如下:

from lxml import etree
text = ```
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a>
     </ul>
 </div>
```
html = etree.HTML(text)
result = etree.tostring(html)
print(result.decode(`utf-8`))
複製程式碼

這裡首先匯入lxml庫的etree模組,然後宣告瞭一段HTML文字,呼叫HTML類進行初始化,這樣就成功構造了一個XPath解析物件。這裡需要注意的是,HTML文字中的最後一個li節點是沒有閉合的,但是etree模組可以自動修正HTML文字。

這裡我們呼叫tostring()方法即可輸出修正後的HTML程式碼,但是結果是bytes型別。這裡利用decode()方法將其轉成str型別,結果如下:

<html><body><div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a>
     </li></ul>
 </div>
</body></html>
複製程式碼

可以看到,經過處理之後,li節點標籤被補全,並且還自動新增了bodyhtml節點。

另外,也可以直接讀取文字檔案進行解析,示例如下:

from lxml import etree

html = etree.parse(`./test.html`, etree.HTMLParser())
result = etree.tostring(html)
print(result.decode(`utf-8`))
複製程式碼

其中test.html的內容就是上面例子中的HTML程式碼,內容如下:

<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a>
     </ul>
 </div>
複製程式碼

這次的輸出結果略有不同,多了一個DOCTYPE的宣告,不過對解析無任何影響,結果如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a>
     </li></ul>
 </div></body></html>
複製程式碼

5. 所有節點

我們一般會用//開頭的XPath規則來選取所有符合要求的節點。這裡以前面的HTML文字為例,如果要選取所有節點,可以這樣實現:

from lxml import etree
html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//*`)
print(result)
複製程式碼

執行結果如下:

[<Element html at 0x10510d9c8>, <Element body at 0x10510da08>, <Element div at 0x10510da48>, <Element ul at 0x10510da88>, <Element li at 0x10510dac8>, <Element a at 0x10510db48>, <Element li at 0x10510db88>, <Element a at 0x10510dbc8>, <Element li at 0x10510dc08>, <Element a at 0x10510db08>, <Element li at 0x10510dc48>, <Element a at 0x10510dc88>, <Element li at 0x10510dcc8>, <Element a at 0x10510dd08>]
複製程式碼

這裡使用*代表匹配所有節點,也就是整個HTML文字中的所有節點都會被獲取。可以看到,返回形式是一個列表,每個元素是Element型別,其後跟了節點的名稱,如htmlbodydivullia等,所有節點都包含在列表中了。

當然,此處匹配也可以指定節點名稱。如果想獲取所有li節點,示例如下:

from lxml import etree
html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//li`)
print(result)
print(result[0])
複製程式碼

這裡要選取所有li節點,可以使用//,然後直接加上節點名稱即可,呼叫時直接使用xpath()方法即可。

執行結果:

[<Element li at 0x105849208>, <Element li at 0x105849248>, <Element li at 0x105849288>, <Element li at 0x1058492c8>, <Element li at 0x105849308>]
<Element li at 0x105849208>
複製程式碼

這裡可以看到提取結果是一個列表形式,其中每個元素都是一個 Element物件。如果要取出其中一個物件,可以直接用中括號加索引,如[0]

6. 子節點

我們通過///即可查詢元素的子節點或子孫節點。假如現在想選擇li節點的所有直接a子節點,可以這樣實現:

from lxml import etree

html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//li/a`)
print(result)
複製程式碼

這裡通過追加/a即選擇了所有li節點的所有直接a子節點。因為//li用於選中所有li節點,/a用於選中li節點的所有直接子節點a,二者組合在一起即獲取所有li節點的所有直接a子節點。

執行結果如下:

[<Element a at 0x106ee8688>, <Element a at 0x106ee86c8>, <Element a at 0x106ee8708>, <Element a at 0x106ee8748>, <Element a at 0x106ee8788>]
複製程式碼

此處的/用於選取直接子節點,如果要獲取所有子孫節點,就可以使用//。例如,要獲取ul節點下的所有子孫a節點,可以這樣實現:

from lxml import etree

html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//ul//a`)
print(result)
複製程式碼

執行結果是相同的。

但是如果這裡用//ul/a,就無法獲取任何結果了。因為/用於獲取直接子節點,而在ul節點下沒有直接的a子節點,只有li節點,所以無法獲取任何匹配結果,程式碼如下:

from lxml import etree

html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//ul/a`)
print(result)
複製程式碼

執行結果如下:

[]
複製程式碼

因此,這裡我們要注意///的區別,其中/用於獲取直接子節點,//用於獲取子孫節點。

7. 父節點

我們知道通過連續的///可以查詢子節點或子孫節點,那麼假如我們知道了子節點,怎樣來查詢父節點呢?這可以用..來實現。

比如,現在首先選中href屬性為link4.htmla節點,然後再獲取其父節點,然後再獲取其class屬性,相關程式碼如下:

from lxml import etree

html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//a[@href="link4.html"]/../@class`)
print(result)
複製程式碼

執行結果如下:

[`item-1`]
複製程式碼

檢查一下結果發現,這正是我們獲取的目標li節點的class

同時,我們也可以通過parent::來獲取父節點,程式碼如下:

from lxml import etree

html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//a[@href="link4.html"]/parent::*/@class`)
print(result)
複製程式碼

8. 屬性匹配

在選取的時候,我們還可以用@符號進行屬性過濾。比如,這裡如果要選取classitem-1li節點,可以這樣實現:

from lxml import etree
html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//li[@class="item-0"]`)
print(result)
複製程式碼

這裡我們通過加入[@class="item-0"],限制了節點的class屬性為item-0,而HTML文字中符合條件的li節點有兩個,所以結果應該返回兩個匹配到的元素。結果如下:

[<Element li at 0x10a399288>, <Element li at 0x10a3992c8>]
複製程式碼

可見,匹配結果正是兩個,至於是不是那正確的兩個,後面再驗證。

9. 文字獲取

我們用XPath中的text()方法獲取節點中的文字,接下來嘗試獲取前面li節點中的文字,相關程式碼如下:

from lxml import etree

html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//li[@class="item-0"]/text()`)
print(result)
複製程式碼

執行結果如下:

[`
     `]
複製程式碼

奇怪的是,我們並沒有獲取到任何文字,只獲取到了一個換行符,這是為什麼呢?因為XPath中text()前面是/,而此處/的含義是選取直接子節點,很明顯li的直接子節點都是a節點,文字都是在a節點內部的,所以這裡匹配到的結果就是被修正的li節點內部的換行符,因為自動修正的li節點的尾標籤換行了。

即選中的是這兩個節點:

<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
</li>
複製程式碼

其中一個節點因為自動修正,li節點的尾標籤新增的時候換行了,所以提取文字得到的唯一結果就是li節點的尾標籤和a節點的尾標籤之間的換行符。

因此,如果想獲取li節點內部的文字,就有兩種方式,一種是先選取a節點再獲取文字,另一種就是使用//。接下來,我們來看下二者的區別。

首先,選取到a節點再獲取文字,程式碼如下:

from lxml import etree

html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//li[@class="item-0"]/a/text()`)
print(result)
複製程式碼

執行結果如下:

[`first item`, `fifth item`]
複製程式碼

可以看到,這裡的返回值是兩個,內容都是屬性為item-0li節點的文字,這也印證了前面屬性匹配的結果是正確的。

這裡我們是逐層選取的,先選取了li節點,又利用/選取了其直接子節點a,然後再選取其文字,得到的結果恰好是符合我們預期的兩個結果。

再來看下用另一種方式(即使用//)選取的結果,程式碼如下:

from lxml import etree

html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//li[@class="item-0"]//text()`)
print(result)
複製程式碼

執行結果如下:

[`first item`, `fifth item`, `
     `]
複製程式碼

不出所料,這裡的返回結果是3個。可想而知,這裡是選取所有子孫節點的文字,其中前兩個就是li的子節點a節點內部的文字,另外一個就是最後一個li節點內部的文字,即換行符。

所以說,如果要想獲取子孫節點內部的所有文字,可以直接用//text()的方式,這樣可以保證獲取到最全面的文字資訊,但是可能會夾雜一些換行符等特殊字元。如果想獲取某些特定子孫節點下的所有文字,可以先選取到特定的子孫節點,然後再呼叫text()方法獲取其內部文字,這樣可以保證獲取的結果是整潔的。

10. 屬性獲取

我們知道用text()可以獲取節點內部文字,那麼節點屬性該怎樣獲取呢?其實還是用@符號就可以。例如,我們想獲取所有li節點下所有a節點的href屬性,程式碼如下:

from lxml import etree

html = etree.parse(`./test.html`, etree.HTMLParser())
result = html.xpath(`//li/a/@href`)
print(result)
複製程式碼

這裡我們通過@href即可獲取節點的href屬性。注意,此處和屬性匹配的方法不同,屬性匹配是中括號加屬性名和值來限定某個屬性,如[@href="link1.html"],而此處的@href指的是獲取節點的某個屬性,二者需要做好區分。

執行結果如下:

[`link1.html`, `link2.html`, `link3.html`, `link4.html`, `link5.html`]
複製程式碼

可以看到,我們成功獲取了所有li節點下a節點的href屬性,它們以列表形式返回。

11. 屬性多值匹配

有時候,某些節點的某個屬性可能有多個值,例如:

from lxml import etree
text = ```
<li class="li li-first"><a href="link.html">first item</a></li>
```
html = etree.HTML(text)
result = html.xpath(`//li[@class="li"]/a/text()`)
print(result)
複製程式碼

這裡HTML文字中li節點的class屬性有兩個值lili-first,此時如果還想用之前的屬性匹配獲取,就無法匹配了,此時的執行結果如下:

[]
複製程式碼

這時就需要用contains()函式了,程式碼可以改寫如下:

from lxml import etree
text = ```
<li class="li li-first"><a href="link.html">first item</a></li>
```
html = etree.HTML(text)
result = html.xpath(`//li[contains(@class, "li")]/a/text()`)
print(result)
複製程式碼

這樣通過contains()方法,第一個引數傳入屬性名稱,第二個引數傳入屬性值,只要此屬性包含所傳入的屬性值,就可以完成匹配了。

此時執行結果如下:

[`first item`]
複製程式碼

此種方式在某個節點的某個屬性有多個值時經常用到,如某個節點的class屬性通常有多個。

12. 多屬性匹配

另外,我們可能還遇到一種情況,那就是根據多個屬性確定一個節點,這時就需要同時匹配多個屬性。此時可以使用運算子and來連線,示例如下:

from lxml import etree
text = ```
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
```
html = etree.HTML(text)
result = html.xpath(`//li[contains(@class, "li") and @name="item"]/a/text()`)
print(result)
複製程式碼

這裡的li節點又增加了一個屬性name。要確定這個節點,需要同時根據classname屬性來選擇,一個條件是class屬性裡面包含li字串,另一個條件是name屬性為item字串,二者需要同時滿足,需要用and操作符相連,相連之後置於中括號內進行條件篩選。執行結果如下:

[`first item`]
複製程式碼

這裡的and其實是XPath中的運算子。另外,還有很多運算子,如ormod等,在此總結為表4-2。

表4-2 運算子及其介紹

【Python3網路爬蟲開發實戰】4-解析庫的使用-1 使用XPath

此表參考來源:www.w3school.com.cn/xpath/xpath…

13. 按序選擇

有時候,我們在選擇的時候某些屬性可能同時匹配了多個節點,但是隻想要其中的某個節點,如第二個節點或者最後一個節點,這時該怎麼辦呢?

這時可以利用中括號傳入索引的方法獲取特定次序的節點,示例如下:

from lxml import etree

text = ```
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a>
     </ul>
 </div>
```
html = etree.HTML(text)
result = html.xpath(`//li[1]/a/text()`)
print(result)
result = html.xpath(`//li[last()]/a/text()`)
print(result)
result = html.xpath(`//li[position()<3]/a/text()`)
print(result)
result = html.xpath(`//li[last()-2]/a/text()`)
print(result)
複製程式碼

第一次選擇時,我們選取了第一個li節點,中括號中傳入數字1即可。注意,這裡和程式碼中不同,序號是以1開頭的,不是以0開頭。

第二次選擇時,我們選取了最後一個li節點,中括號中傳入last()即可,返回的便是最後一個li節點。

第三次選擇時,我們選取了位置小於3的li節點,也就是位置序號為1和2的節點,得到的結果就是前兩個li節點。

第四次選擇時,我們選取了倒數第三個li節點,中括號中傳入last()-2即可。因為last()是最後一個,所以last()-2就是倒數第三個。

執行結果如下:

[`first item`]
[`fifth item`]
[`first item`, `second item`]
[`third item`]
複製程式碼

這裡我們使用了last()position()等函式。在XPath中,提供了100多個函式,包括存取、數值、字串、邏輯、節點、序列等處理功能,它們的具體作用可以參考:www.w3school.com.cn/xpath/xpath…

14. 節點軸選擇

XPath提供了很多節點軸選擇方法,包括獲取子元素、兄弟元素、父元素、祖先元素等,示例如下:

print(result)
複製程式碼

執行結果如下:

[<Element html at 0x107941808>, <Element body at 0x1079418c8>, <Element div at 0x107941908>, <Element ul at 0x107941948>]
[<Element div at 0x107941908>]
[`item-0`]
[<Element a at 0x1079418c8>]
[<Element span at 0x107941948>]
[<Element a at 0x1079418c8>]
[<Element li at 0x107941948>, <Element li at 0x107941988>, <Element li at 0x1079419c8>, <Element li at 0x107941a08>]
複製程式碼

第一次選擇時,我們呼叫了ancestor軸,可以獲取所有祖先節點。其後需要跟兩個冒號,然後是節點的選擇器,這裡我們直接使用*,表示匹配所有節點,因此返回結果是第一個li節點的所有祖先節點,包括htmlbodydivul

第二次選擇時,我們又加了限定條件,這次在冒號後面加了div,這樣得到的結果就只有div這個祖先節點了。

第三次選擇時,我們呼叫了attribute軸,可以獲取所有屬性值,其後跟的選擇器還是*,這代表獲取節點的所有屬性,返回值就是li節點的所有屬性值。

第四次選擇時,我們呼叫了child軸,可以獲取所有直接子節點。這裡我們又加了限定條件,選取href屬性為link1.htmla節點。

第五次選擇時,我們呼叫了descendant軸,可以獲取所有子孫節點。這裡我們又加了限定條件獲取span節點,所以返回的結果只包含span節點而不包含a節點。

第六次選擇時,我們呼叫了following軸,可以獲取當前節點之後的所有節點。這裡我們雖然使用的是*匹配,但又加了索引選擇,所以只獲取了第二個後續節點。

第七次選擇時,我們呼叫了following-sibling軸,可以獲取當前節點之後的所有同級節點。這裡我們使用*匹配,所以獲取了所有後續同級節點。

以上是XPath軸的簡單用法,更多軸的用法可以參考:www.w3school.com.cn/xpath/xpath…

15. 結語

到現在為止,我們基本上把可能用到的XPath選擇器介紹完了。XPath功能非常強大,內建函式非常多,熟練使用之後,可以大大提升HTML資訊的提取效率。

如果想查詢更多XPath的用法,可以檢視:www.w3school.com.cn/xpath/index…

如果想查詢更多Python lxml庫的用法,可以檢視lxml.de/

本資源首發於崔慶才的個人部落格靜覓: Python3網路爬蟲開發實戰教程 | 靜覓

如想了解更多爬蟲資訊,請關注我的個人微信公眾號:進擊的Coder

weixin.qq.com/r/5zsjOyvEZ… (二維碼自動識別)

相關文章