第09讲:爬虫解析利器 PyQuery 的使用
上一課時我們學習了正則表達式的基本用法,然而一旦你的正則表達式寫法有問題,我們就無法獲取需要的信息。
你可能會思考:每個網頁,都有一定的特殊結構和層級關系,而且很多節點都有 id 或 class 作為區分,我們可以借助它們的結構和屬性來提取信息嗎?
這的確可行。這個課時我會為你介紹一個更加強大的 HTML 解析庫:pyquery。利用它,我們可以直接解析 DOM 節點的結構,并通過 DOM 節點的一些屬性快速進行內容提取。
接下來,我們就來感受一下 pyquery 的強大之處。
準備工作
pyquery 是 Python 的第三方庫,我們可以借助于 pip3 來安裝,安裝命令如下:
pip3 install pyquery更詳細的安裝方法可以參考:https://cuiqingcai.com/5186.html。
初始化
我們在解析 HTML 文本的時候,首先需要將其初始化為一個 pyquery 對象。它的初始化方式有多種,比如直接傳入字符串、傳入 URL、傳入文件名,等等。
下面我們來詳細介紹一下。
字符串初始化
我們可以直接把 HTML 的內容當作參數來初始化 pyquery 對象。我們用一個實例來感受一下:
html = ''' <div><ul><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div> ''' from pyquery import PyQuery as pq doc = pq(html) print(doc('li'))運行結果如下:
<li class="item-0">first item</li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-1 active"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a></li>這里首先引入 pyquery 這個對象,取別名為 pq,然后聲明了一個長 HTML 字符串,并將其當作參數傳遞給 pyquery 類,這樣就成功完成了初始化。
接下來,將初始化的對象傳入 CSS 選擇器。在這個實例中,我們傳入 li 節點,這樣就可以選擇所有的 li 節點。
URL 初始化
初始化的參數不僅可以以字符串的形式傳遞,還可以傳入網頁的 URL,此時只需要指定參數為 url 即可:
from pyquery import PyQuery as pq doc = pq(url='https://cuiqingcai.com') print(doc('title'))運行結果:
<title>靜覓丨崔慶才的個人博客</title>這樣的話,pyquery 對象會首先請求這個 URL,然后用得到的 HTML 內容完成初始化。這就相當于將網頁的源代碼以字符串的形式傳遞給 pyquery 類來初始化。
它與下面的功能是相同的:
from pyquery import PyQuery as pq import requests doc = pq(requests.get('https://cuiqingcai.com').text) print(doc('title'))文件初始化
當然除了傳遞一個 URL,我們還可以傳遞本地的文件名,參數指定為 filename 即可:
from pyquery import PyQuery as pq doc = pq(filename='demo.html') print(doc('li'))當然,這里需要有一個本地 HTML 文件 demo.html,其內容是待解析的 HTML 字符串。這樣它會先讀取本地的文件內容,然后將文件內容以字符串的形式傳遞給 pyquery 類來初始化。
以上 3 種方式均可初始化,當然最常用的初始化方式還是以字符串形式傳遞。
基本 CSS 選擇器
我們先用一個實例來感受一下 pyquery 的 CSS 選擇器的用法:
html = ''' <div id="container"><ul class="list"><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div> ''' from pyquery import PyQuery as pq doc = pq(html) print(doc('#container .list li')) print(type(doc('#container .list li')))運行結果:
<li class="item-0">first item</li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-1 active"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a></li> <class 'pyquery.pyquery.PyQuery'>在上面的例子中,我們初始化 pyquery 對象之后,傳入 CSS 選擇器 #container .list li,它的意思是先選取 id 為 container 的節點,然后再選取其內部 class 為 list 的所有 li 節點,最后打印輸出。
可以看到,我們成功獲取到了符合條件的節點。我們將它的類型打印輸出后發現,它的類型依然是 pyquery 類型。
下面,我們直接遍歷這些節點,然后調用 text 方法,就可以獲取節點的文本內容,代碼示例如下:
for item in doc('#container .list li').items():print(item.text())運行結果如下:
first item second item third item fourth item fifth item怎么樣?我們沒有再寫正則表達式,而是直接通過選擇器和 text 方法,就得到了我們想要提取的文本信息,是不是方便多了?
下面我們再來詳細了解一下 pyquery 的用法吧,我將為你講解如何用它查找節點、遍歷節點、獲取各種信息等操作方法。掌握了這些,我們就能更高效地完成數據提取。
查找節點
下面我們介紹一些常用的查詢方法。
子節點
查找子節點需要用到 find 方法,傳入的參數是 CSS 選擇器,我們還是以上面的 HTML 為例:
from pyquery import PyQuery as pq doc = pq(html) items = doc('.list') print(type(items)) print(items) lis = items.find('li') print(type(lis)) print(lis)運行結果:
<class 'pyquery.pyquery.PyQuery'> <ul class="list"><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li> </ul> <class 'pyquery.pyquery.PyQuery'> <li class="item-0">first item</li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-1 active"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a></li>首先,我們通過 .list 參數選取 class 為 list 的節點,然后調用 find 方法,傳入 CSS 選擇器,選取其內部的 li 節點,最后打印輸出。可以發現,find 方法會將符合條件的所有節點選擇出來,結果的類型是 pyquery 類型。
find 的查找范圍是節點的所有子孫節點,而如果我們只想查找子節點,那可以用 children 方法:
lis = items.children() print(type(lis)) print(lis)運行結果如下:
<class 'pyquery.pyquery.PyQuery'> <li class="item-0">first item</li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-1 active"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a></li>如果要篩選所有子節點中符合條件的節點,比如想篩選出子節點中 class 為 active 的節點,可以向 children 方法傳入 CSS 選擇器 .active,代碼如下:
lis = items.children('.active') print(lis)運行結果:
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-1 active"><a href="link4.html">fourth item</a></li>我們看到輸出的結果已經做了篩選,留下了 class 為 active 的節點。
父節點
我們可以用 parent 方法來獲取某個節點的父節點,下面用一個實例來感受一下:
html = ''' <div class="wrap"><div id="container"><ul class="list"><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div></div> ''' from pyquery import PyQuery as pq doc = pq(html) items = doc('.list') container = items.parent() print(type(container)) print(container)運行結果如下:
<class 'pyquery.pyquery.PyQuery'> <div id="container"><ul class="list"><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div>在上面的例子中我們首先用 .list 選取 class 為 list 的節點,然后調用 parent 方法得到其父節點,其類型依然是 pyquery 類型。
這里的父節點是該節點的直接父節點,也就是說,它不會再去查找父節點的父節點,即祖先節點。
但是如果你想獲取某個祖先節點,該怎么辦呢?我們可以用 parents 方法:
from pyquery import PyQuery as pq doc = pq(html) items = doc('.list') parents = items.parents() print(type(parents)) print(parents)運行結果如下:
<class 'pyquery.pyquery.PyQuery'> <div class="wrap"><div id="container"><ul class="list"><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div></div><div id="container"><ul class="list"><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div>可以看到,這個例子的輸出結果有兩個:一個是 class 為 wrap 的節點,一個是 id 為 container 的節點。也就是說,使用 parents 方法會返回所有的祖先節點。
如果你想要篩選某個祖先節點的話,可以向 parents 方法傳入 CSS 選擇器,這樣就會返回祖先節點中符合 CSS 選擇器的節點:
parent = items.parents('.wrap') print(parent)運行結果如下:
<div class="wrap"><div id="container"><ul class="list"><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div></div>可以看到,輸出結果少了一個節點,只保留了 class 為 wrap 的節點。
兄弟節點
前面我們說明了子節點和父節點的用法,還有一種節點叫作兄弟節點。如果要獲取兄弟節點,可以使用 siblings 方法。這里還是以上面的 HTML 代碼為例:
from pyquery import PyQuery as pq doc = pq(html) li = doc('.list .item-0.active') print(li.siblings())在這個例子中我們首先選擇 class 為 list 的節點,內部 class 為 item-0 和 active 的節點,也就是第 3 個 li 節點。很明顯,它的兄弟節點有 4 個,那就是第 1、2、4、5 個 li 節點。
我們來運行一下:
<li class="item-1"><a href="link2.html">second item</a></li> <li class="item-0">first item</li> <li class="item-1 active"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a></li>可以看到,結果顯示的正是我們剛才所說的 4 個兄弟節點。
如果要篩選某個兄弟節點,我們依然可以用 siblings 方法傳入 CSS 選擇器,這樣就會從所有兄弟節點中挑選出符合條件的節點了:
from pyquery import PyQuery as pq doc = pq(html) li = doc('.list .item-0.active') print(li.siblings('.active'))在這個例子中我們篩選 class 為 active 的節點,從剛才的結果中可以觀察到,class 為 active 兄弟節點的是第 4 個 li 節點,所以結果應該是1個。
我們再看一下運行結果:
<li class="item-1 active"><a href="link4.html">fourth item</a></li>遍歷
通過剛才的例子我們可以觀察到,pyquery 的選擇結果既可能是多個節點,也可能是單個節點,類型都是 pyquery 類型,并沒有返回列表。
對于單個節點來說,可以直接打印輸出,也可以直接轉成字符串:
from pyquery import PyQuery as pq doc = pq(html) li = doc('.item-0.active') print(li) print(str(li))運行結果如下:
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>對于有多個節點的結果,我們就需要用遍歷來獲取了。例如,如果要把每一個 li 節點進行遍歷,需要調用 items 方法:
from pyquery import PyQuery as pq doc = pq(html) lis = doc('li').items() print(type(lis)) for li in lis:print(li, type(li))運行結果如下:
<class 'generator'> <li class="item-0">first item</li> <class 'pyquery.pyquery.PyQuery'> <li class="item-1"><a href="link2.html">second item</a></li> <class 'pyquery.pyquery.PyQuery'> <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <class 'pyquery.pyquery.PyQuery'> <li class="item-1 active"><a href="link4.html">fourth item</a></li> <class 'pyquery.pyquery.PyQuery'> <li class="item-0"><a href="link5.html">fifth item</a></li> <class 'pyquery.pyquery.PyQuery'>可以發現,調用 items 方法后,會得到一個生成器,遍歷一下,就可以逐個得到 li 節點對象了,它的類型也是 pyquery 類型。每個 li 節點還可以調用前面所說的方法進行選擇,比如繼續查詢子節點,尋找某個祖先節點等,非常靈活。
獲取信息
提取到節點之后,我們的最終目的當然是提取節點所包含的信息了。比較重要的信息有兩類,一是獲取屬性,二是獲取文本,下面分別進行說明。
獲取屬性
提取到某個 pyquery 類型的節點后,就可以調用 attr 方法來獲取屬性:
html = ''' <div class="wrap"><div id="container"><ul class="list"><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div></div> ''' from pyquery import PyQuery as pq doc = pq(html) a = doc('.item-0.active a') print(a, type(a)) print(a.attr('href'))運行結果如下:
<a href="link3.html"><span class="bold">third item</span></a> <class 'pyquery.pyquery.PyQuery'> link3.html在這個例子中我們首先選中 class 為 item-0 和 active 的 li 節點內的 a 節點,它的類型是 pyquery 類型。
然后調用 attr 方法。在這個方法中傳入屬性的名稱,就可以得到屬性值了。
此外,也可以通過調用 attr 屬性來獲取屬性值,用法如下:
print(a.attr.href)結果:
link3.html這兩種方法的結果完全一樣。如果選中的是多個元素,然后調用 attr 方法,會出現怎樣的結果呢?我們用實例來測試一下:
a = doc('a') print(a, type(a)) print(a.attr('href')) print(a.attr.href)運行結果如下:
<a href="link2.html">second item</a><a href="link3.html"><span class="bold">third item</span></a><a href="link4.html">fourth item</a><a href="link5.html">fifth item</a> <class 'pyquery.pyquery.PyQuery'> link2.html link2.html照理來說,我們選中的 a 節點應該有 4 個,打印結果也應該是 4 個,但是當我們調用 attr 方法時,返回結果卻只有第 1 個。這是因為,當返回結果包含多個節點時,調用 attr 方法,只會得到第 1 個節點的屬性。
那么,遇到這種情況時,如果想獲取所有的 a 節點的屬性,就要用到前面所說的遍歷了:
from pyquery import PyQuery as pq doc = pq(html) a = doc('a') for item in a.items():print(item.attr('href'))運行結果:
link2.html link3.html link4.html link5.html因此,在進行屬性獲取時,先要觀察返回節點是一個還是多個,如果是多個,則需要遍歷才能依次獲取每個節點的屬性。
獲取文本
獲取節點之后的另一個主要操作就是獲取其內部文本了,此時可以調用 text 方法來實現:
html = ''' <div class="wrap"><div id="container"><ul class="list"><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div></div> ''' from pyquery import PyQuery as pq doc = pq(html) a = doc('.item-0.active a') print(a) print(a.text())運行結果:
<a href="link3.html"><span class="bold">third item</span></a> third item這里我們首先選中一個 a 節點,然后調用 text 方法,就可以獲取其內部的文本信息了。text 會忽略節點內部包含的所有 HTML,只返回純文字內容。
但如果你想要獲取這個節點內部的 HTML 文本,就要用 html 方法了:
from pyquery import PyQuery as pq doc = pq(html) li = doc('.item-0.active') print(li) print(li.html())這里我們選中第 3 個 li 節點,然后調用 html 方法,它返回的結果應該是 li 節點內的所有 HTML 文本。
運行結果:
<a href="link3.html"><span class="bold">third item</span></a>這里同樣有一個問題,如果我們選中的結果是多個節點,text 或 html 方法會返回什么內容?我們用實例來看一下:
html = ''' <div class="wrap"><div id="container"><ul class="list"><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div></div> ''' from pyquery import PyQuery as pq doc = pq(html) li = doc('li') print(li.html()) print(li.text()) print(type(li.text())運行結果如下:
<a href="link2.html">second item</a> second item third item fourth item fifth item <class'str'>結果比較出乎意料,html 方法返回的是第 1 個 li 節點的內部 HTML 文本,而 text 則返回了所有的 li 節點內部的純文本,中間用一個空格分割開,即返回結果是一個字符串。
這個地方值得注意,如果你想要得到的結果是多個節點,并且需要獲取每個節點的內部 HTML 文本,則需要遍歷每個節點。而 text 方法不需要遍歷就可以獲取,它將所有節點取文本之后合并成一個字符串。
節點操作
pyquery 提供了一系列方法來對節點進行動態修改,比如為某個節點添加一個 class,移除某個節點等,這些操作有時會為提取信息帶來極大的便利。
由于節點操作的方法太多,下面舉幾個典型的例子來說明它的用法。
addClass 和 removeClass
我們先用一個實例來感受一下:
首先選中第 3 個 li 節點,然后調用 removeClass 方法,將 li 節點的 active 這個 class 移除,第 2 步調用 addClass 方法,將 class 添加回來。每執行一次操作,就打印輸出當前 li 節點的內容。
運行結果如下:
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-0"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>可以看到,一共輸出了 3 次。第 2 次輸出時,li 節點的 active 這個 class 被移除了,第 3 次 class 又添加回來了。
所以說,addClass 和 removeClass 方法可以動態改變節點的 class 屬性。
attr、text、html
當然,除了操作 class 這個屬性外,也可以用 attr 方法對屬性進行操作。此外,我們還可以用 text 和 html 方法來改變節點內部的內容。示例如下:
這里我們首先選中 li 節點,然后調用 attr 方法來修改屬性。該方法的第 1 個參數為屬性名,第 2 個參數為屬性值。最后調用 text 和 html 方法來改變節點內部的內容。3 次操作后,分別打印輸出當前的 li 節點。
運行結果如下:
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-0 active" name="link"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-0 active" name="link">changed item</li> <li class="item-0 active" name="link"><span>changed item</span></li>我們發現,調用 attr 方法后,li 節點多了一個原本不存在的屬性 name,其值為 link。接著調用 text 方法傳入文本,li 節點內部的文本全被改為傳入的字符串文本。最后,調用 html 方法傳入 HTML 文本,li 節點內部又變為傳入的 HTML 文本了。
所以說,使用 attr 方法時如果只傳入第 1 個參數的屬性名,則是獲取這個屬性值;如果傳入第 2 個參數,可以用來修改屬性值。使用 text 和 html 方法時如果不傳參數,則是獲取節點內純文本和 HTML 文本,如果傳入參數,則進行賦值。
remove
顧名思義,remove 方法就是移除,它有時會為信息的提取帶來非常大的便利。下面有一段 HTML 文本:
html = ''' <div class="wrap">Hello, World<p>This is a paragraph.</p></div> ''' from pyquery import PyQuery as pq doc = pq(html) wrap = doc('.wrap') print(wrap.text())現在我們想提取“Hello, World”這個字符串,該怎樣操作呢?
這里先直接嘗試提取 class 為 wrap 的節點的內容,看看是不是我們想要的。
運行結果如下:
Hello, World This is a paragraph.這個結果還包含了內部的 p 節點的內容,也就是說 text 把所有的純文本全提取出來了。
如果我們想去掉 p 節點內部的文本,可以選擇再把 p 節點內的文本提取一遍,然后從整個結果中移除這個子串,但這個做法明顯比較煩瑣。
這時 remove 方法就可以派上用場了,我們可以接著這么做:
wrap.find('p').remove() print(wrap.text())首先選中 p 節點,然后調用 remove 方法將其移除,這時 wrap 內部就只剩下“Hello, World”這句話了,最后利用 text 方法提取即可。
其實還有很多其他節點操作的方法,比如 append、empty 和 prepend 等方法,詳細的用法可以參考官方文檔:http://pyquery.readthedocs.io/en/latest/api.html。
偽類選擇器
CSS 選擇器之所以強大,還有一個很重要的原因,那就是它支持多種多樣的偽類選擇器,例如選擇第一個節點、最后一個節點、奇偶數節點、包含某一文本的節點等。示例如下:
html = ''' <div class="wrap"><div id="container"><ul class="list"><li class="item-0">first item</li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1 active"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div></div> ''' from pyquery import PyQuery as pq doc = pq(html) li = doc('li:first-child') print(li) li = doc('li:last-child') print(li) li = doc('li:nth-child(2)') print(li) li = doc('li:gt(2)') print(li) li = doc('li:nth-child(2n)') print(li) li = doc('li:contains(second)') print(li)在這個例子中我們使用了 CSS3 的偽類選擇器,依次選擇了第 1 個 li 節點、最后一個 li 節點、第 2 個 li 節點、第 3 個 li 之后的 li 節點、偶數位置的 li 節點、包含 second 文本的 li 節點。
關于 CSS 選擇器的更多用法,可以參考 http://www.w3school.com.cn/css/index.asp。
到此為止,pyquery 的常用用法就介紹完了。如果想查看更多的內容,可以參考 pyquery 的官方文檔:http://pyquery.readthedocs.io。相信一旦你擁有了它,解析網頁將不再是難事。
總結
以上是生活随笔為你收集整理的第09讲:爬虫解析利器 PyQuery 的使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第11讲:Reqeusts + PyQu
- 下一篇: 第49讲:实战上手,Scrapy-Red