【Python3網路爬蟲開發實戰】4-解析庫的使用-2 使用Beautiful Soup

崔慶才丨靜覓發表於2018-03-19

前面介紹了正規表示式的相關用法,但是一旦正規表示式寫的有問題,得到的可能就不是我們想要的結果了。而且對於一個網頁來說,都有一定的特殊結構和層級關係,而且很多節點都有idclass來作區分,所以藉助它們的結構和屬性來提取不也可以嗎?

這一節中,我們就來介紹一個強大的解析工具Beautiful Soup,它藉助網頁的結構和屬性等特性來解析網頁。有了它,我們不用再去寫一些複雜的正規表示式,只需要簡單的幾條語句,就可以完成網頁中某個元素的提取。

廢話不多說,接下來就來感受一下Beautiful Soup的強大之處吧。

1. 簡介

簡單來說,Beautiful Soup就是Python的一個HTML或XML的解析庫,可以用它來方便地從網頁中提取資料。官方解釋如下:

Beautiful Soup提供一些簡單的、Python式的函式來處理導航、搜尋、修改分析樹等功能。它是一個工具箱,通過解析文件為使用者提供需要抓取的資料,因為簡單,所以不需要多少程式碼就可以寫出一個完整的應用程式。
Beautiful Soup自動將輸入文件轉換為Unicode編碼,輸出文件轉換為UTF-8編碼。你不需要考慮編碼方式,除非文件沒有指定一個編碼方式,這時你僅僅需要說明一下原始編碼方式就可以了。
Beautiful Soup已成為和lxml、html6lib一樣出色的Python直譯器,為使用者靈活地提供不同的解析策略或強勁的速度。

所以說,利用它可以省去很多煩瑣的提取工作,提高了解析效率。

2. 準備工作

在開始之前,請確保已經正確安裝好了Beautiful Soup和lxml,如果沒有安裝,可以參考第1章的內容。

3. 解析器

Beautiful Soup在解析時實際上依賴解析器,它除了支援Python標準庫中的HTML解析器外,還支援一些第三方解析器(比如lxml)。表4-3列出了Beautiful Soup支援的解析器。

表4-3 Beautiful Soup支援的解析器

【Python3網路爬蟲開發實戰】4-解析庫的使用-2 使用Beautiful Soup

通過以上對比可以看出,lxml解析器有解析HTML和XML的功能,而且速度快,容錯能力強,所以推薦使用它。

如果使用lxml,那麼在初始化Beautiful Soup時,可以把第二個引數改為lxml即可:

from bs4 import BeautifulSoup
soup = BeautifulSoup('<p>Hello</p>', 'lxml')
print(soup.p.string)
複製程式碼

在後面,Beautiful Soup的用法例項也統一用這個解析器來演示。

4. 基本用法

下面首先用例項來看看Beautiful Soup的基本用法:

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.prettify())
print(soup.title.string)
複製程式碼

執行結果如下:

<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
 <body>
  <p class="title" name="dromouse">
   <b>
    The Dormouse's story
   </b>
  </p>
  <p class="story">
   Once upon a time there were three little sisters; and their names were
   <a class="sister" href="http://example.com/elsie" id="link1">
    <!-- Elsie -->
   </a>
   ,
   <a class="sister" href="http://example.com/lacie" id="link2">
    Lacie
   </a>
   and
   <a class="sister" href="http://example.com/tillie" id="link3">
    Tillie
   </a>
   ;
and they lived at the bottom of a well.
  </p>
  <p class="story">
   ...
  </p>
 </body>
</html>
The Dormouse's story
複製程式碼

這裡首先宣告變數html,它是一個HTML字串。但是需要注意的是,它並不是一個完整的HTML字串,因為bodyhtml節點都沒有閉合。接著,我們將它當作第一個引數傳給BeautifulSoup物件,該物件的第二個引數為解析器的型別(這裡使用lxml),此時就完成了BeaufulSoup物件的初始化。然後,將這個物件賦值給soup變數。

接下來,就可以呼叫soup的各個方法和屬性解析這串HTML程式碼了。

首先,呼叫prettify()方法。這個方法可以把要解析的字串以標準的縮排格式輸出。這裡需要注意的是,輸出結果裡面包含bodyhtml節點,也就是說對於不標準的HTML字串BeautifulSoup,可以自動更正格式。這一步不是由prettify()方法做的,而是在初始化BeautifulSoup時就完成了。

