1. 程式人生 > 實用技巧 >Python網路爬蟲開發實戰使用XPath,xpath的多種用法

Python網路爬蟲開發實戰使用XPath,xpath的多種用法

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

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

很多人學習python,不知道從何學起。
很多人學習python,掌握了基本語法過後,不知道在哪裡尋找案例上手。
很多已經做案例的人,卻不知道如何去學習更加高深的知識。
那麼針對這三類人,我給大家提供一個好的學習平臺,免費領取視訊教程,電子書籍,以及課程的原始碼!
QQ群:101677771

1. XPath概覽

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

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

2. XPath常用規則

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

表4-1 XPath常用規則

表示式

描述

nodename

選取此節點的所有子節點

/

從當前節點選取直接子節點

//

從當前節點選取子孫節點

.

選取當前節點

..

選取當前節點的父節點

@

選取屬性

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

1 //title[@lang='eng']

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

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

3. 準備工作

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

4. 例項引入

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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型別,結果如下:

1 2 3 4 5 6 7 8 9 10 <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節點。

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

1 2 3 4 5 from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = etree.tostring(html) print(result.decode('utf-8'))

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

1 2 3 4 5 6 7 8 9 <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的宣告,不過對解析無任何影響,結果如下:

1 2 3 4 5 6 7 8 9 10 <!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文字為例,如果要選取所有節點,可以這樣實現:

1 2 3 4 from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//*') print(result)

執行結果如下:

1 [<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節點,示例如下:

1 2 3 4 5 from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li') print(result) print(result[0])

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

執行結果:

1 2 [<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子節點,可以這樣實現:

1 2 3 4 5 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子節點。

執行結果如下:

1 [<Element a at 0x106ee8688>, <Element a at 0x106ee86c8>, <Element a at 0x106ee8708>, <Element a at 0x106ee8748>, <Element a at 0x106ee8788>]

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

1 2 3 4 5 from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//ul//a') print(result)

執行結果是相同的。

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

1 2 3 4 5 from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//ul/a') print(result)

執行結果如下:

1 []

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

7. 父節點

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

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

1 2 3 4 5 from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//a[@href="link4.html"]/../@class') print(result)

執行結果如下:

1 ['item-1']

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

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

1 2 3 4 5 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節點,可以這樣實現:

1 2 3 4 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節點有兩個,所以結果應該返回兩個匹配到的元素。結果如下:

1 [<Element li at 0x10a399288>, <Element li at 0x10a3992c8>]

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

9. 文字獲取

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

1 2 3 4 5 from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]/text()') print(result)

執行結果如下:

1 ['\n ']

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

即選中的是這兩個節點:

1 2 3 <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節點再獲取文字,程式碼如下:

1 2 3 4 5 from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]/a/text()') print(result)

執行結果如下:

1 ['first item', 'fifth item']

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

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

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

1 2 3 4 5 from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]//text()') print(result)

執行結果如下:

1 ['first item', 'fifth item', '\n ']

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

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

10. 屬性獲取

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

1 2 3 4 5 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指的是獲取節點的某個屬性,二者需要做好區分。

執行結果如下:

1 ['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']

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

11. 屬性多值匹配

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

1 2 3 4 5 6 7 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,此時如果還想用之前的屬性匹配獲取,就無法匹配了,此時的執行結果如下:

1 []

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

1 2 3 4 5 6 7 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()方法,第一個引數傳入屬性名稱,第二個引數傳入屬性值,只要此屬性包含所傳入的屬性值,就可以完成匹配了。

此時執行結果如下:

1 ['first item']

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

12. 多屬性匹配

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

1 2 3 4 5 6 7 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操作符相連,相連之後置於中括號內進行條件篩選。執行結果如下:

1 ['first item']

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

表4-2 運算子及其介紹

運算子

描述

例項

返回值

or

age=19 or age=20

如果age是19,則返回true。如果age是21,則返回false

and

age>19 and age<21

如果age是20,則返回true。如果age18,則返回false

mod

計算除法的餘數

5 mod 2

1

|

計算兩個節點集

//book | //cd

返回所有擁有bookcd元素的節點集

+

加法

6 + 4

10

-

減法

6 - 4

2

*

乘法

6 * 4

24

div

除法

8 div 4

2

=

等於

age=19

如果age是19,則返回true。如果age是20,則返回false

!=

不等於

age!=19

如果age是18,則返回true。如果age是19,則返回false

<

小於

age<19

如果age是18,則返回true。如果age是19,則返回false

<=

小於或等於

age<=19

如果age是19,則返回true。如果age是20,則返回false

>

大於

age>19

如果age是20,則返回true。如果age是19,則返回false

>=

大於或等於

age>=19

如果age是19,則返回true。如果age是18,則返回false

此表參考來源:http://www.w3school.com.cn/xpath/xpath_operators.asp

13. 按序選擇

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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就是倒數第三個。

執行結果如下:

1 2 3 4 ['first item'] ['fifth item'] ['first item', 'second item'] ['third item']

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

14. 節點軸選擇

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

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 from lxml import etree text = ''' <div> <ul> <li class="item-0"><a href="link1.html"><span>first item</span></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]/ancestor::*') print(result) result = html.xpath('//li[1]/ancestor::div') print(result) result = html.xpath('//li[1]/attribute::*') print(result) result = html.xpath('//li[1]/child::a[@href="link1.html"]') print(result) result = html.xpath('//li[1]/descendant::span') print(result) result = html.xpath('//li[1]/following::*[2]') print(result) result = html.xpath('//li[1]/following-sibling::*')<code class="lang-python"> <span class="kwd">print</span><span class="pun">(</span><span class="pln">result</span><span class="pun">)</span>

執行結果如下:

1 2 3 4 5 6 7 [<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軸的簡單用法,更多軸的用法可以參考:http://www.w3school.com.cn/xpath/xpath_axes.asp

15. 結語

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

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

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