Python爬蟲中如何使用xpath解析HTML
最近工作上寫了個(gè)爬蟲,要爬取國家標(biāo)準(zhǔn)網(wǎng)上的一些信息,這自然離不了 Python,而在解析 HTML 方面,xpath 則可當(dāng)仁不讓的成為兵器譜第一。
你可能之前聽說或用過其它的解析方式,像 Beautiful Soup,用的人好像也不少,但 xpath 與之相比,語法更簡單,解析速度更快,就像正則表達(dá)式一樣,剛上手要學(xué)習(xí)一番,然而用久了,那些規(guī)則自然而然的就記住了,熟練之后也很難忘記。
安裝 lxml
xpath 只是解析規(guī)則,其背后是要有相應(yīng)的庫來實(shí)現(xiàn)功能的,就像正則表達(dá)式只是規(guī)則,而 Python 內(nèi)置的 re 庫,則是提供了解析功能。在 Python 中,lxml 就是 xpath 解析的實(shí)現(xiàn)庫。
安裝 lxml 非常簡單,pip install lxml 就搞定了。
下面我們來看一下,在我這次真實(shí)的項(xiàng)目中,該如何發(fā)揮出它的威力。
加載 HTML 內(nèi)容
加載 HTML 內(nèi)容,應(yīng)該用 etree.parse()、etree.fromstring() 還是 etree.HTML() ?
首先,把 lxml 庫導(dǎo)進(jìn)來:from lxml import etree。
HTML 內(nèi)容的加載,是通過 etree 的方法載入的,具體有 3 個(gè)方法:parse()、fromstring() 和 HTML()。
parse() 是從文件加載。fromstring() 是從字符串加載。HTML() 也是從字符串加載,但是以 HTML 兼容的方式加載進(jìn)來的。
那我們應(yīng)該選哪個(gè)方法呢?別猶豫,選 etree.HTML(),即使你的 HTML 內(nèi)容來自文件。這是為何?
首先要說的一點(diǎn)是,HTML 也是 XML 的一種,而 XML 的標(biāo)準(zhǔn)規(guī)定,其必須擁有一個(gè)根標(biāo)簽,否則,這段 XML 就是非法的。而我們加載進(jìn)來的 HTML 內(nèi)容,可能本身就不是完整的,只是個(gè)片段,且沒有根標(biāo)簽;或是加載進(jìn)來的 HTML 從頭到腳看起來都是完整的,但是中間的節(jié)點(diǎn),有的缺少結(jié)束標(biāo)簽,這些情況,其實(shí)都是非法的 XML。那么,在用 parse() 或 formstring() 加載這種缺胳膊少腿的 HTML 的時(shí)候,就會(huì)報(bào)錯(cuò);而用 etree.HTML() 則不會(huì)。
這是因?yàn)?etree.HTML() 加載方式,有很好的 HTML 兼容性,它會(huì)補(bǔ)全缺胳膊少腿的 HTML,把它變成一個(gè)完整的、合法的 HTML。
下面是一個(gè)從文件加載 HTML 的例子:
from lxml import etree
with open('test.html', 'r') as f:
html = etree.HTML(f.read())
print(html, type(html))
打印出來的結(jié)果是:<Element html at 0x7f7efa762040> <class 'lxml.etree._Element'>,加載進(jìn)來的 HTML 字符串,已經(jīng)變成了 Element 對象。
后面我們通過 xpath 找 HTML 節(jié)點(diǎn),全都是在這個(gè) Element 對象上操作的。
找到你需要的 HTML 節(jié)點(diǎn)
下面是我想要找的 HTML 節(jié)點(diǎn)

