Python爬蟲中如何使用xpath解析HTML
最近工作上寫了個爬蟲,要爬取國家標(biāo)準(zhǔn)網(wǎng)上的一些信息,這自然離不了 Python,而在解析 HTML 方面,xpath 則可當(dāng)仁不讓的成為兵器譜第一。
你可能之前聽說或用過其它的解析方式,像 Beautiful Soup,用的人好像也不少,但 xpath 與之相比,語法更簡單,解析速度更快,就像正則表達(dá)式一樣,剛上手要學(xué)習(xí)一番,然而用久了,那些規(guī)則自然而然的就記住了,熟練之后也很難忘記。
安裝 lxml
xpath 只是解析規(guī)則,其背后是要有相應(yīng)的庫來實現(xiàn)功能的,就像正則表達(dá)式只是規(guī)則,而 Python 內(nèi)置的 re 庫,則是提供了解析功能。在 Python 中,lxml 就是 xpath 解析的實現(xiàn)庫。
安裝 lxml 非常簡單,pip install lxml 就搞定了。
下面我們來看一下,在我這次真實的項目中,該如何發(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 個方法:parse()、fromstring() 和 HTML()。
parse() 是從文件加載。fromstring() 是從字符串加載。HTML() 也是從字符串加載,但是以 HTML 兼容的方式加載進(jìn)來的。
那我們應(yīng)該選哪個方法呢?別猶豫,選 etree.HTML(),即使你的 HTML 內(nèi)容來自文件。這是為何?
首先要說的一點是,HTML 也是 XML 的一種,而 XML 的標(biāo)準(zhǔn)規(guī)定,其必須擁有一個根標(biāo)簽,否則,這段 XML 就是非法的。而我們加載進(jìn)來的 HTML 內(nèi)容,可能本身就不是完整的,只是個片段,且沒有根標(biāo)簽;或是加載進(jìn)來的 HTML 從頭到腳看起來都是完整的,但是中間的節(jié)點,有的缺少結(jié)束標(biāo)簽,這些情況,其實都是非法的 XML。那么,在用 parse() 或 formstring() 加載這種缺胳膊少腿的 HTML 的時候,就會報錯;而用 etree.HTML() 則不會。
這是因為 etree.HTML() 加載方式,有很好的 HTML 兼容性,它會補(bǔ)全缺胳膊少腿的 HTML,把它變成一個完整的、合法的 HTML。
下面是一個從文件加載 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é)點,全都是在這個 Element 對象上操作的。
找到你需要的 HTML 節(jié)點
下面是我想要找的 HTML 節(jié)點
在這個 table 表格中,第一個 tbody 是表頭,第二個 tbody 是表內(nèi)容,我們要如何定位到第二個 tbody ?
我們通常是調(diào)用上面獲得的 Element
對象的 xpath()
方法,通過傳入的 xpath 路徑查找的。而路徑有兩種寫法:一種是 /
開頭,從 html
根標(biāo)簽,沿著子節(jié)點一個個找下來;另一種是 //
開頭,即不論我們要找的節(jié)點在什么位置,找到就算,這種方式是最常用的。
比如,我們現(xiàn)在要找的 tbody
節(jié)點,它在 table
節(jié)點下,我們就可以這樣寫:html.xpath('//table/tbody')
。這里的 html
是上面獲得的 Element
對象,然后去找 HTML 內(nèi)容中的、不管在任何位置的所有的 table
,找到后再繼續(xù)找它們下面的直接子節(jié)點 tbody
,于是就匹配出來了。
可是這里有 2 個 tbody
,我需要的是第二個,我們可以在 []
中寫條件表達(dá)式:html.xpath('//table/tbody[2]')
,注意這里的序號是從 1
開始的。
強(qiáng)大的屬性選擇器
你可能有個疑問,如果 HTML 內(nèi)容中不只有一個 table
表格,那我們通過 html.xpath('//table/tbody[2]')
豈不是找到了 2 個 table
里的第二個 tbody
,而我需要的只是其中之一。沒錯,是存在這樣一個問題。此時,我們就可以用屬性選擇器,來更精確的定位元素。
觀察一下上面的 HTML 結(jié)構(gòu),table
表格的最外層有一個 div
,它還有個 class 屬性:table-responsive
,假設(shè)這個 div 的 class 屬性是整個 HTML 里獨一無二的,那么我們就可以很放心的去查找 div.table-responsive
下的 table
,進(jìn)而精確定位我們想要的元素。
那么,要怎樣寫 class = "table-responsive"
這個條件呢?看看上面寫條件表達(dá)式的 []
,那里面除了可以寫數(shù)字來指定位置以外,也可以寫其它各式各樣的條件,比如:
html.xpath('//div[@class="table-responsive"]/table/tbody[2]')
,這里我們就把 class = "table-responsive"
這個條件寫進(jìn)去了,從而定位到想要的元素。注意,在 xpath 中,所有的 HTML 屬性匹配都是以 @
打頭的,比如有這樣一個 <a href="#">Click Me</a>
元素,我們想要通過 id 定位它,可以這樣寫://a[@id="show_me"]
,是不是很簡單。
假設(shè)很遺憾,我們這里的 table-responsive
不是唯一的,可能還有其它地方的 div 的 class = "table-responsive",這該怎么辦?沒關(guān)系,我們可以找其它具有唯一 class 值的元素,比如:最外層 div 下的 table.result_list
這個元素,這個是唯一的。好了,下面開始寫定位代碼:html.xpath('//table[@class="result_list"]/tbody[2]')
,但是運(yùn)行后,發(fā)現(xiàn)找不到元素,這是為什么?
其實仔細(xì)觀察一下就能發(fā)現(xiàn),這個元素的 class 里不只有 result_list
,它還包括其它一長串的內(nèi)容:class = "table result_list table-striped table-hover"
,所以匹配失敗了。那要如何指定 class 包含某個屬性呢?其實可以在條件表達(dá)式中,用 contains()
函數(shù),無需精確匹配,而是模糊匹配,只要包含指定的字符串就可以了。比如:html.xpath('//table[contains(@class, "result_list")]/tbody[2]')
這樣就可以實現(xiàn)了。
需要提一點的是,xpath 定位到的元素,不管是不是全局唯一的,它的返回值都是一個列表,需要通過下標(biāo)獲取其中的元素。
相對定位
我最終的目標(biāo),是要遍歷表格中所有的內(nèi)容行,獲取其中的標(biāo)準(zhǔn)號和標(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)在我能夠成功地定位到每一行,下面需要再基于每一行,找到我需要的列:
此時,我在 for 循環(huán)的內(nèi)部,已經(jīng)拿到了每一行 row
,再通過 row.xpath('//td')
繼續(xù)往下定位 td
就好了。
可是,當(dāng)你運(yùn)行這段代碼的時候,你會發(fā)現(xiàn)不對勁,一行里面總共只有 8 個 td,為什么出來了 80 個【一行 8 個,總共 10 行】?這是把 HTML 中所有的 td 都找出來了吧,可是我明明是用上面獲取的 row
對象來查找的呀,不是應(yīng)該只基于當(dāng)前行往下找嗎?
這就牽扯到了 絕對定位
和 相對定位
。
其實,我們上面講到的 /
和 //
,都是絕對定位,也就是從 HTML 內(nèi)容的根節(jié)點往下查找。一個 HTML 內(nèi)容的根節(jié)點是什么呢,它是 html
,再往下是 body
,再再往下才是自定義的標(biāo)簽。所以,上面代碼的執(zhí)行結(jié)果是那種情況,也就不足為奇了,因為它不是在當(dāng)前所在的 row
節(jié)點查找的,而是從根節(jié)點 /html/body/xxx/xxx/td
往下查找的呀。
所以,在這里不能用 絕對定位
了,要用 相對定位
,那要如何用?很簡單,用 .
和 ..
即可,這個我們可太熟悉了,.
就代表了當(dāng)前節(jié)點 row
,而 ..
則代表了當(dāng)前節(jié)點的上一層父節(jié)點 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 個 td
,然后再單獨處理第 2 個和第 4 個單元格,獲取其中的信息就好了。
通過已知節(jié)點獲取屬性和文本
到目前為止,我們能拿到第 2 個和第 4 個 td
節(jié)點了,只要再獲取里面的 a
標(biāo)簽的屬性和文本就可以了。
我們先獲取 onclick
屬性,通過 td.xpath('./a')
,可以找到此 td
節(jié)點下面的 a
標(biāo)簽,然后調(diào)用 a
節(jié)點的 get()
方法,即可獲得對應(yīng)的屬性值,代碼如下:
a1 = td.xpath('./a')[0] onclick = a1.get('onclick')
注意哦,xpath()
方法的返回值,始終是一個列表,所以我們用下標(biāo) [0]
先把它從列表中取出來,然后再獲取其屬性。 至于屬性內(nèi)的值,我實際想取的是里面的一串 ID 字符串,這個再用正則表達(dá)式取一下就可以了。
要獲取節(jié)點內(nèi)的文本,也很簡單,獲得到的節(jié)點有一個 text
屬性,可以直接得到節(jié)點的文本內(nèi)容:a1.text
。
好用的兄弟節(jié)點選擇器
上面的代碼邏輯有點挫,我們先是獲取到一行里的所有 td
,然后循環(huán)遍歷它,在遍歷的過程當(dāng)中,只取其中的 2 個 td
,著實有些浪費。假設(shè)一行里有 1000 個 td
,那這里豈不是要循環(huán) 1000 次,就只為了取 2 個?
雖然從實際運(yùn)行速度上來講,影響微乎其微,但對于有代碼潔癖和強(qiáng)迫癥的人來說,是不可接受的,所以,我們要改造它。
重新觀察一下 HTML 結(jié)構(gòu),我發(fā)現(xiàn)第 4 個單元格有個明顯的特征,它的 class = "mytxt"
:
我們可以很容易地找到它:title_td = tr.xpath('./td[@class="mytxt"]')[0]
,然后再基于剛找到的 title_td
,查找從它往上數(shù)第 2 個兄弟節(jié)點,這樣就省略了一個循環(huán),只要查找兩次就完成了。
那么,怎么查找上面的兄弟節(jié)點呢?用 preceing-sibling
,比如:title_td.xpath('./preceding-sibling::td[2]')
,這就代表要查找 title_td
上面的、從它這里往上數(shù)、排在第 2 位的 td
節(jié)點。
除了 preceding-sibling
之外,還有 following-sibling
,顧名思意,是往下查找兄弟節(jié)點。
以上我只介紹了這 2 個,其實還有很多類似的選擇器,具體可以參考下面的速查手冊。
最后,我改造的代碼如下:
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á)式一樣,它還有許多不常用卻很好用的特性,你還是需要偶爾查一下具體的作用和用法。
這里有一個非常好的速查手冊,雖然里面的內(nèi)容看起來不夠豐富、很簡單,但是可以一目了然,并且它用 css 的語法來作類比,就能夠更好地理解每一個 xpath 規(guī)則的實際用途。
速查手冊: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é)點,包括使用絕對定位和相對定位。
到此這篇關(guān)于Python爬蟲中如何使用xpath解析HTML的文章就介紹到這了,更多相關(guān)Python爬蟲使用xpath解析HTML內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python序列化基礎(chǔ)知識(json/pickle)
這篇文章主要為大家詳細(xì)介紹了Python序列化json和pickle基礎(chǔ)知識,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-10-10Python基礎(chǔ)教程之循環(huán)語句(for、while和嵌套循環(huán))
這篇文章主要給大家介紹了關(guān)于Python基礎(chǔ)教程之循環(huán)語句(for、while和嵌套循環(huán))的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03在tensorflow下利用plt畫論文中l(wèi)oss,acc等曲線圖實例
這篇文章主要介紹了在tensorflow下利用plt畫論文中l(wèi)oss,acc等曲線圖實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-06-06Python內(nèi)建類型str源碼學(xué)習(xí)
這篇文章主要為大家介紹了Python內(nèi)建類型str的源碼學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05Python selenium實現(xiàn)斷言3種方法解析
這篇文章主要介紹了Python selenium實現(xiàn)斷言3種方法解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-09-09