XPath,全稱XML Path Language,即XML路徑語言,它是一門在XML文檔中查找信息的語言。它最初是用來搜尋XML文檔的,可是它一樣適用於HTML文檔的搜索。html
因此在作爬蟲時,咱們徹底可使用XPath來作相應的信息抽取。本節中,咱們就來介紹XPath的基本用法。node
XPath的選擇功能十分強大,它提供了很是簡潔明瞭的路徑選擇表達式。另外,它還提供了超過100個內建函數,用於字符串、數值、時間的匹配以及節點、序列的處理等。幾乎全部咱們想要定位的節點,均可以用XPath來選擇。python
XPath於1999年11月16日成爲W3C標準,它被設計爲供XSLT、XPointer以及其餘XML解析軟件使用,更多的文檔能夠訪問其官方網站:https://www.w3.org/TR/xpath/。git
表4-1列舉了XPath的幾個經常使用規則。github
表4-1 XPath經常使用規則網絡
表達式app |
描述函數 |
---|---|
|
選取此節點的全部子節點ui |
|
從當前節點選取直接子節點 |
|
從當前節點選取子孫節點 |
|
選取當前節點 |
|
選取當前節點的父節點 |
|
選取屬性 |
這裏列出了XPath的經常使用匹配規則,示例以下:
1
|
//title[@lang='eng']
|
這就是一個XPath規則,它表明選擇全部名稱爲title
,同時屬性lang
的值爲eng
的節點。
後面會經過Python的lxml庫,利用XPath進行HTML的解析。
使用以前,首先要確保安裝好lxml庫,若沒有安裝,能夠參考第1章的安裝過程。
如今經過實例來感覺一下使用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
節點標籤被補全,而且還自動添加了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>
|
咱們通常會用//開頭的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]
。
咱們經過/
或//
便可查找元素的子節點或子孫節點。假如如今想選擇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
|
[]
|
所以,這裏咱們要注意/
和//
的區別,其中/
用於獲取直接子節點,//
用於獲取子孫節點。
咱們知道經過連續的/
或//
能夠查找子節點或子孫節點,那麼假如咱們知道了子節點,怎樣來查找父節點呢?這能夠用..
來實現。
好比,如今首先選中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)
|
在選取的時候,咱們還能夠用@
符號進行屬性過濾。好比,這裏若是要選取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>]
|
可見,匹配結果正是兩個,至因而不是那正確的兩個,後面再驗證。
咱們用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()
方法獲取其內部文本,這樣能夠保證獲取的結果是整潔的。
咱們知道用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
屬性,它們以列表形式返回。
有時候,某些節點的某個屬性可能有多個值,例如:
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
屬性一般有多個。
另外,咱們可能還遇到一種狀況,那就是根據多個屬性肯定一個節點,這時就須要同時匹配多個屬性。此時可使用運算符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。
有時候,咱們在選擇的時候某些屬性可能同時匹配了多個節點,可是隻想要其中的某個節點,如第二個節點或者最後一個節點,這時該怎麼辦呢?
這時能夠利用中括號傳入索引的方法獲取特定次序的節點,示例以下:
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。
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。
到如今爲止,咱們基本上把可能用到的XPath選擇器介紹完了。XPath功能很是強大,內置函數很是多,熟練使用以後,能夠大大提高HTML信息的提取效率。
若是想查詢更多XPath的用法,能夠查看:http://www.w3school.com.cn/xpath/index.asp。
若是想查詢更多Python lxml庫的用法,能夠查看http://lxml.de/。
轉載請註明:靜覓 » [Python3網絡爬蟲開發實戰] 4.1-使用XPath