在這個(gè) table 表格中,第一個(gè) tbody 是表頭,第二個(gè) tbody 是表內(nèi)容,我們要如何定位到第二個(gè) tbody ?
我們通常是調(diào)用上面獲得的 Element 對象的 xpath() 方法,通過傳入的 xpath 路徑查找的。而路徑有兩種寫法:一種是 / 開頭,從 html 根標(biāo)簽,沿著子節(jié)點(diǎn)一個(gè)個(gè)找下來;另一種是 // 開頭,即不論我們要找的節(jié)點(diǎn)在什么位置,找到就算,這種方式是最常用的。
比如,我們現(xiàn)在要找的 tbody 節(jié)點(diǎn),它在 table 節(jié)點(diǎn)下,我們就可以這樣寫:html.xpath('//table/tbody')。這里的 html 是上面獲得的 Element 對象,然后去找 HTML 內(nèi)容中的、不管在任何位置的所有的 table,找到后再繼續(xù)找它們下面的直接子節(jié)點(diǎn) tbody,于是就匹配出來了。
可是這里有 2 個(gè) tbody,我需要的是第二個(gè),我們可以在 [] 中寫條件表達(dá)式:html.xpath('//table/tbody[2]'),注意這里的序號(hào)是從 1 開始的。
強(qiáng)大的屬性選擇器
你可能有個(gè)疑問,如果 HTML 內(nèi)容中不只有一個(gè) table 表格,那我們通過 html.xpath('//table/tbody[2]') 豈不是找到了 2 個(gè) table 里的第二個(gè) tbody,而我需要的只是其中之一。沒錯(cuò),是存在這樣一個(gè)問題。此時(shí),我們就可以用屬性選擇器,來更精確的定位元素。

觀察一下上面的 HTML 結(jié)構(gòu),table 表格的最外層有一個(gè) div,它還有個(gè) class 屬性:table-responsive,假設(shè)這個(gè) div 的 class 屬性是整個(gè) HTML 里獨(dú)一無二的,那么我們就可以很放心的去查找 div.table-responsive 下的 table,進(jìn)而精確定位我們想要的元素。
那么,要怎樣寫 class = "table-responsive" 這個(gè)條件呢?看看上面寫條件表達(dá)式的 [],那里面除了可以寫數(shù)字來指定位置以外,也可以寫其它各式各樣的條件,比如:
html.xpath('//div[@class="table-responsive"]/table/tbody[2]'),這里我們就把 class = "table-responsive" 這個(gè)條件寫進(jìn)去了,從而定位到想要的元素。注意,在 xpath 中,所有的 HTML 屬性匹配都是以 @ 打頭的,比如有這樣一個(gè) <a href="#">Click Me</a> 元素,我們想要通過 id 定位它,可以這樣寫://a[@id="show_me"],是不是很簡單。
假設(shè)很遺憾,我們這里的 table-responsive 不是唯一的,可能還有其它地方的 div 的 class = "table-responsive",這該怎么辦?沒關(guān)系,我們可以找其它具有唯一 class 值的元素,比如:最外層 div 下的 table.result_list 這個(gè)元素,這個(gè)是唯一的。好了,下面開始寫定位代碼:html.xpath('//table[@class="result_list"]/tbody[2]'),但是運(yùn)行后,發(fā)現(xiàn)找不到元素,這是為什么?

其實(shí)仔細(xì)觀察一下就能發(fā)現(xiàn),這個(gè)元素的 class 里不只有 result_list,它還包括其它一長串的內(nèi)容:class = "table result_list table-striped table-hover",所以匹配失敗了。那要如何指定 class 包含某個(gè)屬性呢?其實(shí)可以在條件表達(dá)式中,用 contains() 函數(shù),無需精確匹配,而是模糊匹配,只要包含指定的字符串就可以了。比如:html.xpath('//table[contains(@class, "result_list")]/tbody[2]') 這樣就可以實(shí)現(xiàn)了。
需要提一點(diǎn)的是,xpath 定位到的元素,不管是不是全局唯一的,它的返回值都是一個(gè)列表,需要通過下標(biāo)獲取其中的元素。
相對定位
我最終的目標(biāo),是要遍歷表格中所有的內(nèi)容行,獲取其中的標(biāo)準(zhǔn)號(hào)和標(biāo)準(zhǔn)名稱,于是我初步完成了如下代碼:
from lxml import etree
with open('test.html', 'r') as f:
html = etree.HTML(f.read())
rows = html.xpath('//table[contains(@class, "result_list")]/tbody[2]/tr')
for row in rows:
td_list = row.xpath('...')
現(xiàn)在我能夠成功地定位到每一行,下面需要再基于每一行,找到我需要的列:

