爬蟲入門系列(四):HTML 文字解析庫 BeautifulSoup

劉志軍發表於2019-02-27

系列文章的第3篇介紹了網路請求庫神器 Requests ,請求把資料返回來之後就要提取目標資料,不同的網站返回的內容通常有多種不同的格式,一種是 json 格式,這類資料對開發者來說最友好。另一種 XML 格式的,還有一種最常見格式的是 HTML 文件,今天就來講講如何從 HTML 中提取出感興趣的資料

自己寫個 HTML 解析器來解析嗎?還是用正規表示式?這些都不是最好的辦法,好在,Python 社群在這方便早就有了很成熟的方案,BeautifulSoup 就是這一類問題的剋星,它專注於 HTML 文件操作,名字來源於 Lewis Carroll 的一首同名詩歌。

BeautifulSoup 是一個用於解析 HTML 文件的 Python 庫,通過 BeautifulSoup,你只需要用很少的程式碼就可以提取出 HTML 中任何感興趣的內容,此外,它還有一定的 HTML 容錯能力,對於一個格式不完整的HTML 文件,它也可以正確處理。

安裝 BeautifulSoup

pip install beautifulsoup4複製程式碼

BeautifulSoup3 被官方放棄維護,你要下載最新的版本 BeautifulSoup4。

HTML 標籤

學習 BeautifulSoup4 前有必要先對 HTML 文件有一個基本認識,如下程式碼,HTML 是一個樹形組織結構。

<html>  
    <head>
     <title>hello, world</title>
    </head>
    <body>
        <h1>BeautifulSoup</h1>
        <p>如何使用BeautifulSoup</p>
    <body>
</html>複製程式碼
  • 它由很多標籤(Tag)組成,比如 html、head、title等等都是標籤
  • 一個標籤對構成一個節點,比如 … 是一個根節點
  • 節點之間存在某種關係,比如 h1 和 p 互為鄰居,他們是相鄰的兄弟(sibling)節點
  • h1 是 body 的直接子(children)節點,還是 html 的子孫(descendants)節點
  • body 是 p 的父(parent)節點,html 是 p 的祖輩(parents)節點
  • 巢狀在標籤之間的字串是該節點下的一個特殊子節點,比如 “hello, world” 也是一個節點,只不過沒名字。

使用 BeautifulSoup

構建一個 BeautifulSoup 物件需要兩個引數,第一個引數是將要解析的 HTML 文字字串,第二個引數告訴 BeautifulSoup 使用哪個解析器來解析 HTML。

解析器負責把 HTML 解析成相關的物件,而 BeautifulSoup 負責運算元據(增刪改查)。”html.parser” 是Python內建的解析器,”lxml” 則是一個基於c語言開發的解析器,它的執行速度更快,不過它需要額外安裝

通過 BeautifulSoup 物件就可以定位到 HTML 中的任何一個標籤節點。

from bs4 import BeautifulSoup  
text = """
<html>  
    <head>
     <title >hello, world</title>
    </head>
    <body>
        <h1>BeautifulSoup</h1>
        <p class="bold">如何使用BeautifulSoup</p>
        <p class="big" id="key1"> 第二個p標籤</p>
        <a href="http://foofish.net">python</a>
    </body>
</html>  
"""
soup = BeautifulSoup(text, "html.parser")

# title 標籤
>>> soup.title
<title>hello, world</title>

# p 標籤
>>> soup.p
<p class="bold">u5982u4f55u4f7fu7528BeautifulSoup</p>

# p 標籤的內容
>>> soup.p.string
u`u5982u4f55u4f7fu7528BeautifulSoup`複製程式碼

BeatifulSoup 將 HTML 抽象成為 4 類主要的資料型別,分別是Tag , NavigableString , BeautifulSoup,Comment 。每個標籤節點就是一個Tag物件,NavigableString 物件一般是包裹在Tag物件中的字串,BeautifulSoup 物件代表整個 HTML 文件。例如:

>>> type(soup)
<class `bs4.BeautifulSoup`>
>>> type(soup.h1)
<class `bs4.element.Tag`>
>>> type(soup.p.string)
<class `bs4.element.NavigableString`>複製程式碼

Tag

每個 Tag 都有一個名字,它對應 HTML 的標籤名稱。

>>> soup.h1.name
u`h1`
>>> soup.p.name
u`p`複製程式碼

標籤還可以有屬性,屬性的訪問方式和字典是類似的,它返回一個列表物件


>>> soup.p[`class`]
[u`bold`]複製程式碼

獲取標籤中的內容,直接使用 .stirng 即可獲取,它是一個 NavigableString 物件,你可以顯式地將它轉換為 unicode 字串。

>>> soup.p.string
u`u5982u4f55u4f7fu7528BeautifulSoup`
>>> type(soup.p.string)
<class `bs4.element.NavigableString`>
>>> unicode_str = unicode(soup.p.string)
>>> unicode_str
u`u5982u4f55u4f7fu7528BeautifulSoup`複製程式碼

基本概念介紹完,現在可以正式進入主題了,如何從 HTML 中找到我們關心的資料?BeautifulSoup 提供了兩種方式,一種是遍歷,另一種是搜尋,通常兩者結合來完成查詢任務。

遍歷文件樹

遍歷文件樹,顧名思義,就是是從根節點 html 標籤開始遍歷,直到找到目標元素為止,遍歷的一個缺陷是,如果你要找的內容在文件的末尾,那麼它要遍歷整個文件才能找到它,速度上就慢了。因此還需要配合第二種方法。

