Python3網絡爬蟲實戰---2八、解析庫的使用:XPath

上一篇文章: Python3網絡爬蟲實戰---2七、Requests與正則表達式抓取貓眼電影排行
下一篇文章:

上一節咱們實現了一個最基本的爬蟲,但提取頁面信息時咱們使用的是正則表達式,用過以後咱們會發現構造一個正則表達式仍是比較的繁瑣的,並且萬一有一點地方寫錯了就可能會致使匹配失敗,因此使用正則來提取頁面信息多多少少仍是有些不方便的。html

對於網頁的節點來講,它能夠定義 id、class 或其餘的屬性,並且節點之間還具備層次關係,在網頁中能夠經過 XPath 或 CSS 選擇器來定位一個或多個節點。那麼在頁面解析時,咱們利用 XPath 或 CSS 選擇器來提取到某個節點,而後再調用相應的方法去獲取它的正文內容或者屬性不就能夠提取咱們想要的任意信息了嗎?node

在 Python 中,咱們怎樣來實現這個操做呢?不用擔憂,這種解析庫已經很是多了,其中比較強大的庫有 LXML、BeautifulSoup、PyQuery 等等,本章咱們就來介紹一下這三個解析庫的使用,有了它們,咱們不用再爲正則發愁,並且解析效率也會大大提升,實爲爬蟲必備利器。正則表達式

XPath的使用

XPath,全稱 XML Path Language,即 XML 路徑語言,它是一門在XML文檔中查找信息的語言。XPath 最初設計是用來搜尋XML文檔的,可是它一樣適用於 HTML 文檔的搜索。segmentfault

因此在作爬蟲時,咱們徹底可使用 XPath 來作相應的信息抽取,本節咱們來介紹一下 XPath 的基本用法。網絡

1. XPath概覽

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

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

2. XPath經常使用規則

咱們現用表格列舉一下幾個經常使用規則:spa

表達式 描述
nodename 選取此節點的全部子節點
/ 從當前節點選取直接子節點
// 從當前節點選取子孫節點
. 選取當前節點
.. 選取當前節點的父節點
@ 選取屬性

在這裏列出了XPath的經常使用匹配規則,例如 / 表明選取直接子節點,// 表明選擇全部子孫節點,. 表明選取當前節點,.. 表明選取當前節點的父節點,@ 則是加了屬性的限定,選取匹配屬性的特定節點。設計

例如:code

//title[@lang=’eng’]

這就是一個 XPath 規則,它就表明選擇全部名稱爲 title,同時屬性 lang 的值爲 eng 的節點。

在後文咱們會介紹 XPath 的詳細用法,經過 Python 的 LXML 庫利用 XPath 進行 HTML 的解析。

3. 準備工做

在使用以前咱們首先要確保安裝好了 LXML 庫,如沒有安裝能夠參考第一章的安裝過程。

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 節點標籤被補全,而且還自動添加了 body、html 節點。

另外咱們也能夠直接讀取文本文件進行解析,示例以下:

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 類型,其後跟了節點的名稱,如 html、body、div、ul、li、a 等等,全部的節點都包含在列表中了。

固然此處匹配也能夠指定節點名稱,若是咱們想獲取全部 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.html 的 a 節點,而後再獲取其父節點,而後再獲取其 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. 屬性匹配

在選取的時候咱們還能夠用 @ 符號進行屬性過濾,好比在這裏若是咱們要選取 class 爲 item-1 的 li 節點,能夠這樣實現:

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)

運行結果以下:

['\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', '\n     ']

不出所料,這裏返回結果是三個,可想而知這裏是選取全部子孫節點的文本,其中前兩個就是 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 屬性有兩個值 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 屬性一般有多個。

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)

在這裏 HTML 文本的 li 節點又增長了一個屬性 name,這時候咱們須要同時根據 class 和 name 屬性來選擇,就能夠 and 運算符鏈接兩個條件,兩個條件都被中括號包圍,運行結果以下:

['first item']

這裏的 and 實際上是 XPath 中的運算符,另外還有不少運算符,如 or、mod 等等,在此總結以下:

運算符 描述 實例 返回值
or price=9.80 or price=9.70 若是 price 是 9.80,則返回 true。若是 price 是 9.50,則返回 false。
and price>9.00 and price<9.90 若是 price 是 9.80,則返回 true。若是 price 是 8.50,則返回 false。
mod 計算除法的餘數 5 mod 2 1
\ 計算兩個節點集 //book //cd 返回全部擁有 book 和 cd 元素的節點集
+ 加法 6 + 4 10
- 減法 6 - 4 2
* 乘法 6 * 4 24
div 除法 8 div 4 2
= 等於 price=9.80 若是 price 是 9.80,則返回 true。若是 price 是 9.90,則返回 false。
!= 不等於 price!=9.80 若是 price 是 9.90,則返回 true。若是 price 是 9.80,則返回 false。
< 小於 price<9.80 若是 price 是 9.00,則返回 true。若是 price 是 9.90,則返回 false。
<= 小於或等於 price<=9.80 若是 price 是 9.00,則返回 true。若是 price 是 9.90,則返回 false。
> 大於 price>9.80 若是 price 是 9.90,則返回 true。若是 price 是 9.80,則返回 false。
>= 大於或等於 price>=9.80 若是 price 是 9.90,則返回 true。若是 price 是 9.70,則返回 false。

此表參考來源:http://www.w3school.com.cn/xp...

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 的節點,獲得的結果就是前 2 個 li 節點。

第四次選擇咱們選取了倒數第三個 li 節點,中括號中傳入 last()-2便可,由於 last() 是最後一個,因此 last()-2 就是倒數第三個。

運行結果以下:

['first item']
['fifth item']
['first item', 'second item']
['third item']

在這裏咱們使用了 last()、position() 等函數,XPath 中提供了 100 多個函數,包括存取、數值、字符串、邏輯、節點、序列等處理功能,具體全部的函數做用能夠參考:http://www.w3school.com.cn/xp...

13. 節點軸選擇

XPath 提供了不少節點軸選擇方法,英文叫作 XPath Axes,包括獲取子元素、兄弟元素、父元素、祖先元素等等,在必定狀況下使用它能夠方便地完成節點的選擇,咱們用一個實例來感覺一下:

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 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/xp...

14. 結語

到如今爲止咱們基本上把可能用到的 XPath 選擇器介紹完了, XPath 功能很是強大,內置函數很是多,熟練使用以後能夠大大提高 HTML 信息的提取效率。

如想查詢更多 XPath 的用法能夠查看:http://www.w3school.com.cn/xp...

如想查詢更多 Python LXML 庫的用法能夠查看:http://lxml.de/

相關文章
相關標籤/搜索