然後呼叫soup.title.string,這實際上是輸出HTML中title節點的文字內容。所以,soup.title可以選出HTML中的title節點,再呼叫string屬性就可以得到裡面的文字了,所以我們可以通過簡單呼叫幾個屬性完成文字提取,這是不是非常方便?

5. 節點選擇器

直接呼叫節點的名稱就可以選擇節點元素,再呼叫string屬性就可以得到節點內的文字了,這種選擇方式速度非常快。如果單個節點結構層次非常清晰,可以選用這種方式來解析。

選擇元素

下面再用一個例子詳細說明選擇元素的方法:

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.title)
print(type(soup.title))
print(soup.title.string)
print(soup.head)
print(soup.p)
複製程式碼

執行結果如下:

<title>The Dormouse's story</title>
<class 'bs4.element.Tag'>
The Dormouse's story
<head><title>The Dormouse's story</title></head>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
複製程式碼

這裡依然選用剛才的HTML程式碼,首先列印輸出title節點的選擇結果,輸出結果正是title節點加里面的文字內容。接下來,輸出它的型別,是bs4.element.Tag型別,這是Beautiful Soup中一個重要的資料結構。經過選擇器選擇後,選擇結果都是這種Tag型別。Tag具有一些屬性,比如string屬性,呼叫該屬性,可以得到節點的文字內容,所以接下來的輸出結果正是節點的文字內容。

接下來,我們又嘗試選擇了head節點,結果也是節點加其內部的所有內容。最後,選擇了p節點。不過這次情況比較特殊,我們發現結果是第一個p節點的內容,後面的幾個p節點並沒有選到。也就是說,當有多個節點時,這種選擇方式只會選擇到第一個匹配的節點,其他的後面節點都會忽略。

提取資訊

上面演示了呼叫string屬性來獲取文字的值,那麼如何獲取節點屬性的值呢?如何獲取節點名呢?下面我們來統一梳理一下資訊的提取方式。

(1)獲取名稱

可以利用name屬性獲取節點的名稱。這裡還是以上面的文字為例,選取title節點,然後呼叫name屬性就可以得到節點名稱:

print(soup.title.name)
複製程式碼

執行結果如下:

title
複製程式碼

(2)獲取屬性

每個節點可能有多個屬性,比如idclass等,選擇這個節點元素後,可以呼叫attrs獲取所有屬性:

print(soup.p.attrs)
print(soup.p.attrs['name'])
複製程式碼

執行結果如下:

{'class': ['title'], 'name': 'dromouse'}
dromouse
複製程式碼

可以看到,attrs的返回結果是字典形式,它把選擇的節點的所有屬性和屬性值組合成一個字典。接下來,如果要獲取name屬性,就相當於從字典中獲取某個鍵值,只需要用中括號加屬性名就可以了。比如,要獲取name屬性,就可以通過attrs['name']來得到。

其實這樣有點煩瑣,還有一種更簡單的獲取方式:可以不用寫attrs,直接在節點元素後面加中括號,傳入屬性名就可以獲取屬性值了。樣例如下:

print(soup.p['name'])
print(soup.p['class'])
複製程式碼

執行結果如下:

dromouse
['title']
複製程式碼

這裡需要注意的是,有的返回結果是字串,有的返回結果是字串組成的列表。比如,name屬性的值是唯一的,返回的結果就是單個字串。而對於class,一個節點元素可能有多個class,所以返回的是列表。在實際處理過程中,我們要注意判斷型別。

(3)獲取內容

可以利用string屬性獲取節點元素包含的文字內容,比如要獲取第一個p節點的文字:

print(soup.p.string)
複製程式碼

執行結果如下:

The Dormouse's story
複製程式碼

再次注意一下,這裡選擇到的p節點是第一個p節點,獲取的文字也是第一個p節點裡面的文字。

巢狀選擇

在上面的例子中,我們知道每一個返回結果都是bs4.element.Tag型別,它同樣可以繼續呼叫節點進行下一步的選擇。比如,我們獲取了head節點元素,我們可以繼續呼叫head來選取其內部的head節點元素:

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.head.title)
print(type(soup.head.title))
print(soup.head.title.string)
複製程式碼