此時(shí),我在 for 循環(huán)的內(nèi)部,已經(jīng)拿到了每一行 row,再通過 row.xpath('//td') 繼續(xù)往下定位 td 就好了。
可是,當(dāng)你運(yùn)行這段代碼的時(shí)候,你會(huì)發(fā)現(xiàn)不對勁,一行里面總共只有 8 個(gè) td,為什么出來了 80 個(gè)【一行 8 個(gè),總共 10 行】?這是把 HTML 中所有的 td 都找出來了吧,可是我明明是用上面獲取的 row 對象來查找的呀,不是應(yīng)該只基于當(dāng)前行往下找嗎?
這就牽扯到了 絕對定位 和 相對定位。
其實(shí),我們上面講到的 / 和 //,都是絕對定位,也就是從 HTML 內(nèi)容的根節(jié)點(diǎn)往下查找。一個(gè) HTML 內(nèi)容的根節(jié)點(diǎn)是什么呢,它是 html,再往下是 body,再再往下才是自定義的標(biāo)簽。所以,上面代碼的執(zhí)行結(jié)果是那種情況,也就不足為奇了,因?yàn)樗皇窃诋?dāng)前所在的 row 節(jié)點(diǎn)查找的,而是從根節(jié)點(diǎn) /html/body/xxx/xxx/td 往下查找的呀。
所以,在這里不能用 絕對定位 了,要用 相對定位,那要如何用?很簡單,用 . 和 .. 即可,這個(gè)我們可太熟悉了,. 就代表了當(dāng)前節(jié)點(diǎn) row,而 .. 則代表了當(dāng)前節(jié)點(diǎn)的上一層父節(jié)點(diǎn) tbody。
好了,我們修正上面的代碼:
from lxml import etree
with open('test.html', 'r') as f:
html = etree.HTML(f.read())
rows = html.xpath('//table[contains(@class, "result_list")]/tbody[2]/tr')
for row in rows:
td_list = row.xpath('./td')
i = 1
for td in td_list:
if i == 2:
pass
elif i == 4:
pass
i += 1
這樣就可以正常地找到每一行里面的 8 個(gè) td,然后再單獨(dú)處理第 2 個(gè)和第 4 個(gè)單元格,獲取其中的信息就好了。
通過已知節(jié)點(diǎn)獲取屬性和文本
到目前為止,我們能拿到第 2 個(gè)和第 4 個(gè) td 節(jié)點(diǎn)了,只要再獲取里面的 a 標(biāo)簽的屬性和文本就可以了。

我們先獲取 onclick 屬性,通過 td.xpath('./a'),可以找到此 td 節(jié)點(diǎn)下面的 a 標(biāo)簽,然后調(diào)用 a 節(jié)點(diǎn)的 get() 方法,即可獲得對應(yīng)的屬性值,代碼如下:
a1 = td.xpath('./a')[0]
onclick = a1.get('onclick')
注意哦,xpath() 方法的返回值,始終是一個(gè)列表,所以我們用下標(biāo) [0] 先把它從列表中取出來,然后再獲取其屬性。 至于屬性內(nèi)的值,我實(shí)際想取的是里面的一串 ID 字符串,這個(gè)再用正則表達(dá)式取一下就可以了。
要獲取節(jié)點(diǎn)內(nèi)的文本,也很簡單,獲得到的節(jié)點(diǎn)有一個(gè) text 屬性,可以直接得到節(jié)點(diǎn)的文本內(nèi)容:a1.text。
好用的兄弟節(jié)點(diǎn)選擇器
上面的代碼邏輯有點(diǎn)挫,我們先是獲取到一行里的所有 td,然后循環(huán)遍歷它,在遍歷的過程當(dāng)中,只取其中的 2 個(gè) td,著實(shí)有些浪費(fèi)。假設(shè)一行里有 1000 個(gè) td,那這里豈不是要循環(huán) 1000 次,就只為了取 2 個(gè)?
雖然從實(shí)際運(yùn)行速度上來講,影響微乎其微,但對于有代碼潔癖和強(qiáng)迫癥的人來說,是不可接受的,所以,我們要改造它。
重新觀察一下 HTML 結(jié)構(gòu),我發(fā)現(xiàn)第 4 個(gè)單元格有個(gè)明顯的特征,它的 class = "mytxt":

