目錄
前言
一、Beautiful Soup 簡介
1.1 Beautiful Soup概述
1.2 準備工作
1.3 解析器
二、基本使用
三、節點選擇器的使用
3.1 選擇元素
3.2 提取信息
3.2.1 獲取名稱
3.2.2 獲取屬性
3.2.3 獲取內容
3.3 嵌套選擇
3.4 關聯選擇
3.4.1 子節點和子孫節點
3.4.2 父節點和祖先節點
3.4.3 兄弟節點
3.4.4 提取信息
前言
??????? 在之前的內容中,我們深入探討了正則表達式的多種用法。然而,正則表達式并非十全十美,一旦出現編寫錯誤或邏輯漏洞,最終得到的結果往往與我們的預期大相徑庭。
????????網頁作為一種具有特定結構和層級關系的信息載體,許多節點都通過 id 或 class 進行了明確區分。那么,能否利用這些網頁的結構和屬性特點,找到一種更為高效、準確的提取方法呢?
????????在本節中,我們將為大家介紹一個強大的網頁解析工具 ——Beautiful Soup。它能夠依據網頁的結構和屬性等特性,輕松地對網頁進行解析。使用 Beautiful Soup,我們無需再編寫復雜冗長的正則表達式,只需幾條簡潔明了的語句,就能完成網頁中特定元素的提取工作。
????????接下來,讓我們一起深入了解 Beautiful Soup 的強大功能,開啟高效解析網頁的新篇章!
一、Beautiful Soup 簡介
1.1 Beautiful Soup概述
??????? 簡單來說,BeautifulSoup 是 Python 語言中一個專門用于解析 HTML 或 XML 的強大庫。它提供了極為便捷的方式,能夠輕松地從網頁中抽取我們所需的數據。其官方對 BeautifulSoup 的解釋為:
????????BeautifulSoup 提供了一系列簡單易用、充滿 Python 風格的函數,這些函數可以實現導航、搜索、修改分析樹等多種功能。它就像一個功能齊全的工具箱,通過對文檔進行解析,為用戶精準地提供所需抓取的數據。而且,BeautifulSoup 的使用非常簡便,只需少量的代碼,就能構建出一個功能完整的應用程序。
????????此外,BeautifulSoup 還具備自動轉換編碼的功能。它會自動將輸入的文檔轉換為 Unicode 編碼,輸出的文檔則轉換為 utf - 8 編碼。當然,如果文檔本身沒有指定編碼方式,用戶只需簡單說明原始編碼方式即可。
????????如今,BeautifulSoup 已經與 lxml、html5lib 等一樣,成為了出色的 Python 解釋器。它能夠為用戶靈活地提供不同的解析策略,并且在解析速度方面表現強勁。
????????由此可見,借助 Beautiful Soup,我們可以大大簡化繁瑣的網頁數據提取工作,顯著提升解析效率。
1.2 準備工作
????????在開始使用 Beautiful Soup 進行網頁解析之前,請務必確保已經正確安裝了 Beautiful Soup 和 lxml 庫。如果還沒有安裝,請自行安裝。
1.3 解析器
????????Beautiful Soup 在進行網頁解析時,實際上是依賴于解析器來完成工作的。它不僅支持 Python 標準庫中自帶的 HTML 解析器,還兼容一些功能強大的第三方解析器,比如 lxml。下面的表詳細列出了 Beautiful Soup 所支持的各種解析器。
Beautiful Soup支持的解析器
????????通過上述對比可以清晰地看出,lxml 解析器不僅具備解析 HTML 和 XML 的雙重功能,而且在速度方面表現出色,同時還具有很強的容錯能力。因此,我們強烈推薦使用 lxml 解析器。
????????如果選擇使用 lxml 解析器,在初始化 Beautiful Soup 對象時,只需將第二個參數設置為 lxml 即可。示例代碼如下:
from bs4 import BeautifulSoup
soup = BeautifulSoup('<p>Hello</p>', 'lxml')
print(soup.p.string)
????????在后續關于 Beautiful Soup 的用法實例演示中,我們將統一采用 lxml 解析器進行講解。
二、基本使用
????????下面,我們通過一個具體的實例來深入了解 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 的變量,它存儲了一段 HTML 字符串。需要注意的是,這段字符串并不是一個完整的 HTML 字符串,其中的 body 和 html 節點都沒有閉合。
????????接下來,我們將 html 字符串作為第一個參數傳遞給 BeautifulSoup 對象,同時將第二個參數設置為解析器類型 “lxml”,這樣就完成了 BeautifulSoup 對象的初始化,并將其賦值給了 soup 變量。
????????初始化完成后,我們就可以調用 soup 對象的各種方法和屬性,來對這段 HTML 代碼進行解析了。
????????首先,我們調用了 prettify () 方法。這個方法的作用是將待解析的字符串以標準縮進格式輸出,使代碼結構更加清晰易讀。需要特別注意的是,在輸出結果中,我們可以看到原本未閉合的 body 和 html 節點都已經自動閉合了。這并不是 prettify () 方法的作用,而是在初始化 BeautifulSoup 對象時,它就已經自動對不規范的 HTML 字符串進行了格式校正。
????????然后,我們調用了 soup.title.string。這里的 soup.title 可以選中 HTML 中的 title 節點,而 string 屬性則用于獲取該節點中的文本內容。通過這兩個簡單的屬性調用,我們就輕松完成了文本的提取工作,充分體現了 Beautiful Soup 的便捷性。
三、節點選擇器的使用
????????在 Beautiful Soup 中,直接調用節點的名稱就可以選擇對應的節點元素,再調用 string
屬性就能夠獲取節點內的文本內容。這種選擇方式的優點是速度非常快,特別適用于單個節點結構層次清晰的情況。
3.1 選擇元素
下面,我們通過一個具體的例子來詳細說明如何選擇元素:
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)
運行結果:
????????在這個例子中,我們仍然使用了之前的 HTML 代碼。首先,我們打印輸出了 title 節點的選擇結果,可以看到輸出的正是 title 節點及其內部的文字內容。
????????接著,我們輸出了 title 節點的類型,結果是 bs4.element.Tag
類型。這是 Beautiful Soup 中一個非常重要的數據結構,經過選擇器選擇后的結果,通常都是這種 Tag 類型。
????????Tag 類型具有一些實用的屬性,比如 string 屬性。當我們調用 string 屬性時,就可以獲取到節點的文本內容,這也正是接下來的輸出結果。
?
????????然后,我們嘗試選擇了 head 節點,輸出結果顯示的是 head 節點及其內部的所有內容。
????????最后,我們選擇了 p 節點。但這里需要注意的是,輸出結果僅僅是第一個 p 節點的內容,后面的幾個 p 節點并沒有被選中。這表明,當存在多個相同節點時,這種直接調用節點名稱的選擇方式只會選擇到第一個匹配的節點,而忽略后面的其他節點。
3.2 提取信息
????????在前面的演示中,我們展示了如何通過調用 string
屬性來獲取節點文本的值。那么,如何獲取節點屬性的值呢?又如何獲取節點的名稱呢?下面,我們將對這些信息的提取方式進行統一梳理。
3.2.1 獲取名稱
????????在 Beautiful Soup 中,可以利用 name
屬性來獲取節點的名稱。我們還是以上面的文本為例,選取 title 節點,然后調用 name
屬性,就可以得到節點的名稱:
print(soup.title.name)
運行結果:
title
3.2.2 獲取屬性
????????每個節點都可能擁有多個屬性,比如常見的 id 和 class 等。在選擇了一個節點元素后,可以通過調用 attrs
方法來獲取該節點的所有屬性:
print(soup.p.attrs)
print(soup.p.attrs['name'])
運行結果:
??????? 從運行結果可以看出,attrs
的返回結果是一個字典形式,它將選擇的節點的所有屬性和屬性值組合成了一個字典。
????????如果我們想要獲取某個特定的屬性值,比如 name 屬性,就相當于從字典中獲取某個鍵值,只需要使用中括號加上屬性名即可。例如,要獲取 name 屬性的值,可以通過 attrs['name']
來實現。
????????其實,還有一種更簡便的獲取屬性值的方式,我們可以不用寫 attrs,直接在節點元素后面加上中括號,傳入屬性名就可以獲取屬性值了。示例代碼如下:
print(soup.p['name'])
print(soup.p['class'])
運行結果如下:
dromouse
['title']
????????在這里需要注意的是,不同屬性的返回結果類型可能不同。有的返回結果是字符串,比如 name 屬性的值是唯一的,所以返回的是單個字符串;而對于 class 屬性,一個節點元素可能有多個 class,因此返回的是一個由字符串組成的列表。在實際處理過程中,我們需要根據具體情況判斷屬性值的類型。
3.2.3 獲取內容
????????我們可以利用 string
屬性來獲取節點元素所包含的文本內容。例如,要獲取第一個 p 節點的文本內容,可以使用以下代碼:
print(soup.p.string)
運行結果如下:
The Dormouse's story
????????再次強調,這里選擇到的 p 節點是第一個 p 節點,獲取的文本內容也是第一個 p 節點內部的文本。
3.3 嵌套選擇
????????在前面的例子中,我們知道每一個通過選擇器得到的返回結果都是 bs4.element.Tag
類型。這種類型的對象同樣可以繼續調用節點進行下一步的選擇,也就是所謂的嵌套選擇。
????????例如,我們先獲取了 head 節點元素,然后可以繼續調用 head 節點內部的 title 節點元素:
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)
運行結果如下:
????????第一行輸出結果是我們調用 head 之后再次調用 title 而選擇的 title 節點元素。然后,我們打印輸出了它的類型,可以看到它仍然是 bs4.element.Tag
類型。
????????這說明,我們在 Tag 類型的基礎上再次進行選擇得到的結果依然是 Tag 類型。每次返回的結果類型都相同,這就為我們進行嵌套選擇提供了可能。
????????最后,我們輸出了它的 string
屬性,也就是該節點里的文本內容。
3.4 關聯選擇
????????在實際進行節點選擇時,有時候我們無法一步就選到想要的節點元素,而是需要先選中某一個節點元素,然后以它為基準,再去選擇它的子節點、父節點、兄弟節點等。下面,我們就來詳細介紹如何選擇這些關聯的節點元素。
3.4.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>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.p.contents)
運行結果如下:
?
????????從運行結果可以看出,返回的結果是一個列表形式。p 節點中既包含了文本內容,又包含了其他節點元素,最終這些內容都會以列表的形式統一返回。
????????需要注意的是,列表中的每個元素都是 p 節點的直接子節點。例如,第一個 a 節點里面包含了一層 span 節點,這相當于 p 節點的孫子節點,但在返回結果中,并沒有單獨把 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)
運行結果如下:
?
????????這里我們使用的是同樣的 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)
運行結果如下:
????????此時返回的結果依然是一個生成器。通過遍歷輸出可以看到,這次的輸出結果包含了 span 節點。這是因為 descendants 屬性會遞歸查詢所有子節點,從而得到所有的子孫節點。
3.4.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)
運行結果如下:
?
????????在這個例子中,我們選擇的是第一個 a 節點的父節點元素。很明顯,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)))
運行結果如下:
????????從運行結果可以發現,返回的結果是生成器類型。我們通過將其轉換為列表,并輸出了它的索引和內容,列表中的元素就是 a 節點的祖先節點。
3.4.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)))
運行結果如下:
????????這里調用了 4 個屬性,next_sibling
和previous_sibling
分別用于獲取節點的下一個和上一個兄弟元素。如果目標節點沒有對應的兄弟節點,previous_sibling
可能返回None
,就像這里第一個<a>
標簽的previous_sibling
。next_siblings
和previous_siblings
則分別返回后面和前面的兄弟節點集合,返回結果是生成器類型,通過list()
函數將其轉換為列表以便查看所有兄弟節點。
3.4.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'])
運行結果:
????????要是選擇節點的返回結果只有一個節點,那直接用string、attrs這些屬性,就能拿到它的文本內容和屬性信息。但要是返回的是一個能生成多個節點的生成器,就得先把它變成列表,從里面挑出想要的元素,接著再用string、attrs屬性,這樣才能得到對應節點的文本和屬性。?