? ? ? ? 在提取網頁信息時,最基礎的方法是使用正則表達式,但過程比較煩瑣且容易出錯。對于網頁節點來說,可以定義id、class或其他屬性,而且節點之間還有層次關系,在網頁中可以通過XPath或CSS選擇器來定位一個或多個節點。那么,在解析頁面時,利用XPath或CSS選擇器提取某個節點,然后調用相應方法獲取該節點的正文內容或者屬性,就可以提取我們想要的任意信息了。
? ? ? ? 在Python中,如何實現上述操作呢?不用擔心,相關解析庫有非常多,其中比較強大的有lxml、Beautiful Soup,parsel、pyquery等。此帖介紹使用lxml庫來定位網頁源代碼所需部分。(哇哦,Python也太強大了!又對Python深愛了一份!!!)
目錄
XPath的使用
1. XPath概覽
2. XPath常用規則
3.準備工作
4.實例引入
5、所有節點
6.子節點
7.父節點
8、屬性匹配
?9、文本獲取
10.屬性獲取
11.屬性多值匹配
12.多屬性匹配
13.按序選擇
XPath的使用
XPath 的全稱是 XML Path Language, 即XML 路徑語言, 用來在 XML 文檔中查找信息。它雖然最初是用來搜尋 XML 文檔的,但同樣適用于 HTML 文檔的搜索。
所以在做爬蟲時,我們完全可以使用XPath實現相應的信息抽取。本節我們就介紹一下 XPath的基本用法。
1. XPath概覽
XPath的選擇功能十分強大,它提供了非常簡潔明了的路徑選擇表達式。另外,它還提供了 100多個內建函數,用于字符串、數值、時間的匹配以及節點、序列的處理等。幾乎所有我們想要定位的節點, 都可以用XPath選擇。
XPath于1999年11月16日成為 W3C標準,它被設計出來, 供XSLT、XPointer以及其他XML解析軟件使用。
2. XPath常用規則
????????下表列舉了 XPath的幾個常用規則。
表 達 式 | 描 | 述 | |
nodename | 選取此節點的所有子節點 | ||
/ | 從當前節點選取直接子節點 | ||
// | 從當前節點選取子孫節點 | ||
. | 選取當前節點 | ||
. . | 選取當前節點的父節點 | ||
@ | 選取屬性 |
????????這里列出了 XPath 的一個常用匹配規則,如下:
//title[@lang='eng']
????????它代表選擇所有名稱為 title,同時屬性 lang 的值為 eng的節點。后面會通過 Python 的 lxml庫, 利用XPath對HTML 進行解析。
3.準備工作
????????使用lxml庫之前,首先要確保其已安裝好。可以使用 pip3 來安裝:
pip3 install 1xml
更詳細的安裝說明可以參考:https://setup.scrape.center/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類型,結果如下:
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>
'''
????????可以看到,經過處理之后的 li 節點標簽得以補全,并且自動添加了 body、html節點。另外,也可以不聲明,直接讀取文本文件。?首先要將HTML文本新建一個html程序,然后采用調用的方式
test.html文件代碼:(該html文本一定需要創建到與一下Python程序同一目錄下,后面都是基于此html代碼進行分析)
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>
'''
Python代碼:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))
? ? ? ? ?此次輸出結果略有不同,多了一個DOCTYPE聲明,不過對解析無任何影響,結果如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p>text = '''
</p><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文本為例,選取其中所有節點。
Python代碼:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//*')
pprint.pprint(result)
結果如下:
[<Element html at 0x2a9cf235280>,<Element body at 0x2a9cf4ea780>,<Element p at 0x2a9cf4ea7c0>,<Element div at 0x2a9cf4ea040>,<Element ul at 0x2a9cf4ea200>,<Element li at 0x2a9cf4ea680>,<Element a at 0x2a9cf4ea700>,<Element li at 0x2a9cf4ea740>,<Element a at 0x2a9cf4ea8c0>,<Element li at 0x2a9cf4ea540>,<Element a at 0x2a9cf4eaa40>,<Element li at 0x2a9cf4eaa00>,<Element a at 0x2a9cf4ea840>,<Element li at 0x2a9cf4eab80>,<Element a at 0x2a9cf4eabc0>]
????????這里使用*代表匹配所有節點,也就是獲取整個HTML 文本中的所有節點。從運行結果可以看到返回形式是一個列表,其中每個元素是Element類型,類型后面跟著節點的名稱,如html、body、div?ul、li、a等,所有節點都包含在了列表中。當然,此處匹配也可以指定節點名稱。例如想獲取所有 li 節點,實例如下:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li')
pprint.pprint(result)
pprint.pprint(result[0])
????????這里選取所有li 節點,可以使用//,然后直接加上節點名稱,調用時使用xpath方法即可。
運行結果如下:
[<Element li at 0x1c5211da700>,<Element li at 0x1c5211da740>,<Element li at 0x1c5211d9fc0>,<Element li at 0x1c5211da180>,<Element li at 0x1c5211da4c0>]
<Element li at 0x1c5211da700>
????????可以看到,提取結果也是一個列表,其中每個元素都是 Element類型。要是想取出其中一個對象可以直接用中括號加索引獲取,如[0]。
6.子節點
通過/ 或//即可查找元素的子節點或子孫節點。假如現在想選擇 li 節點的所有直接子節點a,可以這樣實現:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a')
pprint.pprint(result)
? ? ? ? 這里通過追加/a的方式,選擇了li節點的所有直接子節點a。其中//li用于選中所有li節點,/a用于選中li節點的所有直接子節點a。
運行結果如下 :
[<Element a at 0x1c663e9a780>,<Element a at 0x1c663e9a7c0>,<Element a at 0x1c663e9a040>,<Element a at 0x1c663e9a200>,<Element a at 0x1c663e9a540>]
? ? ? ? 上面的/用于選取節點的直接子節點,如果要獲取節點的所有孫子節點,可以使用//。例如:要獲取ul節點下的所有子孫節點a,可以這樣實現:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//ul//a')
pprint.pprint(result)
????????運行結果與上面相同。如果這里用//ul/a,就無法獲取結果了。因為/用于獲取直接子節點,二ul節點下沒有直接的a子節點,只有li子節點。因此要注意/和//的區別,前者用于獲取直接子節點,后者用于獲取子孫節點。
7.父節點
????????通過連續的 /或 //可以查找子節點或子孫節點,那么假如知道了子節點,怎樣查找父節點呢?這可以用..實現。例如, 首先選中 href屬性為 link4. html的a節點, 然后獲取其父節點,再獲取父節點的 class 屬性,相關代碼如下:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//a[@href="link4.html"]/../@class')
pprint.pprint(result)
運行結果如下:
['item-1']
????????檢查一下結果發現,這正是我們獲取的目標li節點的 class 屬性。
也可以通過 parent::獲取父節點,代碼如下:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//a[@href="link4.html"]/parent::*/@class')
pprint.pprint(result)
8、屬性匹配
? ? ? ? 在選取節點的時候,還可以使用@符號實現屬性過濾。例如,要選取class屬性為item-0的li節點,可以這樣實現:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]')
pprint.pprint(result)
結果如下:
[<Element li at 0x1cf3f68a7c0>, <Element li at 0x1cf3f68a040>]
?9、文本獲取
? ? ? ? 用XPath中的text方法可以獲取節點中的文本,接下來嘗試獲取前面li節點中的文本,代碼如下:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]/text()')
pprint.pprint(result)
結果如下:
['\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節點的尾標簽和a節點的尾標簽之間的換行符。因此,如果想獲取 li 節點內部的文本,就有兩種方式,一種是先選取 a 節點再獲取文本,另一種是使用//。接下來,我們看下兩種方式的區別。
先選取a節點,再獲取文本的代碼如下:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]/a/text()')
pprint.pprint(result)
結果如下:
['first item', 'fifth item']
????????可以看到,這里有兩個返回值,內容都是 class 屬性為 item-0 的 1i 節點的文本,這也印證了前面屬性匹配的結果是正確的。這種方式下,我們是逐層選取的,先選取li節點,然后利用/選取其直接子節點a,再選取節點a的文本,得到的兩個結果恰好是符合我們預期的。這種方式下,我們是逐層選取的,先選li節點,然后利用/選取其直接子節點a,再選取節點a 的文本,得到的兩個結果恰好是符合我們預期的。再來看一下使用//能夠獲取什么樣的結果,代碼如下:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]//text()')
pprint.pprint(result)
運行結果如下:
['first item', 'fifth item', '\r\n ']
????????不出所料,這里的返回結果是三個。可想而知,這里選取的是所有子孫節點的文本,其中前兩個是 li 的子節點 a 內部的文本,另外一個是最后一個 li 節點內部的文本,即換行符。由此,要想獲取子孫節點內部的所有文本,可以直接使用//加text 方法的方式,這樣能夠保證獲取最全面的文本信息,但是可能會夾雜一些換行符等特殊字符。如果想獲取某些特定子孫節點下的所有文本,則可以先選取特定的子孫節點,再調用 text方法獲取其內部的文本,這樣可以保證獲取的結果是整潔的。
10.屬性獲取
????????我們已經可以用text方法獲取節點內部文本,那么節點屬性該怎樣獲取呢? 其實依然可以用@符號。例如,通過如下代碼獲取所有 li 節點下所有 a 節點的 href屬性:
import pprint
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a/@href')
pprint.pprint(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']
? ? ? ? contains方法經常在某個節點的某個屬性有多個值用到。
12.多屬性匹配
? ? ? ? 我們還可能遇到一種情況,?就是根據多個屬性確定一個節點,?這時需要同時匹配多個屬性。運算符and用于連接多個屬性,?實例如下:
from lxml import etreetext = '''
<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。因此要確定?li?節點,?需要同時考察?class?和?name?屬性,一個條件是class?屬性里面包含li字符串,?另一個條件是?name?屬性為item字符串,?這二者同時得到滿足,?才是?li?節點。class?和?name?屬性需要用?and?運算符相連,?相連之后置于中括號內進行條件篩選。運行結果如下:
['first item']
????????這里的 and其實是 XPath中的運算符。除了它,還有很多其他運算符,如or、mod等,在此總結為下表。
運?算?符 | 描述 | 實例 | 返 回 值 | ||
or | 或 | age=19 or age=20 | 如果 age 是 19, 則返回true。 | ||
and | 與 | age>19 and age<21 | 如果 age 是 20, 則返回true。如果age 是18, 則返回false | ||
mod | 計算除法的余數 | 5 mod 2 | 1 | ||
| | 計算兩個節點集 | //book|//cd | 返回所有擁有 book 和cd元素的節點集 | ||
+ | 加法 | 6 + 4 | 10 | ||
- | 減法 | 6 - 4 | 2 | ||
* | 乘法 | 6 * 4 | 24 | ||
div | 除法 | 8 div 4 | 2 | ||
= | 等于 | age=19 | 如果 age 是 19, 則返回true。 | ||
!= | 不等于 | age!=19 | 如果 age 是 18, 則返回true。如果age 是 19, 則返回false | ||
< | 小于 | age<19 | 如果 age 是 18, 則返回 true。如果age 是 19, 則返回 false | ||
<= | 小于或等于 | <=19 | 如果 age 是 19, 則返回 true。如果age 是 20, 則返回false | ||
> | 大于 | age>19 | 如果 age 是 20, 則返回true。如果age 是 19, 則返回 false | ||
>= | 大于或等于 | age>=19 | 如果age 是 19, 則返回true。如果age 是18, 則返回false |
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?方法即可實現。第三次選擇時,?選取了位置小于3?的?li?節點,?也就是位置序號為1?和2?的節點,?得到的結果就是前兩個?li節點。第四次選擇時,?選取了倒數第三個?li?節點,?在中括號中調用last?方法再減去2即可實現。因為last?方法代表最后一個,?在此基礎上減2?得到的就是倒數第三個。
運行結果如下:
['first item']
['fifth item']
['first item',?'second item']
['third item']
????????在這個實例中,?我們使用了?last、position等方法。XPath?提供了?100多個方法,?包括存取、數值、字符串、邏輯、節點、序列等處理功能。
? ? ? ? XPath還有一個節點軸的選擇方法,但由于很少使用,故在此不在介紹!!!
注:今天,又是深愛Python的一天!!!