[Python3 網(wǎng)絡(luò)爬蟲開發(fā)實戰(zhàn)] 4.3 - 使用 pyquery
在上一節(jié)中,我們介紹了 Beautiful Soup 的用法,它是一個非常強(qiáng)大的網(wǎng)頁解析庫,你是否覺得它的一些方法用起來有點不適應(yīng)?有沒有覺得它的 CSS 選擇器的功能沒有那么強(qiáng)大?

如果你對 Web 有所涉及,如果你比較喜歡用 CSS 選擇器,如果你對 jQuery 有所了解,那么這里有一個更適合你的解析庫 ——pyquery。
接下來,我們就來感受一下 pyquery 的強(qiáng)大之處。
1. 準(zhǔn)備工作
在開始之前,請確保已經(jīng)正確安裝好了 pyquery。若沒有安裝,可以參考第 1 章的安裝過程。
2. 初始化
像 Beautiful Soup 一樣,初始化 pyquery 的時候,也需要傳入 HTML 文本來初始化一個 PyQuery 對象。它的初始化方式有多種,比如直接傳入字符串,傳入 URL,傳入文件名,等等。下面我們來詳細(xì)介紹一下。
字符串初始化
首先,我們用一個實例來感受一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
html = '''
'''
from pyquery import PyQuery as pq
doc = pq(html)
print(doc('li'))
運(yùn)行結(jié)果如下:
1
2
3
4
5
這里首先引入 PyQuery 這個對象,取別名為 pq。然后聲明了一個長 HTML 字符串,并將其當(dāng)作參數(shù)傳遞給 PyQuery 類,這樣就成功完成了初始化。接下來,將初始化的對象傳入 CSS 選擇器。在這個實例中,我們傳入 li 節(jié)點,這樣就可以選擇所有的 li 節(jié)點。
URL 初始化
初始化的參數(shù)不僅可以以字符串的形式傳遞,還可以傳入網(wǎng)頁的 URL,此時只需要指定參數(shù)為 url 即可:
1
2
3
from pyquery import PyQuery as pq
doc = pq(url='http://cuiqingcai.com')
print(doc('title'))
運(yùn)行結(jié)果如下:
1
這樣的話,PyQuery 對象會首先請求這個 URL,然后用得到的 HTML 內(nèi)容完成初始化,這其實就相當(dāng)于用網(wǎng)頁的源代碼以字符串的形式傳遞給 PyQuery 類來初始化。
它與下面的功能是相同的:
1
2
3
4
from pyquery import PyQuery as pq
import requests
doc = pq(requests.get('http://cuiqingcai.com').text)
print(doc('title'))
文件初始化
當(dāng)然,除了傳遞 URL,還可以傳遞本地的文件名,此時將參數(shù)指定為 filename 即可:
1
2
3
from pyquery import PyQuery as pq
doc = pq(filename='demo.html')
print(doc('li'))
當(dāng)然,這里需要有一個本地 HTML 文件 demo.html,其內(nèi)容是待解析的 HTML 字符串。這樣它會首先讀取本地的文件內(nèi)容,然后用文件內(nèi)容以字符串的形式傳遞給 PyQuery 類來初始化。
以上 3 種初始化方式均可,當(dāng)然最常用的初始化方式還是以字符串形式傳遞。
3. 基本 CSS 選擇器
首先,用一個實例來感受 pyquery 的 CSS 選擇器的用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
html = '''
'''
from pyquery import PyQuery as pq
doc = pq(html)
print(doc('#container .list li'))
print(type(doc('#container .list li')))
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
這里我們初始化 PyQuery 對象之后,傳入了一個 CSS 選擇器#container .list li,它的意思是先選取 id 為 container 的節(jié)點,然后再選取其內(nèi)部的 class 為 list 的節(jié)點內(nèi)部的所有 li 節(jié)點。然后,打印輸出。可以看到,我們成功獲取到了符合條件的節(jié)點。
最后,將它的類型打印輸出。可以看到,它的類型依然是 PyQuery 類型。
4. 查找節(jié)點
下面我們介紹一些常用的查詢函數(shù),這些函數(shù)和 jQuery 中函數(shù)的用法完全相同。
子節(jié)點
查找子節(jié)點時,需要用到 find() 方法,此時傳入的參數(shù)是 CSS 選擇器。這里還是以前面的 HTML 為例:
1
2
3
4
5
6
7
8
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)
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
首先,我們選取 class 為 list 的節(jié)點,然后調(diào)用了 find() 方法,傳入 CSS 選擇器,選取其內(nèi)部的 li 節(jié)點,最后打印輸出。可以發(fā)現(xiàn),find() 方法會將符合條件的所有節(jié)點選擇出來,結(jié)果的類型是 PyQuery 類型。
其實 find() 的查找范圍是節(jié)點的所有子孫節(jié)點,而如果我們只想查找子節(jié)點,那么可以用 children() 方法:
1
2
3
lis = items.children()
print(type(lis))
print(lis)
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
如果要篩選所有子節(jié)點中符合條件的節(jié)點,比如想篩選出子節(jié)點中 class 為 active 的節(jié)點,可以向 children() 方法傳入 CSS 選擇器.active:
1
2
lis = items.children('.active')
print(lis)
運(yùn)行結(jié)果如下:
1
2
可以看到,輸出結(jié)果已經(jīng)做了篩選,留下了 class 為 active 的節(jié)點。
父節(jié)點
我們可以用 parent() 方法來獲取某個節(jié)點的父節(jié)點,示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
html = '''
'''
from pyquery import PyQuery as pq
doc = pq(html)
items = doc('.list')
container = items.parent()
print(type(container))
print(container)
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
7
8
9
10
這里我們首先用.list 選取 class 為 list 的節(jié)點,然后調(diào)用 parent() 方法得到其父節(jié)點,其類型依然是 PyQuery 類型。
這里的父節(jié)點是該節(jié)點的直接父節(jié)點,也就是說,它不會再去查找父節(jié)點的父節(jié)點,即祖先節(jié)點。
但是如果想獲取某個祖先節(jié)點,該怎么辦呢?這時可以用 parents() 方法:
1
2
3
4
5
6
from pyquery import PyQuery as pq
doc = pq(html)
items = doc('.list')
parents = items.parents()
print(type(parents))
print(parents)
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
可以看到,輸出結(jié)果有兩個:一個是 class 為 wrap 的節(jié)點,一個是 id 為 container 的節(jié)點。也就是說,parents() 方法會返回所有的祖先節(jié)點。
如果想要篩選某個祖先節(jié)點的話,可以向 parents() 方法傳入 CSS 選擇器,這樣就會返回祖先節(jié)點中符合 CSS 選擇器的節(jié)點:
1
2
parent = items.parents('.wrap')
print(parent)
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
7
8
9
10
11
可以看到,輸出結(jié)果少了一個節(jié)點,只保留了 class 為 wrap 的節(jié)點。
兄弟節(jié)點
前面我們說明了子節(jié)點和父節(jié)點的用法,還有一種節(jié)點,那就是兄弟節(jié)點。如果要獲取兄弟節(jié)點,可以使用 siblings() 方法。這里還是以上面的 HTML 代碼為例:
1
2
3
4
from pyquery import PyQuery as pq
doc = pq(html)
li = doc('.list .item-0.active')
print(li.siblings())
這里首先選擇 class 為 list 的節(jié)點內(nèi)部 class 為 item-0 和 active 的節(jié)點,也就是第三個 li 節(jié)點。那么,很明顯,它的兄弟節(jié)點有 4 個,那就是第一、二、四、五個 li 節(jié)點。
運(yùn)行結(jié)果如下:
1
2
3
4
可以看到,這正是我們剛才所說的 4 個兄弟節(jié)點。
如果要篩選某個兄弟節(jié)點,我們依然可以向 siblings 方法傳入 CSS 選擇器,這樣就會從所有兄弟節(jié)點中挑選出符合條件的節(jié)點了:
1
2
3
4
from pyquery import PyQuery as pq
doc = pq(html)
li = doc('.list .item-0.active')
print(li.siblings('.active'))
這里我們篩選了 class 為 active 的節(jié)點,通過剛才的結(jié)果可以觀察到,class 為 active 的兄弟節(jié)點只有第四個 li 節(jié)點,所以結(jié)果應(yīng)該是一個。
我們再看一下運(yùn)行結(jié)果:
1
5. 遍歷
剛才可以觀察到,pyquery 的選擇結(jié)果可能是多個節(jié)點,也可能是單個節(jié)點,類型都是 PyQuery 類型,并沒有返回像 Beautiful Soup 那樣的列表。
對于單個節(jié)點來說,可以直接打印輸出,也可以直接轉(zhuǎn)成字符串:
1
2
3
4
5
from pyquery import PyQuery as pq
doc = pq(html)
li = doc('.item-0.active')
print(li)
print(str(li))
運(yùn)行結(jié)果如下:
1
2
對于多個節(jié)點的結(jié)果,我們就需要遍歷來獲取了。例如,這里把每一個 li 節(jié)點進(jìn)行遍歷,需要調(diào)用 items() 方法:
1
2
3
4
5
6
from pyquery import PyQuery as pq
doc = pq(html)
lis = doc('li').items()
print(type(lis))
for li in lis:
print(li, type(li))
運(yùn)行結(jié)果如下:
1
2
3
4
5
6
7
8
9
10
11
可以發(fā)現(xiàn),調(diào)用 items() 方法后,會得到一個生成器,遍歷一下,就可以逐個得到 li 節(jié)點對象了,它的類型也是 PyQuery 類型。每個 li 節(jié)點還可以調(diào)用前面所說的方法進(jìn)行選擇,比如繼續(xù)查詢子節(jié)點,尋找某個祖先節(jié)點等,非常靈活。
6. 獲取信息
提取到節(jié)點之后,我們的最終目的當(dāng)然是提取節(jié)點所包含的信息了。比較重要的信息有兩類,一是獲取屬性,二是獲取文本,下面分別進(jìn)行說明。
獲取屬性
提取到某個 PyQuery 類型的節(jié)點后,就可以調(diào)用 attr() 方法來獲取屬性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
html = '''
'''
from pyquery import PyQuery as pq
doc = pq(html)
a = doc('.item-0.active a')
print(a, type(a))
print(a.attr('href'))
運(yùn)行結(jié)果如下:
1
2
link3.html
這里首先選中 class 為 item-0 和 active 的 li 節(jié)點內(nèi)的 a 節(jié)點,它的類型是 PyQuery 類型。
然后調(diào)用 attr() 方法。在這個方法中傳入屬性的名稱,就可以得到這個屬性值了。
此外,也可以通過調(diào)用 attr 屬性來獲取屬性,用法如下:
1
print(a.attr.href)
結(jié)果如下:
1
link3.html
這兩種方法的結(jié)果完全一樣。
如果選中的是多個元素,然后調(diào)用 attr() 方法,會出現(xiàn)怎樣的結(jié)果呢?我們用實例來測試一下:
1
2
3
4
a = doc('a')
print(a, type(a))
print(a.attr('href'))
print(a.attr.href)
運(yùn)行結(jié)果如下:
1
2
3
second itemthird itemfourth itemfifth item
link2.html
link2.html
照理來說,我們選中的 a 節(jié)點應(yīng)該有 4 個,而且打印結(jié)果也應(yīng)該是 4 個,但是當(dāng)我們調(diào)用 attr() 方法時,返回結(jié)果卻只是第一個。這是因為,當(dāng)返回結(jié)果包含多個節(jié)點時,調(diào)用 attr() 方法,只會得到第一個節(jié)點的屬性。
那么,遇到這種情況時,如果想獲取所有的 a 節(jié)點的屬性,就要用到前面所說的遍歷了:
1
2
3
4
5
from pyquery import PyQuery as pq
doc = pq(html)
a = doc('a')
for item in a.items():
print(item.attr('href'))
此時的運(yùn)行結(jié)果如下:
1
2
3
4
link2.html
link3.html
link4.html
link5.html
因此,在進(jìn)行屬性獲取時,可以觀察返回節(jié)點是一個還是多個,如果是多個,則需要遍歷才能依次獲取每個節(jié)點的屬性。
獲取文本
獲取節(jié)點之后的另一個主要操作就是獲取其內(nèi)部的文本了,此時可以調(diào)用 text() 方法來實現(xiàn):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
html = '''
'''
from pyquery import PyQuery as pq
doc = pq(html)
a = doc('.item-0.active a')
print(a)
print(a.text())
運(yùn)行結(jié)果如下:
1
2
third item
這里首先選中一個 a 節(jié)點,然后調(diào)用 text() 方法,就可以獲取其內(nèi)部的文本信息。此時它會忽略掉節(jié)點內(nèi)部包含的所有 HTML,只返回純文字內(nèi)容。
但如果想要獲取這個節(jié)點內(nèi)部的 HTML 文本,就要用 html() 方法了:
1
2
3
4
5
from pyquery import PyQuery as pq
doc = pq(html)
li = doc('.item-0.active')
print(li)
print(li.html())
這里我們選中了第三個 li 節(jié)點,然后調(diào)用了 html() 方法,它返回的結(jié)果應(yīng)該是 li 節(jié)點內(nèi)的所有 HTML 文本。
運(yùn)行結(jié)果如下:
1
這里同樣有一個問題,如果我們選中的結(jié)果是多個節(jié)點,text() 或 html() 會返回什么內(nèi)容?我們用實例來看一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
html = '''
'''
from pyquery import PyQuery as pq
doc = pq(html)
li = doc('li')
print(li.html())
print(li.text())
print(type(li.text())
運(yùn)行結(jié)果如下:
1
2
3
second item third item fourth item fifth item
結(jié)果可能比較出乎意料,html() 方法返回的是第一個 li 節(jié)點的內(nèi)部 HTML 文本,而 text() 則返回了所有的 li 節(jié)點內(nèi)部的純文本,中間用一個空格分割開,即返回結(jié)果是一個字符串。
所以這個地方值得注意,如果得到的結(jié)果是多個節(jié)點,并且想要獲取每個節(jié)點的內(nèi)部 HTML 文本,則需要遍歷每個節(jié)點。而 text() 方法不需要遍歷就可以獲取,它將所有節(jié)點取文本之后合并成一個字符串。
7. 節(jié)點操作
pyquery 提供了一系列方法來對節(jié)點進(jìn)行動態(tài)修改,比如為某個節(jié)點添加一個 class,移除某個節(jié)點等,這些操作有時候會為提取信息帶來極大的便利。
由于節(jié)點操作的方法太多,下面舉幾個典型的例子來說明它的用法。
addClass 和 removeClass
我們先用實例來感受一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
html = '''
'''
from pyquery import PyQuery as pq
doc = pq(html)
li = doc('.item-0.active')
print(li)
li.removeClass('active')
print(li)
li.addClass('active')
print(li)
首先選中了第三個 li 節(jié)點,然后調(diào)用 removeClass() 方法,將 li 節(jié)點的 active 這個 class 移除,后來又調(diào)用 addClass() 方法,將 class 添加回來。每執(zhí)行一次操作,就打印輸出當(dāng)前 li 節(jié)點的內(nèi)容。
運(yùn)行結(jié)果如下:
1
2
3
可以看到,一共輸出了 3 次。第二次輸出時,li 節(jié)點的 active 這個 class 被移除了,第三次 class 又添加回來了。
所以說,addClass() 和 removeClass() 這些方法可以動態(tài)改變節(jié)點的 class 屬性。
attr、text 和 html
當(dāng)然,除了操作 class 這個屬性外,也可以用 attr() 方法對屬性進(jìn)行操作。此外,還可以用 text() 和 html() 方法來改變節(jié)點內(nèi)部的內(nèi)容。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
html = '''
'''
from pyquery import PyQuery as pq
doc = pq(html)
li = doc('.item-0.active')
print(li)
li.attr('name', 'link')
print(li)
li.text('changed item')
print(li)
li.html('changed item')
print(li)
這里我們首先選中 li 節(jié)點,然后調(diào)用 attr() 方法來修改屬性,其中該方法的第一個參數(shù)為屬性名,第二個參數(shù)為屬性值。接著,調(diào)用 text() 和 html() 方法來改變節(jié)點內(nèi)部的內(nèi)容。三次操作后,分別打印輸出當(dāng)前的 li 節(jié)點。
運(yùn)行結(jié)果如下:
1
2
3
4
可以發(fā)現(xiàn),調(diào)用 attr() 方法后,li 節(jié)點多了一個原本不存在的屬性 name,其值為 link。接著調(diào)用 text() 方法,傳入文本之后,li 節(jié)點內(nèi)部的文本全被改為傳入的字符串文本了。最后,調(diào)用 html() 方法傳入 HTML 文本后,li 節(jié)點內(nèi)部又變?yōu)閭魅氲?HTML 文本了。
所以說,如果 attr() 方法只傳入第一個參數(shù)的屬性名,則是獲取這個屬性值;如果傳入第二個參數(shù),可以用來修改屬性值。text() 和 html() 方法如果不傳參數(shù),則是獲取節(jié)點內(nèi)純文本和 HTML 文本;如果傳入?yún)?shù),則進(jìn)行賦值。
remove()
顧名思義,remove() 方法就是移除,它有時會為信息的提取帶來非常大的便利。下面有一段 HTML 文本:
1
2
3
4
5
6
7
8
9
10
html = '''
Hello, World
This is a paragraph.
'''
from pyquery import PyQuery as pq
doc = pq(html)
wrap = doc('.wrap')
print(wrap.text())
現(xiàn)在想提取 Hello, World 這個字符串,而不要 p 節(jié)點內(nèi)部的字符串,需要怎樣操作呢?
這里直接先嘗試提取 class 為 wrap 的節(jié)點的內(nèi)容,看看是不是我們想要的。運(yùn)行結(jié)果如下:
1
Hello, World This is a paragraph.
這個結(jié)果還包含了內(nèi)部的 p 節(jié)點的內(nèi)容,也就是說 text() 把所有的純文本全提取出來了。如果我們想去掉 p 節(jié)點內(nèi)部的文本,可以選擇再把 p 節(jié)點內(nèi)的文本提取一遍,然后從整個結(jié)果中移除這個子串,但這個做法明顯比較煩瑣。
這時 remove() 方法就可以派上用場了,我們可以接著這么做:
1
2
wrap.find('p').remove()
print(wrap.text())
首先選中 p 節(jié)點,然后調(diào)用了 remove() 方法將其移除,然后這時 wrap 內(nèi)部就只剩下 Hello, World 這句話了,然后再利用 text() 方法提取即可。
另外,其實還有很多節(jié)點操作的方法,比如 append()、empty() 和 prepend() 等方法,它們和 jQuery 的用法完全一致,詳細(xì)的用法可以參考官方文檔:http://pyquery.readthedocs.io/en/latest/api.html。
8. 偽類選擇器
CSS 選擇器之所以強(qiáng)大,還有一個很重要的原因,那就是它支持多種多樣的偽類選擇器,例如選擇第一個節(jié)點、最后一個節(jié)點、奇偶數(shù)節(jié)點、包含某一文本的節(jié)點等。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
html = '''
'''
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 的偽類選擇器,依次選擇了第一個 li 節(jié)點、最后一個 li 節(jié)點、第二個 li 節(jié)點、第三個 li 之后的 li 節(jié)點、偶數(shù)位置的 li 節(jié)點、包含 second 文本的 li 節(jié)點。
關(guān)于 CSS 選擇器的更多用法,可以參考 http://www.w3school.com.cn/css/index.asp。
到此為止,pyquery 的常用用法就介紹完了。如果想查看更多的內(nèi)容,可以參考 pyquery 的官方文檔:http://pyquery.readthedocs.io。我們相信有了它,解析網(wǎng)頁不再是難事。
Python 網(wǎng)絡(luò)
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。