【Python3 爬蟲學習筆記】解析庫的使用 2 —— 使用XPath 2
8. 文字獲取
我們使用XPath中的text()方法獲取節點中文字,接下來嘗試獲取前面li節點中的文字,相關程式碼如下:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]/test()')
print(result)
執行結果如下:
['\r\n']
從結果我們可以看到,沒有獲得任何文字,只獲得了\r\n,主要是因為在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-0的li節點的文字。
這裡我們是逐層選取的,先選取了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', '\r\n']
可以看到,這裡選取所有子節點的文字,其中前兩個就是li的子節點a節點內部的文字,另外一個就是最後一個li節點內部的文字,即換行符。
所以說,如果想要獲取子孫節點內部的所有文字,可以直接用//加text()的方式,這樣可以保證獲取到最全面的文字資訊,但是可能會夾雜一些換行符等特殊字元。如果想獲取某些特定子孫節點下的所有文字,可以先選取到特定的子孫節點,然後再呼叫text()方法獲取其內部文字,這樣可以保證獲取的結果是整潔的。
9. 屬性獲取
可以使用@符號進行屬性獲取。
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屬性,它們以列表形式返回。
10. 屬性多值匹配
有時候,某些節點的某個屬性可能有多個值,例如:
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,此時如果還想用之前的屬性匹配獲取,就無法匹配了,此時的執行結果如下:
[]
這時就需要用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屬性通常有多個。
11. 多屬性匹配
另外,我們可能還會遇到一種情況,那就是根據多個屬性確定一個節點,這時就需要同時匹配多個屬性。此時可以使用運算子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。要確定這個節點,需要同時根據class和name屬性來選擇,一個條件是class屬性裡面包含li字串,另一個條件是name屬性為item字串,二者需要同時滿足,需要用and操作符相連,相連之後置於中括號內進行條件篩選。執行結果如下:
['first item']
這裡的and其實是XPath中的運算子。另外,還有很多運算子,如or、mod等,運算子如下表:
運算子 | 描述 | 例項 | 返回值 |
---|---|---|---|
or | 或 | age=19 or age=20 | 如果age是19,則返回true。如果age是21,則返回false |
and | 與 | age>19 and age<21 | 如果age是20,則返回true。如果age是18,則返回false |
mod | 計算除法的餘數 | 5 mod 2 | 1 |
+ | 加法 | 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 |
12. 按序選擇
有時候,我們在選擇的時候某些屬性可能同時匹配了多個節點,但是隻想要其中的某個節點,如第二個節點或者最後一個節點,這時可以利用中括號傳入索引的方法獲取特定的次序的節點,示例如下:
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(0-2就是倒數第三個。
執行結果如下:
['first item']
['fifth item']
['first item', 'second item']
['third item']
這裡我們使用了last()、position()等函式。在XPath中,提供了100多個函式,包括存取、數值、字串、邏輯、節點、序列等處理功能,它們的具體作用可以參考:http://www.w3school.com.cn/xpath/xpath_functions.asp。
15. 節點軸選擇
XPath提供了很多節點軸選擇方法,包括獲取子元素、兄弟元素、父元素、祖先元素等,示例如下:
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::*')
print(result)
執行結果如下:
[<Element html at 0x287aaf8e688>, <Element body at 0x287aaf8e4c8>, <Element div at 0x287aaf8e388>, <Element ul at 0x287aaf8e3c8>]
[<Element div at 0x287aaf8e388>]
['item-0']
[<Element a at 0x287aaf8e3c8>]
[<Element span at 0x287aaf8e388>]
[<Element a at 0x287aaf8e3c8>]
[<Element li at 0x287aaf8e4c8>, <Element li at 0x287aaf8e288>, <Element li at 0x287aaf8e208>, <Element li at 0x287aaf8e708>]
第一次選擇時,我們呼叫了ancestor軸,可以獲取所有祖先節點。其後需要跟兩個冒號,然後是節點的選擇器,這裡我們直接使用*,表示匹配所有節點,因此返回結果是第一個li節點的所有祖先節點,包括html、body、div和ul。
第二次選擇時,我們又加了限定條件,這次在冒號後面加了div,這樣得到的結果就只有div這個祖先節點了。
第三次選擇時,我們呼叫了attribute軸,可以獲取所有屬性值,其後跟的選擇器還是(星號),這代表獲取節點的所有屬性,返回值就是li節點的所有屬性值。
第四次選擇時,我們呼叫child軸,可以獲取所有直接子節點。這裡我們又加了限定條件,選取href屬性為link1.html的a節點。
第五次選擇時,我們呼叫了descendant軸,可以獲取所有子孫節點。這裡我們又加了限定條件獲取span節點,所以返回的結果只包含span節點而不包含a節點。
第六次選擇時,我們呼叫了following軸,可以獲取當前節點之後的所有節點。這裡我們雖然使用的是(星號)匹配,但又加了索引選擇,所以只獲取了第二個後續節點。
第七次選擇時,我們呼叫了following-sibing軸,可以獲取當前節點之後的所有同級節點。這裡我們使用(星號)匹配,所以獲取了所有後續同級節點。