執行結果如下:

<title>The Dormouse's story</title>
<class 'bs4.element.Tag'>
The Dormouse's story
複製程式碼

第一行結果是呼叫head之後再次呼叫title而選擇的title節點元素。然後列印輸出了它的型別,可以看到,它仍然是bs4.element.Tag型別。也就是說,我們在Tag型別的基礎上再次選擇得到的依然還是Tag型別,每次返回的結果都相同,所以這樣就可以做巢狀選擇了。

最後,輸出它的string屬性,也就是節點裡的文字內容。

關聯選擇

在做選擇的時候,有時候不能做到一步就選到想要的節點元素,需要先選中某一個節點元素,然後以它為基準再選擇它的子節點、父節點、兄弟節點等,這裡就來介紹如何選擇這些節點元素。

(1)子節點和子孫節點

選取節點元素之後,如果想要獲取它的直接子節點,可以呼叫contents屬性,示例如下:

html = """
<html>
    <head>
        <title>The Dormouse's story</title>
    </head>
    <body>
        <p class="story">
            Once upon a time there were three little sisters; and their names were
            <a href="http://example.com/elsie" class="sister" id="link1">
                <span>Elsie</span>
            </a>
            <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 
            and
            <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>
            and they lived at the bottom of a well.
        </p>
        <p class="story">...</p>
"""
複製程式碼

執行結果如下:

['\n            Once upon a time there were three little sisters; and their names were\n            ', <a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>, '\n', <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, ' \n            and\n            ', <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>, '\n            and they lived at the bottom of a well.\n        ']
複製程式碼

可以看到,返回結果是列表形式。p節點裡既包含文字,又包含節點,最後會將它們以列表形式統一返回。

需要注意的是,列表中的每個元素都是p節點的直接子節點。比如第一個a節點裡麵包含一層span節點,這相當於孫子節點了,但是返回結果並沒有單獨把span節點選出來。所以說,contents屬性得到的結果是直接子節點的列表。

同樣,我們可以呼叫children屬性得到相應的結果:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.p.children)
for i, child in enumerate(soup.p.children):
    print(i, child)
複製程式碼

執行結果如下:

<list_iterator object at 0x1064f7dd8>
0 
            Once upon a time there were three little sisters; and their names were

1 <a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
2 

3 <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
4  
            and

5 <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
6 
            and they lived at the bottom of a well.
複製程式碼

還是同樣的HTML文字,這裡呼叫了children屬性來選擇,返回結果是生成器型別。接下來,我們用for迴圈輸出相應的內容。

如果要得到所有的子孫節點的話,可以呼叫descendants屬性:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.p.descendants)
for i, child in enumerate(soup.p.descendants):
    print(i, child)
複製程式碼

執行結果如下:

<generator object descendants at 0x10650e678>
0 
            Once upon a time there were three little sisters; and their names were

1 <a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
2 

3 <span>Elsie</span>
4 Elsie
5 

6 

7 <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
8 Lacie
9  
            and

10 <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
11 Tillie
12 
            and they lived at the bottom of a well.
複製程式碼

此時返回結果還是生成器。遍歷輸出一下可以看到,這次的輸出結果就包含了span節點。descendants會遞迴查詢所有子節點,得到所有的子孫節點。

(2)父節點和祖先節點

如果要獲取某個節點元素的父節點,可以呼叫parent屬性:

html = """
<html>
    <head>
        <title>The Dormouse's story</title>
    </head>
    <body>
        <p class="story">
            Once upon a time there were three little sisters; and their names were
            <a href="http://example.com/elsie" class="sister" id="link1">
                <span>Elsie</span>
            </a>
        </p>
        <p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.a.parent)
複製程式碼

執行結果如下:

<p class="story">
            Once upon a time there were three little sisters; and their names were
            <a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
複製程式碼

這裡我們選擇的是第一個a節點的父節點元素。很明顯,它的父節點是p節點,輸出結果便是p節點及其內部的內容。

需要注意的是,這裡輸出的僅僅是a節點的直接父節點,而沒有再向外尋找父節點的祖先節點。如果想獲取所有的祖先節點,可以呼叫parents屬性:

html = """
<html>
    <body>
        <p class="story">
            <a href="http://example.com/elsie" class="sister" id="link1">
                <span>Elsie</span>
            </a>
        </p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(type(soup.a.parents))
