Xpath最初被設計用來搜尋XML文檔,但它同樣適用於HTML文檔的搜索。通過簡潔明瞭的路徑選擇表達式,它提供了強大的選擇功能;同時得益於其內置的豐富的函數,它可以匹配和處理字元串、數值、時間等數據格式,幾乎所有節點我們都可以通過Xpath來定位。

在Python中,lxml庫為我們提供了完整的Xpath選擇器,今天我們就用它來學習Xpath的使用,我們的目標是用最少的時間來掌握使用頻率最高的核心技能,而這些核心技能基本上可以滿足我們網頁抓取的需求。

畢竟我們不是單獨在使用Xpath,在Python中,很多數據處理和匹配的工作我們可以用更加「Python」、更加通用的方法來解決,沒必要為了5%的使用而花費數倍的時間。

我們都知道,在很多領域裡,從0到80分只需要花費很少的時間,從80分到95分則可能會花費上一階段的數倍時間,至於從95分往上,每一分的提高都可能需要巨大的時間成本。我們需要權衡最初的學習訴求、收穫和時間成本的匹配度等,以判斷我們要到達哪一個水平,並規劃出對應的學習方案。

我學習爬蟲的目的並不是成為一個精通網路爬蟲的大師,而是將它作為一個工具,用來幫助我更好地進行數據挖掘分析的工作。因此,在學習過程中會儘可能地功力,力求以最少的時間掌握最核心的技能。Xpath簡直是針對這種學習思路設計的,因為它太容易上手了,核心功能只需要十分鐘就可以熟練掌握,而那多達上百的函數對我們來說可能一輩子都用不到幾回。


一、Xpath常用規則

下表是最常用的Xpath規則,絕大多數的Xpath表達式都由它們構成。

| 表達式 | 描述 | | :-: | :-: | | nodename | 選取此節點的所有子節點 | | / | 從當前節點直接選取子節點 | | // | 從當前節點選取所有子孫節點 | | . | 選取當前節點 | | .. | 選取當前節點的父節點 | | @ | 選取屬性 |

二、抓取趙雷熱門作品頁面

單純的羅列簡直是耍流氓,實戰纔是硬道理。正如標題所言,今天我們就使用Xpath來解析網易雲音樂的歌手頁面。我個人很喜歡趙雷,那我們就先嘗試解析一下趙雷的熱門作品。

網易雲音樂抓取難度較低,沒有亂七八糟的驗證,抓取的時候我們只需要帶上header就可以成功獲取我們需要的內容了。

首先,我們打開網易雲音樂的首頁,搜索並進入趙雷的頁面。右鍵檢查並切換到Network選項卡,刷新一下,就看到了一大串網路請求,我們要做的就是從中定位到歌曲列表所在的請求。

我們優先看document類的文件,第一個打開後通過preview可以看到這裡是通用內容,包含了一些網易雲音樂的信息,那麼接下來我們看下邊這個紅框裏的請求,首先請求名稱裏包含了artist以及一個對應的id,看起來有點像。

接下來我們單擊進去看看:

我們成功看到了趙雷的熱門作品列表,說明我們找對了位置。我們同樣可以通過在response裏搜索來確定這一請求是否是我們尋找的那一個。比如我們搜索「成都」、「南方姑娘」等,來看下我們的歌曲列表是不是在這個response中。

確定了請求之後,我們就需要抓取並解析了。首先我們切換到Headers選項,在General下找到Request URL作為請求連接;然後在Request Headers下找到『User-Agent』,並將其複製下來用作模擬瀏覽器發起請求。

接下來我們嘗試抓取頁面:

import requests

url = https://music.163.com/artist?id=6731
headers = {
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36
}
html = requests.get(url, headers=headers)
print(html.status_code)

結果如下:

200

這說明我們的請求成功了,接下來我們看下html的內容是否符合要求:

print(html.text)

這裡列印的結果太長就不貼出來了,我們可以把列印的內容和剛才那個請求返回的結果做一個比對,看是不是一樣的內容。通過觀察,我們發現這就是我們需要的內容。

三、解析熱門作品列表

1. 構建對象

那麼接下來就要解析了,解析之前,我們需要先使用lxml構建我們需要的對象:

from lxml import etree

result = etree.HTML(html.text)
print(type(result))
print(result)

輸出為:

<class lxml.etree._Element> <Element html at 0x7fdef020afc8>

2. 子節點、子孫節點、屬性過濾、文本選取

然後我們觀察網頁,定位到歌單位置:

我們發現歌曲列表藏在一個<ul>標籤中,這個標籤擁有class="f-hide"屬性;<ul>標籤中嵌套了一個<li>標籤;<li>標籤中又嵌套了一個<a>標籤,這個<a>標籤有個href屬性,其值為每首歌的相對鏈接,而歌曲名稱就在<a>標籤對應的文本中。

好了,接下來我們構建Xpath路徑選擇表達式:

song_list = result.xpath(//ul[@class="f-hide"]/li/a/text())
print(song_list)

看下輸出:

[成都, 南方姑娘, 理想, 畫, 我們的時光, 少年錦時, 阿刁, 鼓樓, 三十歲的女人, 無法長大, 八十年代的歌, 十九歲, 彩虹下面, 瑪麗, 讓我偷偷看你, 吉姆餐廳, 朵, 靜下來, 家鄉, 已是兩條路上的人, 未給姐姐遞出的信, 北京的冬天, 孤獨, 小屋, 再也不會去麗江, 再見北京, 媽媽, 夢中的哈德森, 人家, 背影, 窯上路, 浮游, 不開的脣, 開往北京的火車, 趙小雷, 往事只能回味, 愛人你在哪裡, 未給姐姐遞出的信 , Over, 民謠, 憑什麼說愛你, 逆流而上, 夏天, 米店, 飛來飛去, 朵兒 (Live版), 何必, 塔吉汗, 月亮粑粑 (Live)]

好,我們成功解析出了歌曲列表。那麼接下來我們回頭看一下這個表達式:

首先,我們使用.xpath()方法來調用Xpath表達式;//ul[@class="f-hide"]代表我們選取所有擁有class="f-hide"屬性的<ul>標籤;/li代表在上邊返回的<ul>標籤的子節點中選取所有的<li>標籤,/a同理,進一步選取子節點中的<a>標籤;/text()則是將結果中<a>標籤中的文本信息返回為一個列表。

是不是很簡單?Xpath使用起來完全符合我們的直觀感覺,非常人性化。

3. 父節點

那如果我想查看某個節點的父節點怎麼辦?很好辦,使用/..即可:

temp = result.xpath(//ul[@class="f-hide"]/../@class)
print(temp)

輸出為:

[u-slt f-ib]

也就是說,剛才我們定位到的<ul>節點的class屬性的值為u-slt f-ib。這裡我們使用..選取了父節點,並使用/@選取了該節點的class屬性。

4. 屬性選取

關於屬性的選取,我們再看一個例子:

link_list = result.xpath(//ul[@class="f-hide"]/li/a/@href)
print(link_list)

輸出為:

[/song?id=436514312, /song?id=202373, /song?id=29567189, /song?id=202369, /song?id=29567193, /song?id=29567192, /song?id=447925059, /song?id=447926067, /song?id=29567191, /song?id=437608773, /song?id=447925066, /song?id=530995556, /song?id=1295824647, /song?id=447925058, /song?id=33166602, /song?id=29567187, /song?id=447926063, /song?id=517567264, /song?id=29567188, /song?id=28111471, /song?id=202368, /song?id=29567185, /song?id=447925063, /song?id=29567194, /song?id=34852810, /song?id=447925067, /song?id=202377, /song?id=29567190, /song?id=202367, /song?id=202376, /song?id=447926068, /song?id=29567186, /song?id=202370, /song?id=202375, /song?id=202371, /song?id=433018045, /song?id=433018044, /song?id=29810320, /song?id=202374, /song?id=433018041, /song?id=432792901, /song?id=433018046, /song?id=432792905, /song?id=31460216, /song?id=433018047, /song?id=553546118, /song?id=432792904, /song?id=432792903, /song?id=460628183]

這次我們沒有選取歌名,而是通過<a>標籤的href屬性選取了每首歌的相對鏈接地址。通過這一地址,我們可以與music.163.com/#一起拼接出每首歌的實際地址。

5. 屬性多值情況處理

前邊我們通過//ul[@class="f-hide"]嘗試了按照屬性進行篩選,那麼當一個屬性有多個取值的時候我們怎麼辦呢?

觀察網頁,有這麼一個<div>標籤,它的class屬性有三個取值,假如我們想選取擁有class="sltbtn"屬性的<div>標籤,就會把它給漏掉。這時候,Xpath自帶的contains()函數就該出馬了:

temp2 = result.xpath(//div[contains(@class, "sltbtn")]/@class)
print(temp2)

輸出為:

[u-btn2 u-btn2-1 sltbtn]

我們成功通過class屬性的一個取值定位到了這個標籤,並選取了它的class屬性的所有取值。

6. 多屬性過濾處理

那麼還有一種常見的情況,就是假如我需要通過多個屬性來定位一個標籤的時候,應該怎麼辦呢?注意,這裡是多個屬性,上邊一種情況是一個屬性的多個取值。

針對多個屬性的情況,我們可以通過and運算符來連接,Xpath支持一系列的運算符,包括andor|、加減乘除以及大小判斷等。

以上邊這個例子來說,他有classdata-res-type等屬性,下面我們來看如何同時過濾他的兩個屬性值:

temp3 = result.xpath(//li[@class="choose" and @data-res-type>1]/text())
print(temp3)

輸出為:

[作詞作品, 作曲作品]

可以看到,由於我設置了@data-res-type>1,所以出來了兩條記錄,因為有兩個<li>標籤的@data-res-type屬性的取值分別為2和3。

7. 按順序選擇

best_song = result.xpath(//ul[@class="f-hide"]/li/a[1]/text())
last_song = result.xpath(//ul[@class="f-hide"]/li/a[last()]/text())
last_3_song = result.xpath(//ul[@class="f-hide"]/li/a[last()-2]/text())
best_3_songs = result.xpath(//ul[@class="f-hide"]/li/a[position()<4]/text())
print(best_song)
print(last_song)
print(last_3_song)
print(best_3_songs)

輸出為:

[成都] [月亮粑粑 (Live)] [何必] [成都, 南方姑娘, 理想]

我們使用[1]選取了第一個<li>標籤,使用[last()]選取了最後一個標籤,使用[last()-2]選取了倒數第三個標籤,使用[position()<4]選取了前三個標籤。

8. 節點軸選擇

Xpath有一系列的軸選擇方法,如下表所示:

| 軸名稱 | 結果 | | :-: | :-: | | ancestor | 選取當前節點的所有先輩(父、祖父等) | | ancestor-or-self | 選取當前節點的所有先輩(父、祖父等)以及當前節點本身 | | attribute | 選取當前節點的所有屬性 | | child | 選取當前節點的所有子元素 | | descendant | 選取當前節點的所有後代元素(子、孫等) | | descendant-or-self | 選取當前節點的所有後代元素(子、孫等)以及當前節點本身 | | following | 選取文檔中當前節點的結束標籤之後的所有節點 | | namespace | 選取當前節點的所有命名空間節點 | | parent | 選取當前節點的父節點 | | preceding | 選取文檔中當前節點的開始標籤之前的所有節點 | | preceding-sibling | 選取當前節點之前的所有同級節點 | | self | 選取當前節點 |

這些軸選擇有一些跟上邊提到的方法是一樣的,大部分都能通過前邊提到的///..等組合而來。但很多情況下,直接使用這些軸會更加快捷。軸選擇的使用方法如下,以剛才的父節點為例,我們將..替換為parent::**代表選取父節點中的所有節點,當然,事實上這裡只有1個節點,不過在::之後必須要選擇至少一個節點,這是語法的要求,所以我們用*作為後綴加在parent::後邊。

temp = result.xpath(//ul[@class="f-hide"]/parent::/@class)
print(temp)

好了,今天我們演示瞭如何通過Xpath解析網頁數據,上述內容基本涵蓋了爬取網頁過程中的絕大多數解析需求,我們只需多加練習,能夠在不同場景下靈活組合這些基本技能,就可以順暢地從HTML文本中獲取我們所需要的信息了。

推薦閱讀:

相關文章