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常用規則
表示式 |
描述 |
---|---|
|
選取此節點的所有子節點 |
|
從當前節點選取直接子節點 |
|
從當前節點選取子孫節點 |
|
選取當前節點 |
|
選取當前節點的父節點 |
|
選取屬性 |
這裡列出了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 = ''' |
這裡首先匯入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
節點標籤被補全,並且還自動添加了body
、html
節點。
另外,也可以直接讀取文字檔案進行解析,示例如下:
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
型別,其後跟了節點的名稱,如html
、body
、div
、ul
、li
、a
等,所有節點都包含在列表中了。
當然,此處匹配也可以指定節點名稱。如果想獲取所有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.html
的a
節點,然後再獲取其父節點,然後再獲取其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. 屬性匹配
在選取的時候,我們還可以用@
符號進行屬性過濾。比如,這裡如果要選取class
為item-1
的li
節點,可以這樣實現:
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-0
的li
節點的文字,這也印證了前面屬性匹配的結果是正確的。
這裡我們是逐層選取的,先選取了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
屬性有兩個值li
和li-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
。要確定這個節點,需要同時根據class
和name
屬性來選擇,一個條件是class
屬性裡面包含li
字串,另一個條件是name
屬性為item
字串,二者需要同時滿足,需要用and
操作符相連,相連之後置於中括號內進行條件篩選。執行結果如下:
1 | ['first item'] |
這裡的and
其實是XPath中的運算子。另外,還有很多運算子,如or
、mod
等,在此總結為表4-2。
表4-2 運算子及其介紹
運算子 |
描述 |
例項 |
返回值 |
---|---|---|---|
|
或 |
|
如果 |
|
與 |
|
如果 |
|
計算除法的餘數 |
|
1 |
|
計算兩個節點集 |
|
返回所有擁有 |
|
加法 |
|
10 |
|
減法 |
|
2 |
|
乘法 |
|
24 |
|
除法 |
|
2 |
|
等於 |
|
如果 |
|
不等於 |
|
如果 |
|
小於 |
|
如果 |
|
小於或等於 |
|
如果 |
|
大於 |
|
如果 |
|
大於或等於 |
|
如果 |
此表參考來源: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
節點的所有祖先節點,包括html
、body
、div
和ul
。
第二次選擇時,我們又加了限定條件,這次在冒號後面加了div
,這樣得到的結果就只有div
這個祖先節點了。
第三次選擇時,我們呼叫了attribute
軸,可以獲取所有屬性值,其後跟的選擇器還是*,這代表獲取節點的所有屬性,返回值就是li
節點的所有屬性值。
第四次選擇時,我們呼叫了child
軸,可以獲取所有直接子節點。這裡我們又加了限定條件,選取href
屬性為link1.html
的a
節點。
第五次選擇時,我們呼叫了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/。