通過遍歷文件樹的方式獲取標籤節點可以直接通過 .標籤名的方式獲取,例如:

獲取 body 標籤:

>>> soup.body
<body>
<h1>BeautifulSoup</h1>
<p class="bold">u5982u4f55u4f7fu7528BeautifulSoup</p>
</body>複製程式碼

獲取 p 標籤

>>> soup.body.p
<p class="bold">u5982u4f55u4f7fu7528BeautifulSoup</p>複製程式碼

獲取 p 標籤的內容

>>> soup.body.p.string
u5982u4f55u4f7fu7528BeautifulSoup複製程式碼

前面說了,內容也是一個節點,這裡就可以用 .string 的方式得到。遍歷文件樹的另一個缺點是隻能獲取到與之匹配的第一個子節點,例如,如果有兩個相鄰的 p 標籤時,第二個標籤就沒法通過 .p 的方式獲取,這是需要借用 next_sibling 屬性獲取相鄰且在後面的節點。此外,還有很多不怎麼常用的屬性,比如:.contents 獲取所有子節點,.parent 獲取父節點,更多的參考請檢視官方文件

搜尋文件樹

搜尋文件樹是通過指定標籤名來搜尋元素,另外還可以通過指定標籤的屬性值來精確定位某個節點元素,最常用的兩個方法就是 find 和 find_all。這兩個方法在 BeatifulSoup 和 Tag 物件上都可以被呼叫。

find_all()

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

find_all 的返回值是一個 Tag 組成的列表,方法呼叫非常靈活,所有的引數都是可選的。

第一個引數 name 是標籤節點的名字。

# 找到所有標籤名為title的節點
>>> soup.find_all("title")
[<title>hello, world</title>]
>>> soup.find_all("p")
[<p class="bold">xc8xe7xbaxcexcaxb9xd3xc3BeautifulSoup</p>, 
<p class="big"> xb5xdaxb6xfexb8xf6pxb1xeaxc7xa9</p>]複製程式碼

第二個引數是標籤的class屬性值

# 找到所有class屬性為big的p標籤
>>> soup.find_all("p", "big")
[<p class="big"> xb5xdaxb6xfexb8xf6pxb1xeaxc7xa9</p>]複製程式碼

等效於

>>> soup.find_all("p", class_="big")
[<p class="big"> xb5xdaxb6xfexb8xf6pxb1xeaxc7xa9</p>]複製程式碼

因為 class 是 Python 關鍵字,所以這裡指定為 class_。

kwargs 是標籤的屬性名值對,例如:查詢有href屬性值為 “foofish.net” 的標籤

>>> soup.find_all(href="http://foofish.net")
[<a href="http://foofish.net">python</a>]複製程式碼

當然,它還支援正規表示式

>>> import re
>>> soup.find_all(href=re.compile("^http"))
[<a href="http://foofish.net">python</a>]複製程式碼

屬性除了可以是具體的值、正規表示式之外,它還可以是一個布林值(True/Flase),表示有屬性或者沒有該屬性。

>>> soup.find_all(id="key1")
[<p class="big" id="key1"> xb5xdaxb6xfexb8xf6pxb1xeaxc7xa9</p>]
>>> soup.find_all(id=True)
[<p class="big" id="key1"> xb5xdaxb6xfexb8xf6pxb1xeaxc7xa9</p>]複製程式碼

遍歷和搜尋相結合查詢,先定位到 body 標籤,縮小搜尋範圍,再從 body 中找 a 標籤。

>>> body_tag = soup.body
>>> body_tag.find_all("a")
[<a href="http://foofish.net">python</a>]複製程式碼

find()

find 方法跟 find_all 類似,唯一不同的地方是,它返回的單個 Tag 物件而非列表,如果沒找到匹配的節點則返回 None。如果匹配多個 Tag,只返回第0個。


>>> body_tag.find("a")
<a href="http://foofish.net">python</a>
>>> body_tag.find("p")
<p class="bold">xc8xe7xbaxcexcaxb9xd3xc3BeautifulSoup</p>複製程式碼

get_text()

獲取標籤裡面內容,除了可以使用 .string 之外,還可以使用 get_text 方法,不同的地方在於前者返回的一個 NavigableString 物件,後者返回的是 unicode 型別的字串。

>>> p1 = body_tag.find(`p`).get_text()
>>> type(p1)
<type `unicode`>
>>> p1
u`xc8xe7xbaxcexcaxb9xd3xc3BeautifulSoup`

>>> p2 = body_tag.find("p").string
>>> type(p2)
<class `bs4.element.NavigableString`>
>>> p2
u`xc8xe7xbaxcexcaxb9xd3xc3BeautifulSoup`
>>>複製程式碼

實際場景中我們一般使用 get_text 方法獲取標籤中的內容。

總結

BeatifulSoup 是一個用於操作 HTML 文件的 Python 庫,初始化 BeatifulSoup 時,需要指定 HTML 文件字串和具體的解析器。BeatifulSoup 有3類常用的資料型別,分別是 Tag、NavigableString、和 BeautifulSoup。查詢 HTML元素有兩種方式,分別是遍歷文件樹和搜尋文件樹,通常快速獲取資料需要二者結合。最後是一個實踐專案:用 Requests 和 BeatifulSoup 把廖雪峰的教程轉換成 PDF 電子書

相關文章