print(list(enumerate(soup.a.parents)))
複製程式碼

執行結果如下:

<class 'generator'>
[(0, <p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>), (1, <body>
<p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
</body>), (2, <html>
<body>
<p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
</body></html>), (3, <html>
<body>
<p class="story">
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
</body></html>)]
複製程式碼

可以發現,返回結果是生成器型別。這裡用列表輸出了它的索引和內容,而列表中的元素就是a節點的祖先節點。

(3)兄弟節點

上面說明了子節點和父節點的獲取方式,如果要獲取同級的節點(也就是兄弟節點),應該怎麼辦呢?示例如下:

html = """
<html>
    <body>
        <p class="story">
            Once upon a time there were three little sisters; and their names were
            <a href="http://example.com/elsie" class="sister" id="link1">
                <span>Elsie</span>
            </a>
            Hello
            <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 
            and
            <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>
            and they lived at the bottom of a well.
        </p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print('Next Sibling', soup.a.next_sibling)
print('Prev Sibling', soup.a.previous_sibling)
print('Next Siblings', list(enumerate(soup.a.next_siblings)))
print('Prev Siblings', list(enumerate(soup.a.previous_siblings)))
複製程式碼

執行結果如下:

Next Sibling 
            Hello

Prev Sibling 
            Once upon a time there were three little sisters; and their names were

Next Siblings [(0, '\n            Hello\n            '), (1, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>), (2, ' \n            and\n            '), (3, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>), (4, '\n            and they lived at the bottom of a well.\n        ')]
Prev Siblings [(0, '\n            Once upon a time there were three little sisters; and their names were\n            ')]
複製程式碼

可以看到,這裡呼叫了4個屬性,其中next_siblingprevious_sibling分別獲取節點的下一個和上一個兄弟元素,next_siblingsprevious_siblings則分別返回所有前面和後面的兄弟節點的生成器。

(4)提取資訊

前面講解了關聯元素節點的選擇方法,如果想要獲取它們的一些資訊,比如文字、屬性等,也用同樣的方法,示例如下:

html = """
<html>
    <body>
        <p class="story">
            Once upon a time there were three little sisters; and their names were
            <a href="http://example.com/elsie" class="sister" id="link1">Bob</a><a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 
        </p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print('Next Sibling:')
print(type(soup.a.next_sibling))
print(soup.a.next_sibling)
print(soup.a.next_sibling.string)
print('Parent:')
print(type(soup.a.parents))
print(list(soup.a.parents)[0])
print(list(soup.a.parents)[0].attrs['class'])
複製程式碼

執行結果如下:

Next Sibling:
<class 'bs4.element.Tag'>
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
Lacie
Parent:
<class 'generator'>
<p class="story">
            Once upon a time there were three little sisters; and their names were
            <a class="sister" href="http://example.com/elsie" id="link1">Bob</a><a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
</p>
['story']
複製程式碼

如果返回結果是單個節點,那麼可以直接呼叫stringattrs等屬性獲得其文字和屬性;如果返回結果是多個節點的生成器,則可以轉為列表後取出某個元素,然後再呼叫stringattrs等屬性獲取其對應節點的文字和屬性。

6. 方法選擇器

前面所講的選擇方法都是通過屬性來選擇的,這種方法非常快,但是如果進行比較複雜的選擇的話,它就比較煩瑣,不夠靈活了。幸好,Beautiful Soup還為我們提供了一些查詢方法,比如find_all()find()等,呼叫它們,然後傳入相應的引數,就可以靈活查詢了。

find_all()

find_all,顧名思義,就是查詢所有符合條件的元素。給它傳入一些屬性或文字,就可以得到符合條件的元素,它的功能十分強大。

它的API如下:

find_all(name , attrs , recursive , text , **kwargs)
複製程式碼

(1)name

我們可以根據節點名來查詢元素,示例如下:

html='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
            <li class="element">Jay</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
        </ul>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(name='ul'))
print(type(soup.find_all(name='ul')[0]))
複製程式碼

執行結果如下:

[<ul class="list" id="list-1">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>, <ul class="list list-small" id="list-2">
<li class="element">Foo</li>
<li class="element">Bar</li>
</ul>]
<class 'bs4.element.Tag'>
複製程式碼