我們可以很容易地找到它:title_td = tr.xpath('./td[@class="mytxt"]')[0],然后再基于剛找到的 title_td,查找從它往上數(shù)第 2 個(gè)兄弟節(jié)點(diǎn),這樣就省略了一個(gè)循環(huán),只要查找兩次就完成了。
那么,怎么查找上面的兄弟節(jié)點(diǎn)呢?用 preceing-sibling,比如:title_td.xpath('./preceding-sibling::td[2]'),這就代表要查找 title_td 上面的、從它這里往上數(shù)、排在第 2 位的 td 節(jié)點(diǎn)。
除了 preceding-sibling 之外,還有 following-sibling,顧名思意,是往下查找兄弟節(jié)點(diǎn)。
以上我只介紹了這 2 個(gè),其實(shí)還有很多類似的選擇器,具體可以參考下面的速查手冊。
最后,我改造的代碼如下:
from lxml import etree
with open('test.html', 'r') as f:
html = etree.HTML(f.read())
rows = html.xpath('//table[contains(@class, "result_list")]/tbody[2]/tr')
for row in rows:
title_td = row.xpath('./td[@class="mytxt"]')[0]
title_link = title_td.xpath('./a')[0]
title_onclick = title_link.get('onclick')
print(title_onclick, title_link.text)
id_td = title_td.xpath('./preceding-sibling::td[2]')[0]
id_link_text = id_td.xpath('./a/text()')[0]
print(id_link_text)
速查手冊
xpath 的規(guī)則并不復(fù)雜,常用的也就那些,用熟了自然就記住了。但像正則表達(dá)式一樣,它還有許多不常用卻很好用的特性,你還是需要偶爾查一下具體的作用和用法。
這里有一個(gè)非常好的速查手冊,雖然里面的內(nèi)容看起來不夠豐富、很簡單,但是可以一目了然,并且它用 css 的語法來作類比,就能夠更好地理解每一個(gè) xpath 規(guī)則的實(shí)際用途。
速查手冊:https://devhints.io/xpath
總結(jié)
文章詳細(xì)介紹了如何使用Python的lxml庫中的xpath進(jìn)行網(wǎng)頁數(shù)據(jù)爬取,解釋了xpath與BeautifulSoup相比的優(yōu)勢,介紹如何使用lxml庫加載HTML內(nèi)容,包括parse()、fromstring()和HTML()方法的使用,展示了如何使用xpath定位HTML節(jié)點(diǎn),包括使用絕對定位和相對定位。
到此這篇關(guān)于Python爬蟲中如何使用xpath解析HTML的文章就介紹到這了,更多相關(guān)Python爬蟲使用xpath解析HTML內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Pycharm加載多個(gè)項(xiàng)目過程圖解
這篇文章主要介紹了基于Pycharm加載多個(gè)項(xiàng)目過程圖解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
Python序列化基礎(chǔ)知識(shí)(json/pickle)
這篇文章主要為大家詳細(xì)介紹了Python序列化json和pickle基礎(chǔ)知識(shí),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
Python基礎(chǔ)教程之循環(huán)語句(for、while和嵌套循環(huán))
這篇文章主要給大家介紹了關(guān)于Python基礎(chǔ)教程之循環(huán)語句(for、while和嵌套循環(huán))的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
在tensorflow下利用plt畫論文中l(wèi)oss,acc等曲線圖實(shí)例
這篇文章主要介紹了在tensorflow下利用plt畫論文中l(wèi)oss,acc等曲線圖實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-06-06
Python內(nèi)建類型str源碼學(xué)習(xí)
這篇文章主要為大家介紹了Python內(nèi)建類型str的源碼學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
Python selenium實(shí)現(xiàn)斷言3種方法解析
這篇文章主要介紹了Python selenium實(shí)現(xiàn)斷言3種方法解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09