這裡我們呼叫了find_all()方法,傳入name引數,其引數值為ul。也就是說,我們想要查詢所有ul節點,返回結果是列表型別,長度為2,每個元素依然都是bs4.element.Tag型別。

因為都是Tag型別,所以依然可以進行巢狀查詢。還是同樣的文字,這裡查詢出所有ul節點後,再繼續查詢其內部的li節點:

for ul in soup.find_all(name='ul'):
    print(ul.find_all(name='li'))
複製程式碼

執行結果如下:

[<li class="element">Foo</li>, <li class="element">Bar</li>, <li class="element">Jay</li>]
[<li class="element">Foo</li>, <li class="element">Bar</li>]
複製程式碼

返回結果是列表型別,列表中的每個元素依然還是Tag型別。

接下來,就可以遍歷每個li,獲取它的文字了:

for ul in soup.find_all(name='ul'):
    print(ul.find_all(name='li'))
    for li in ul.find_all(name='li'):
        print(li.string)
複製程式碼

執行結果如下:

[<li class="element">Foo</li>, <li class="element">Bar</li>, <li class="element">Jay</li>]
Foo
Bar
Jay
[<li class="element">Foo</li>, <li class="element">Bar</li>]
Foo
Bar
複製程式碼

(2)attrs

除了根據節點名查詢,我們也可以傳入一些屬性來查詢,示例如下:

html='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1" name="elements">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
            <li class="element">Jay</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
        </ul>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(attrs={'id': 'list-1'}))
print(soup.find_all(attrs={'name': 'elements'}))
複製程式碼

執行結果如下:

[<ul class="list" id="list-1" name="elements">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>]
[<ul class="list" id="list-1" name="elements">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>]
複製程式碼

這裡查詢的時候傳入的是attrs引數,引數的型別是字典型別。比如,要查詢idlist-1的節點,可以傳入attrs={'id': 'list-1'}的查詢條件,得到的結果是列表形式,包含的內容就是符合idlist-1的所有節點。在上面的例子中,符合條件的元素個數是1,所以結果是長度為1的列表。

對於一些常用的屬性,比如idclass等,我們可以不用attrs來傳遞。比如,要查詢idlist-1的節點,可以直接傳入id這個引數。還是上面的文字,我們換一種方式來查詢:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(id='list-1'))
print(soup.find_all(class_='element'))
複製程式碼

執行結果如下:

[<ul class="list" id="list-1">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>]
[<li class="element">Foo</li>, <li class="element">Bar</li>, <li class="element">Jay</li>, <li class="element">Foo</li>, <li class="element">Bar</li>]
複製程式碼

這裡直接傳入id='list-1',就可以查詢idlist-1的節點元素了。而對於class來說,由於class在Python裡是一個關鍵字,所以後面需要加一個下劃線,即class_='element',返回的結果依然還是Tag組成的列表。

(3)text

text引數可用來匹配節點的文字,傳入的形式可以是字串,可以是正規表示式物件,示例如下:

import re
html='''
<div class="panel">
    <div class="panel-body">
        <a>Hello, this is a link</a>
        <a>Hello, this is a link, too</a>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(text=re.compile('link')))
複製程式碼

執行結果如下:

['Hello, this is a link', 'Hello, this is a link, too']
複製程式碼

這裡有兩個a節點,其內部包含文字資訊。這裡在find_all()方法中傳入text引數,該引數為正規表示式物件,結果返回所有匹配正規表示式的節點文字組成的列表。

find()

除了find_all()方法,還有find()方法,只不過後者返回的是單個元素,也就是第一個匹配的元素,而前者返回的是所有匹配的元素組成的列表。示例如下:

html='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
            <li class="element">Jay</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
        </ul>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.find(name='ul'))
print(type(soup.find(name='ul')))
print(soup.find(class_='list'))
複製程式碼

執行結果如下:

<ul class="list" id="list-1">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>
<class 'bs4.element.Tag'>
<ul class="list" id="list-1">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
</ul>
複製程式碼

這裡的返回結果不再是列表形式,而是第一個匹配的節點元素,型別依然是Tag型別。

另外,還有許多查詢方法,其用法與前面介紹的find_all()find()方法完全相同,只不過查詢範圍不同,這裡簡單說明一下。

  • find_parents()find_parent():前者返回所有祖先節點,後者返回直接父節點。
  • find_next_siblings()find_next_sibling():前者返回後面所有的兄弟節點,後者返回後面第一個兄弟節點。
  • find_previous_siblings()find_previous_sibling():前者返回前面所有的兄弟節點,後者返回前面第一個兄弟節點。
  • find_all_next()find_next():前者返回節點後所有符合條件的節點,後者返回第一個符合條件的節點。
  • find_all_previous()find_previous():前者返回節點後所有符合條件的節點,後者返回第一個符合條件的節點。

7. CSS選擇器

Beautiful Soup還提供了另外一種選擇器,那就是CSS選擇器。如果對Web開發熟悉的話,那麼對CSS選擇器肯定也不陌生。如果不熟悉的話,可以參考www.w3school.com.cn/cssref/css_…瞭解。

使用CSS選擇器時,只需要呼叫select()方法,傳入相應的CSS選擇器即可,示例如下:

html='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
            <li class="element">Jay</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
        </ul>
    </div>
</div>
'''
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.select('.panel .panel-heading'))
print(soup.select('ul li'))
print(soup.select('#list-2 .element'))
print(type(soup.select('ul')[0]))
複製程式碼

執行結果如下:

[<div class="panel-heading">
<h4>Hello</h4>
</div>]
[<li class="element">Foo</li>, <li class="element">Bar</li>, <li class="element">Jay</li>, <li class="element">Foo</li>, <li class="element">Bar</li>]
[<li class="element">Foo</li>, <li class="element">Bar</li>]
<class 'bs4.element.Tag'>
複製程式碼

這裡我們用了3次CSS選擇器,返回的結果均是符合CSS選擇器的節點組成的列表。例如,select('ul li')則是選擇所有ul節點下面的所有li節點,結果便是所有的li節點組成的列表。

最後一句列印輸出了列表中元素的型別。可以看到,型別依然是Tag型別。

巢狀選擇

select()方法同樣支援巢狀選擇。例如,先選擇所有ul節點,再遍歷每個ul節點,選擇其li節點,樣例如下:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
for ul in soup.select('ul'):
    print(ul.select('li'))
複製程式碼

執行結果如下:

[<li class="element">Foo</li>, <li class="element">Bar</li>, <li class="element">Jay</li>]
[<li class="element">Foo</li>, <li class="element">Bar</li>]
複製程式碼

可以看到,這裡正常輸出了所有ul節點下所有li節點組成的列表。

獲取屬性

我們知道節點型別是Tag型別,所以獲取屬性還可以用原來的方法。仍然是上面的HTML文字,這裡嘗試獲取每個ul節點的id屬性:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
for ul in soup.select('ul'):
    print(ul['id'])
    print(ul.attrs['id'])
複製程式碼

執行結果如下:

list-1
list-1
list-2
list-2
複製程式碼

可以看到,直接傳入中括號和屬性名,以及通過attrs屬性獲取屬性值,都可以成功。

獲取文字

要獲取文字,當然也可以用前面所講的string屬性。此外,還有一個方法,那就是get_text(),示例如下:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
for li in soup.select('li'):
    print('Get Text:', li.get_text())
    print('String:', li.string)
複製程式碼

執行結果如下:

Get Text: Foo
String: Foo
Get Text: Bar
String: Bar
Get Text: Jay
String: Jay
Get Text: Foo
String: Foo
Get Text: Bar
String: Bar
複製程式碼

可以看到,二者的效果完全一致。

到此,Beautiful Soup的用法基本就介紹完了,最後做一下簡單的總結。

  • 推薦使用lxml解析庫,必要時使用html.parser。
  • 節點選擇篩選功能弱但是速度快。
  • 建議使用find()或者find_all()查詢匹配單個結果或者多個結果。
  • 如果對CSS選擇器熟悉的話,可以使用select()方法選擇。


本資源首發於崔慶才的個人部落格靜覓: Python3網路爬蟲開發實戰教程 | 靜覓

如想了解更多爬蟲資訊,請關注我的個人微信公眾號:進擊的Coder

weixin.qq.com/r/5zsjOyvEZ… (二維碼自動識別)


相關文章