這是我見過最牛逼,最全面的Beautiful Soup 4.2 教程!沒有之一

Beautiful Soup 是一個可以從HTML或XML文件中提取數據的Python庫.它能夠通過你喜歡的轉換器實現慣用的文檔導航,查找,修改文檔的方式.Beautiful Soup會幫你節省數小時甚至數天的工作時間.

這篇文檔介紹了BeautifulSoup4中所有主要特性,並且有小例子.讓我來向你展示它適合做什麼,如何工作,怎樣使用,如何達到你想要的效果,和處理異常情況.

文檔中出現的例子在Python2.7和Python3.2中的執行結果相同

你可能在尋找 Beautiful Soup3 的文檔,Beautiful Soup 3 目前已經停止開發,我們推薦在現在的項目中使用Beautiful Soup 4, 移植到BS4

尋求幫助

如果你有關於BeautifulSoup的問題,可以發送郵件到 討論組 .如果你的問題包含了一段需要轉換的HTML代碼,那麼確保你提的問題描述中附帶這段HTML文檔的 代碼診斷 [1]

快速開始

下面的一段HTML代碼將作為例子被多次用到.這是 愛麗絲夢遊仙境的 的一段內容(以後內容中簡稱為 愛麗絲 的文檔):

html_doc = """

The Dormouse's story

The Dormouse's story

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

,

and

;

and they lived at the bottom of a well.

...

"""

使用BeautifulSoup解析這段代碼,能夠得到一個 BeautifulSoup 的對象,並能按照標準的縮進格式的結構輸出:

from bs4 import BeautifulSoup

soup = BeautifulSoup(html_doc)

print(soup.prettify())

#

#

#

<p># The Dormouse's story</p><p># </p>

#

#

#

#

# The Dormouse's story

#

#

#

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

#

# Elsie

#

# ,

#

# Lacie

#

# and

#

# Tillie

#

# ; and they lived at the bottom of a well.

#

#

# ...

#

#

#

幾個簡單的瀏覽結構化數據的方法:

soup.title

#

The Dormouse's story

soup.title.name

# u'title'

soup.title.string

# u'The Dormouse's story'

soup.title.parent.name

# u'head'

soup.p

#

The Dormouse's story

soup.p['class']

# u'title'

soup.a

#

soup.find_all('a')

# [ ,

# ,

# ]

soup.find(id="link3")

#

從文檔中找到所有

for link in soup.find_all('a'):

print(link.get('href'))

# http://example.com/elsie

# http://example.com/lacie

# http://example.com/tillie

從文檔中獲取所有文字內容:

print(soup.get_text())

# The Dormouse's story

#

# The Dormouse's story

#

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

# Elsie,

# Lacie and

# Tillie;

# and they lived at the bottom of a well.

#

# ...

這是你想要的嗎?彆著急,還有更好用的

安裝 Beautiful Soup

如果你用的是新版的Debain或ubuntu,那麼可以通過系統的軟件包管理來安裝:

$ apt-get install Python-bs4

Beautiful Soup 4 通過PyPi發佈,所以如果你無法使用系統包管理安裝,那麼也可以通過 easy_install 或 pip 來安裝.包的名字是 beautifulsoup4 ,這個包兼容Python2和Python3.

$ easy_install beautifulsoup4

$ pip install beautifulsoup4

(在PyPi中還有一個名字是 BeautifulSoup 的包,但那可能不是你想要的,那是 Beautiful Soup3 的發佈版本,因為很多項目還在使用BS3, 所以 BeautifulSoup 包依然有效.但是如果你在編寫新項目,那麼你應該安裝的beautifulsoup4 )

如果你沒有安裝 easy_install 或 pip ,那你也可以 下載BS4的源碼 ,然後通過setup.py來安裝.

$ Python setup.py install

如果上述安裝方法都行不通,Beautiful Soup的發佈協議允許你將BS4的代碼打包在你的項目中,這樣無須安裝即可使用.

作者在Python2.7和Python3.2的版本下開發Beautiful Soup, 理論上Beautiful Soup應該在所有當前的Python版本中正常工作

安裝完成後的問題

Beautiful Soup發佈時打包成Python2版本的代碼,在Python3環境下安裝時,會自動轉換成Python3的代碼,如果沒有一個安裝的過程,那麼代碼就不會被轉換.

如果代碼拋出了 ImportError 的異常: “No module named HTMLParser”, 這是因為你在Python3版本中執行Python2版本的代碼.

如果代碼拋出了 ImportError 的異常: “No module named html.parser”, 這是因為你在Python2版本中執行Python3版本的代碼.

如果遇到上述2種情況,最好的解決方法是重新安裝BeautifulSoup4.

如果在ROOT_TAG_NAME = u’[document]’代碼處遇到 SyntaxError “Invalid syntax”錯誤,需要將把BS4的Python代碼版本從Python2轉換到Python3. 可以重新安裝BS4:

$ Python3 setup.py install

或在bs4的目錄中執行Python代碼版本轉換腳本

$ 2to3-3.2 -w bs4

安裝解析器

Beautiful Soup支持Python標準庫中的HTML解析器,還支持一些第三方的解析器,其中一個是 lxml .根據操作系統不同,可以選擇下列方法來安裝lxml:

$ apt-get install Python-lxml

$ easy_install lxml

$ pip install lxml

另一個可供選擇的解析器是純Python實現的 html5lib , html5lib的解析方式與瀏覽器相同,可以選擇下列方法來安裝html5lib:

$ apt-get install Python-html5lib

$ easy_install html5lib

$ pip install html5lib

下表列出了主要的解析器,以及它們的優缺點:

  • 解析器使用方法優勢劣勢Python標準庫BeautifulSoup(markup, "html.parser")Python的內置標準庫
  • 執行速度適中
  • 文檔容錯能力強
  • Python 2.7.3 or 3.2.2)前 的版本中文檔容錯能力差
  • lxml HTML 解析器BeautifulSoup(markup, "lxml")速度快
  • 文檔容錯能力強
  • 需要安裝C語言庫

lxml XML 解析器BeautifulSoup(markup, ["lxml", "xml"])

BeautifulSoup(markup, "xml")

  • 速度快
  • 唯一支持XML的解析器
  • 需要安裝C語言庫
  • html5libBeautifulSoup(markup, "html5lib")最好的容錯性
  • 以瀏覽器的方式解析文檔
  • 生成HTML5格式的文檔
  • 速度慢
  • 不依賴外部擴展

推薦使用lxml作為解析器,因為效率更高. 在Python2.7.3之前的版本和Python3中3.2.2之前的版本,必須安裝lxml或html5lib, 因為那些Python版本的標準庫中內置的HTML解析方法不夠穩定.

提示: 如果一段HTML或XML文檔格式不正確的話,那麼在不同的解析器中返回的結果可能是不一樣的,查看 解析器之間的區別 瞭解更多細節

如何使用

將一段文檔傳入BeautifulSoup 的構造方法,就能得到一個文檔的對象, 可以傳入一段字符串或一個文件句柄.

from bs4 import BeautifulSoup

soup = BeautifulSoup(open("index.html"))

soup = BeautifulSoup("data")

首先,文檔被轉換成Unicode,並且HTML的實例都被轉換成Unicode編碼

BeautifulSoup("Sacré bleu!")

Sacré bleu!

然後,Beautiful Soup選擇最合適的解析器來解析這段文檔,如果手動指定解析器那麼Beautiful Soup會選擇指定的解析器來解析文檔.(參考 解析成XML ).

對象的種類

Beautiful Soup將複雜HTML文檔轉換成一個複雜的樹形結構,每個節點都是Python對象,所有對象可以歸納為4種: Tag , NavigableString , BeautifulSoup , Comment .

Tag

Tag 對象與XML或HTML原生文檔中的tag相同:

soup = BeautifulSoup('Extremely bold')

tag = soup.b

type(tag)

#

Tag有很多方法和屬性,在 遍歷文檔樹 和 搜索文檔樹 中有詳細解釋.現在介紹一下tag中最重要的屬性: name和attributes

Name

每個tag都有自己的名字,通過 .name 來獲取:

tag.name

# u'b'

如果改變了tag的name,那將影響所有通過當前Beautiful Soup對象生成的HTML文檔:

tag.name = "blockquote"

tag

#

Extremely bold

Attributes

一個tag可能有很多個屬性. tag 有一個 “class” 的屬性,值為 “boldest” . tag的屬性的操作方法與字典相同:

tag['class']

# u'boldest'

也可以直接”點”取屬性, 比如: .attrs :

tag.attrs

# {u'class': u'boldest'}

tag的屬性可以被添加,刪除或修改. 再說一次, tag的屬性操作方法與字典一樣

tag['class'] = 'verybold'

tag['id'] = 1

tag

#

Extremely bold

del tag['class']

del

tag['id']

tag

#

Extremely bold

tag['class']

# KeyError: 'class'

print(tag.get('class'))

# None

多值屬性

HTML 4定義了一系列可以包含多個值的屬性.在HTML5中移除了一些,卻增加更多.最常見的多值的屬性是 class (一個tag可以有多個CSS的class). 還有一些屬性 rel , rev , accept-charset , headers , accesskey . 在Beautiful Soup中多值屬性的返回類型是list:

css_soup = BeautifulSoup('

')

css_soup.p['class']

# ["body", "strikeout"]

css_soup = BeautifulSoup('

')

css_soup.p['class']

# ["body"]

如果某個屬性看起來好像有多個值,但在任何版本的HTML定義中都沒有被定義為多值屬性,那麼Beautiful Soup會將這個屬性作為字符串返回

id_soup = BeautifulSoup('

')

id_soup.p['id']

# 'my id'

將tag轉換成字符串時,多值屬性會合併為一個值

rel_soup = BeautifulSoup('

Back to the

')

rel_soup.a['rel']

# ['index']

rel_soup.a['rel'] = ['index', 'contents']

print(rel_soup.p)

#

Back to the

如果轉換的文檔是XML格式,那麼tag中不包含多值屬性

xml_soup = BeautifulSoup('

', 'xml')

xml_soup.p['class']

# u'body strikeout'

可以遍歷的字符串

字符串常被包含在tag內.Beautiful Soup用 NavigableString 類來包裝tag中的字符串:

tag.string

# u'Extremely bold'

type(tag.string)

#

一個 NavigableString 字符串與Python中的Unicode字符串相同,並且還支持包含在 遍歷文檔樹 和 搜索文檔樹 中的一些特性. 通過 unicode() 方法可以直接將 NavigableString 對象轉換成Unicode字符串:

unicode_string = unicode(tag.string)

unicode_string

# u'Extremely bold'

type(unicode_string)

#

tag中包含的字符串不能編輯,但是可以被替換成其它的字符串,用 replace_with() 方法:

tag.string.replace_with("No longer bold")

tag

#

No longer bold

NavigableString 對象支持 遍歷文檔樹 和 搜索文檔樹 中定義的大部分屬性, 並非全部.尤其是,一個字符串不能包含其它內容(tag能夠包含字符串或是其它tag),字符串不支持 .contents 或 .string 屬性或 find() 方法.

如果想在Beautiful Soup之外使用 NavigableString 對象,需要調用 unicode() 方法,將該對象轉換成普通的Unicode字符串,否則就算Beautiful Soup已方法已經執行結束,該對象的輸出也會帶有對象的引用地址.這樣會浪費內存.

BeautifulSoup

BeautifulSoup 對象表示的是一個文檔的全部內容.大部分時候,可以把它當作 Tag 對象,它支持 遍歷文檔樹 和 搜索文檔樹 中描述的大部分的方法.

因為 BeautifulSoup 對象並不是真正的HTML或XML的tag,所以它沒有name和attribute屬性.但有時查看它的 .name 屬性是很方便的,所以 BeautifulSoup 對象包含了一個值為 “[document]” 的特殊屬性 .name

soup.name

# u'[document]'

註釋及特殊字符串

Tag , NavigableString , BeautifulSoup 幾乎覆蓋了html和xml中的所有內容,但是還有一些特殊對象.容易讓人擔心的內容是文檔的註釋部分:

markup = ""

soup = BeautifulSoup(markup)

comment = soup.b.string

type(comment)

#

Comment 對象是一個特殊類型的 NavigableString 對象:

comment

# u'Hey, buddy. Want to buy a used parser'

但是當它出現在HTML文檔中時, Comment 對象會使用特殊的格式輸出:

print(soup.b.prettify())

#

#

#

Beautiful Soup中定義的其它類型都可能會出現在XML的文檔中: CData , ProcessingInstruction , Declaration , Doctype .與 Comment 對象類似,這些類都是 NavigableString 的子類,只是添加了一些額外的方法的字符串獨享.下面是用CDATA來替代註釋的例子:

from bs4 import CData

cdata = CData("A CDATA block")

comment.replace_with(cdata)

print(soup.b.prettify())

#

#

#

遍歷文檔樹

還拿”愛麗絲夢遊仙境”的文檔來做例子:

html_doc = """

The Dormouse's story

The Dormouse's story

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

,

and

;

and they lived at the bottom of a well.

...

"""

from bs4 import BeautifulSoup

soup = BeautifulSoup(html_doc)

通過這段例子來演示怎樣從文檔的一段內容找到另一段內容

子節點

一個Tag可能包含多個字符串或其它的Tag,這些都是這個Tag的子節點.Beautiful Soup提供了許多操作和遍歷子節點的屬性.

注意: Beautiful Soup中字符串節點不支持這些屬性,因為字符串沒有子節點

tag的名字

操作文檔樹最簡單的方法就是告訴它你想獲取的tag的name.如果想獲取

標籤,只要用 soup.head :

soup.head

#

The Dormouse's story

soup.title

#

The Dormouse's story

這是個獲取tag的小竅門,可以在文檔樹的tag中多次調用這個方法.下面的代碼可以獲取

標籤中的第一個標籤:

soup.body.b

# The Dormouse's story

通過點取屬性的方式只能獲得當前名字的第一個tag:

soup.a

#

如果想要得到所有的

soup.find_all('a')

# [ ,

# ,

# ]

.contents 和 .children

tag的 .contents 屬性可以將tag的子節點以列表的方式輸出:

head_tag = soup.head

head_tag

#

The Dormouse's story

head_tag.contents

[

The Dormouse's story]

title_tag = head_tag.contents[0]

title_tag

#

The Dormouse's story

title_tag.contents

# [u'The Dormouse's story']

BeautifulSoup 對象本身一定會包含子節點,也就是說標籤也是 BeautifulSoup 對象的子節點:

len(soup.contents)

# 1

soup.contents[0].name

# u'html'

字符串沒有 .contents 屬性,因為字符串沒有子節點:

text = title_tag.contents[0]

text.contents

# AttributeError: 'NavigableString' object has no attribute 'contents'

通過tag的 .children 生成器,可以對tag的子節點進行循環:

for child in title_tag.children:

print(child)

# The Dormouse's story

.descendants

.contents 和 .children 屬性僅包含tag的直接子節點.例如,

標籤只有一個直接子節點<p>head_tag.contents</p><p># [</p><title>The Dormouse's story]

但是

標籤也包含一個子節點:字符串 “The Dormouse’s story”,這種情況下字符串 “The Dormouse’s story”也屬於標籤的子孫節點. .descendants 屬性可以對所有tag的子孫節點進行遞歸循環 [5] :<p> <div class="lazy-load-ad"></div><strong>for</strong> child <strong>in</strong> head_tag.descendants:</p><p> <strong>print</strong>(child)</p><p> # </p><title>The Dormouse's story

# The Dormouse's story

上面的例子中,

標籤只有一個子節點,但是有2個子孫節點:節點和的子節點, BeautifulSoup 有一個直接子節點(節點),卻有很多子孫節點:

len(list(soup.children))

# 1

len(list(soup.descendants))

# 25

.string

如果tag只有一個 NavigableString 類型子節點,那麼這個tag可以使用 .string 得到子節點:

title_tag.string

# u'The Dormouse's story'

如果一個tag僅有一個子節點,那麼這個tag也可以使用 .string 方法,輸出結果與當前唯一子節點的 .string 結果相同:

head_tag.contents

# [

The Dormouse's story]

head_tag.string

# u'The Dormouse's story'

如果tag包含了多個子節點,tag就無法確定 .string 方法應該調用哪個子節點的內容, .string 的輸出結果是 None :

print(soup.html.string)

# None

.strings 和 stripped_strings

如果tag中包含多個字符串 [2] ,可以使用 .strings 來循環獲取:

for string in soup.strings:

print(repr(string))

# u"The Dormouse's story"

# u'\n\n'

# u"The Dormouse's story"

# u'\n\n'

# u'Once upon a time there were three little sisters; and their names were\n'

# u'Elsie'

# u',\n'

# u'Lacie'

# u' and\n'

# u'Tillie'

# u';\nand they lived at the bottom of a well.'

# u'\n\n'

# u'...'

# u'\n'

輸出的字符串中可能包含了很多空格或空行,使用 .stripped_strings 可以去除多餘空白內容:

for string in soup.stripped_strings:

print(repr(string))

# u"The Dormouse's story"

# u"The Dormouse's story"

# u'Once upon a time there were three little sisters; and their names were'

# u'Elsie'

# u','

# u'Lacie'

# u'and'

# u'Tillie'

# u';\nand they lived at the bottom of a well.'

# u'...'

全部是空格的行會被忽略掉,段首和段末的空白會被刪除

父節點

繼續分析文檔樹,每個tag或字符串都有父節點:被包含在某個tag中

.parent

通過 .parent 屬性來獲取某個元素的父節點.在例子“愛麗絲”的文檔中,

標籤是標籤的父節點:<p>title_tag = soup.title</p><p>title_tag</p><p># </p><title>The Dormouse's story

title_tag.parent

#

The Dormouse's story

文檔title的字符串也有父節點:

標籤<p>title_tag.string.parent</p><p># </p><title>The Dormouse's story

文檔的頂層節點比如的父節點是 BeautifulSoup 對象:

html_tag = soup.html

type(html_tag.parent)

#

BeautifulSoup 對象的 .parent 是None:

print(soup.parent)

# None

.parents

通過元素的 .parents 屬性可以遞歸得到元素的所有父輩節點,下面的例子使用了 .parents 方法遍歷了

link = soup.a

link

#

for parent in link.parents:

if parent is None:

print(parent)

else:

print(parent.name)

# p

# body

# html

# [document]

# None

兄弟節點

看一段簡單的例子:

sibling_soup = BeautifulSoup(" ")

print(sibling_soup.prettify())

#

#

#

#

# text1

#

#

# text2

#

#

#

#

因為標籤和標籤是同一層:他們是同一個元素的子節點,所以

可以被稱為兄弟節點.一段文檔以標準格式輸出時,兄弟節點有相同的縮進級別.在代碼中也可以使用這種關係.

.next_sibling 和 .previous_sibling

在文檔樹中,使用 .next_sibling 和 .previous_sibling 屬性來查詢兄弟節點:

sibling_soup.b.next_sibling

# text2

sibling_soup.c.previous_sibling

# text1

標籤有 .next_sibling 屬性,但是沒有 .previous_sibling 屬性,因為標籤在同級節點中是第一個.同理,標籤有 .previous_sibling 屬性,卻沒有 .next_sibling 屬性:

print(sibling_soup.b.previous_sibling)

# None

print(sibling_soup.c.next_sibling)

# None

例子中的字符串“text1”和“text2”不是兄弟節點,因為它們的父節點不同:

sibling_soup.b.string

# u'text1'

print(sibling_soup.b.string.next_sibling)

# None

實際文檔中的tag的 .next_sibling 和 .previous_sibling 屬性通常是字符串或空白. 看看“愛麗絲”文檔:

如果以為第一個

link = soup.a

link

#

link.next_sibling

# u',\n'

第二個

link.next_sibling.next_sibling

#

.next_siblings 和 .previous_siblings

通過 .next_siblings 和 .previous_siblings 屬性可以對當前節點的兄弟節點迭代輸出:

for sibling in soup.a.next_siblings:

print(repr(sibling))

# u',\n'

#

# u' and\n'

#

# u'; and they lived at the bottom of a well.'

# None

for sibling in soup.find(id="link3").previous_siblings:

print(repr(sibling))

# ' and\n'

#

# u',\n'

#

# u'Once upon a time there were three little sisters; and their names were\n'

# None

回退和前進

看一下“愛麗絲” 文檔:

The Dormouse's story

The Dormouse's story

HTML解析器把這段字符串轉換成一連串的事件: “打開標籤”,”打開一個

標籤”,”打開一個標籤”,”添加一段字符串”,”關閉<title>標籤”,”打開<p>標籤”,等等.Beautiful Soup提供了重現解析器初始化過程的方法.</p><p>.next_element 和 .previous_element</p><p>.next_element 屬性指向解析過程中下一個被解析的對象(字符串或tag),結果可能與 .next_sibling 相同,但通常是不一樣的.</p><p>這是“愛麗絲”文檔中最後一個</p><p>last_a_tag = soup.find("a", id="link3")</p><p>last_a_tag</p><p># </p><p>last_a_tag.next_sibling</p><p># '; and they lived at the bottom of a well.'</p><p>但這個</p><p>last_a_tag.next_element</p><p># u'Tillie'</p><p>這是因為在原始文檔中,字符串“Tillie” 在分號前出現,解析器先進入 標籤,然後是分號和剩餘部分.分號與 <div class="lazy-load-ad"></div></p><p>.previous_element 屬性剛好與 .next_element 相反,它指向當前被解析的對象的前一個解析對象:</p><p>last_a_tag.previous_element</p><p># u' and\n'</p><p>last_a_tag.previous_element.next_element</p><p># </p><p>.next_elements 和 .previous_elements</p><p>通過 .next_elements 和 .previous_elements 的迭代器就可以向前或向後訪問文檔的解析內容,就好像文檔正在被解析一樣:</p><p><strong>for</strong> element <strong>in</strong> last_a_tag.next_elements:</p><p> <strong>print</strong>(repr(element))</p><p># u'Tillie'</p><p># u';\nand they lived at the bottom of a well.'</p><p># u'\n\n'</p><p># </p><p class="story">...</p><p># u'...'</p><p># u'\n'</p><p># None</p><h1>搜索文檔樹</h1><p>Beautiful Soup定義了很多搜索方法,這裡著重介紹2個: find() 和 find_all() .其它方法的參數和用法類似,請讀者舉一反三. <div class="lazy-load-ad"></div></p><p>再以“愛麗絲”文檔作為例子:</p><p>html_doc = """</p><p/><title>The Dormouse's story

The Dormouse's story

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

,

and

;

and they lived at the bottom of a well.

...

"""

from bs4 import BeautifulSoup

soup = BeautifulSoup(html_doc)

使用 find_all() 類似的方法可以查找到想要查找的文檔內容

過濾器

介紹 find_all() 方法前,先介紹一下過濾器的類型 [3] ,這些過濾器貫穿整個搜索的API.過濾器可以被用在tag的name中,節點的屬性中,字符串中或他們的混合中.

字符串

最簡單的過濾器是字符串.在搜索方法中傳入一個字符串參數,Beautiful Soup會查找與字符串完整匹配的內容,下面的例子用於查找文檔中所有的標籤:

soup.find_all('b')

# [The Dormouse's story]

如果傳入字節碼參數,Beautiful Soup會當作UTF-8編碼,可以傳入一段Unicode 編碼來避免Beautiful Soup解析編碼出錯

正則表達式

如果傳入正則表達式作為參數,Beautiful Soup會通過正則表達式的 match() 來匹配內容.下面例子中找出所有以b開頭的標籤,這表示

標籤都應該被找到:

import re

for tag in soup.find_all(re.compile("^b")):

print(tag.name)

# body

# b

下面代碼找出所有名字中包含”t”的標籤:

for tag in soup.find_all(re.compile("t")):

print(tag.name)

# html

# title

列表

如果傳入列表參數,Beautiful Soup會將與列表中任一元素匹配的內容返回.下面代碼找到文檔中所有

soup.find_all(["a", "b"])

# [The Dormouse's story,

# ,

# ,

# ]

True

True 可以匹配任何值,下面代碼查找到所有的tag,但是不會返回字符串節點

for tag in soup.find_all(True):

print(tag.name)

# html

# head

# title

# body

# p

# b

# p

# a

# a

# a

# p

方法

如果沒有合適過濾器,那麼還可以定義一個方法,方法只接受一個元素參數 [4] ,如果這個方法返回 True 表示當前元素匹配並且被找到,如果不是則反回 False

下面方法校驗了當前元素,如果包含 class 屬性卻不包含 id 屬性,那麼將返回 True:

def has_class_but_no_id(tag):

return tag.has_attr('class') and not tag.has_attr('id')

將這個方法作為參數傳入 find_all() 方法,將得到所有

標籤:

soup.find_all(has_class_but_no_id)

# [

The Dormouse's story

,

#

Once upon a time there were...

,

#

...

]

返回結果中只有

標籤沒有

下面代碼找到所有被文字包含的節點內容:

from bs4 import NavigableString

def surrounded_by_strings(tag):

return (isinstance(tag.next_element, NavigableString)

and isinstance(tag.previous_element, NavigableString))

for tag in soup.find_all(surrounded_by_strings):

print tag.name

# p

# a

# a

# a

# p

現在來了解一下搜索方法的細節

find_all()

find_all( name , attrs , recursive , text , **kwargs )

find_all() 方法搜索當前tag的所有tag子節點,並判斷是否符合過濾器的條件.這裡有幾個例子:

soup.find_all("title")

# [

The Dormouse's story]

soup.find_all("p", "title")

# [

The Dormouse's story

]

soup.find_all("a")

# [ ,

# ,

# ]

soup.find_all(id="link2")

# [ ]

import re

soup.find(text=re.compile("sisters"))

# u'Once upon a time there were three little sisters; and their names were\n'

有幾個方法很相似,還有幾個方法是新的,參數中的 text 和 id 是什麼含義? 為什麼 find_all("p", "title") 返回的是CSS Class為”title”的

標籤? 我們來仔細看一下 find_all() 的參數

name 參數

name 參數可以查找所有名字為 name 的tag,字符串對象會被自動忽略掉.

簡單的用法如下:

soup.find_all("title")

# [

The Dormouse's story <div class="lazy-load-ad"></div>]

重申: 搜索 name 參數的值可以使任一類型的 過濾器 ,字符竄,正則表達式,列表,方法或是 True .

keyword 參數

如果一個指定名字的參數不是搜索內置的參數名,搜索時會把該參數當作指定名字tag的屬性來搜索,如果包含一個名字為 id 的參數,Beautiful Soup會搜索每個tag的”id”屬性.

soup.find_all(id='link2')

# [ ]

如果傳入 href 參數,Beautiful Soup會搜索每個tag的”href”屬性:

soup.find_all(href=re.compile("elsie"))

# [ ]

搜索指定名字的屬性時可以使用的參數值包括 字符串 , 正則表達式 , 列表, True .

下面的例子在文檔樹中查找所有包含 id 屬性的tag,無論 id 的值是什麼:

soup.find_all(id=True)

# [ ,

# ,

# ]

使用多個指定名字的參數可以同時過濾tag的多個屬性:

soup.find_all(href=re.compile("elsie"), id='link1')

# [ ]

有些tag屬性在搜索不能使用,比如HTML5中的 data-* 屬性:

data_soup = BeautifulSoup('

foo!
')

data_soup.find_all(data-foo="value")

# SyntaxError: keyword can't be an expression

但是可以通過 find_all() 方法的 attrs 參數定義一個字典參數來搜索包含特殊屬性的tag:

data_soup.find_all(attrs={"data-foo": "value"})

# [

foo!
]

按CSS搜索

按照CSS類名搜索tag的功能非常實用,但標識CSS類名的關鍵字 class 在Python中是保留字,使用 class 做參數會導致語法錯誤.從Beautiful Soup的4.1.1版本開始,可以通過 class_ 參數搜索有指定CSS類名的tag:

soup.find_all("a", class_="sister")

# [ ,

# ,

# ]

class_ 參數同樣接受不同類型的 過濾器 ,字符串,正則表達式,方法或 True :

soup.find_all(class_=re.compile("itl"))

# [

The Dormouse's story

]

def has_six_characters(css_class):

return css_class is not None and len(css_class) == 6

soup.find_all(class_=has_six_characters)

# [ ,

# ,

# ]

tag的 class 屬性是 多值屬性 .按照CSS類名搜索tag時,可以分別搜索tag中的每個CSS類名:

css_soup = BeautifulSoup('

')

css_soup.find_all("p", class_="strikeout")

# [

]

css_soup.find_all("p", class_="body")

# [

]

搜索 class 屬性時也可以通過CSS值完全匹配:

css_soup.find_all("p", class_="body strikeout")

# [

]

完全匹配 class 的值時,如果CSS類名的順序與實際不符,將搜索不到結果:

soup.find_all("a", attrs={"class": "sister"})

# [ ,

# ,

# ]

text 參數

通過 text 參數可以搜搜文檔中的字符串內容.與 name 參數的可選值一樣, text 參數接受 字符串 , 正則表達式 , 列表, True . 看例子:

soup.find_all(text="Elsie")

# [u'Elsie']

soup.find_all(text=["Tillie", "Elsie", "Lacie"])

# [u'Elsie', u'Lacie', u'Tillie']

soup.find_all(text=re.compile("Dormouse"))

[u"The Dormouse's story", u"The Dormouse's story"]

def is_the_only_string_within_a_tag(s):

""Return True if this string is the only child of its parent tag.""

return (s == s.parent.string)

soup.find_all(text=is_the_only_string_within_a_tag)

# [u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...']

雖然 text 參數用於搜索字符串,還可以與其它參數混合使用來過濾tag.Beautiful Soup會找到 .string 方法與 text 參數值相符的tag.下面代碼用來搜索內容裡面包含“Elsie”的

soup.find_all("a", text="Elsie")

# [ ]

limit 參數

find_all() 方法返回全部的搜索結構,如果文檔樹很大那麼搜索會很慢.如果我們不需要全部結果,可以使用 limit 參數限制返回結果的數量.效果與SQL中的limit關鍵字類似,當搜索到的結果數量達到 limit 的限制時,就停止搜索返回結果.

文檔樹中有3個tag符合搜索條件,但結果只返回了2個,因為我們限制了返回數量:

soup.find_all("a", limit=2)

# [ ,

# ]

recursive 參數

調用tag的 find_all() 方法時,Beautiful Soup會檢索當前tag的所有子孫節點,如果只想搜索tag的直接子節點,可以使用參數 recursive=False .

一段簡單的文檔:

<p> The Dormouse's story</p><p> </p>

...

是否使用 recursive 參數的搜索結果:

soup.html.find_all("title")

# [

The Dormouse's story]

soup.html.find_all("title", recursive=False)

# []

像調用 find_all() 一樣調用tag

find_all() 幾乎是Beautiful Soup中最常用的搜索方法,所以我們定義了它的簡寫方法. BeautifulSoup 對象和 tag 對象可以被當作一個方法來使用,這個方法的執行結果與調用這個對象的 find_all() 方法相同,下面兩行代碼是等價的:

soup.find_all("a")

soup("a")

這兩行代碼也是等價的:

soup.title.find_all(text=True)

soup.title(text=True)

find()

find( name , attrs , recursive , text , **kwargs )

find_all() 方法將返回文檔中符合條件的所有tag,儘管有時候我們只想得到一個結果.比如文檔中只有一個

標籤,那麼使用 find_all() 方法來查找標籤就不太合適, 使用 find_all 方法並設置 limit=1 參數不如直接使用 find() 方法.下面兩行代碼是等價的:

soup.find_all('title', limit=1)

# [

The Dormouse's story]

soup.find('title')

#

The Dormouse's story

唯一的區別是 find_all() 方法的返回結果是值包含一個元素的列表,而 find() 方法直接返回結果.

find_all() 方法沒有找到目標是返回空列表, find() 方法找不到目標時,返回 None .

print(soup.find("nosuchtag"))

# None

soup.head.title 是 tag的名字 方法的簡寫.這個簡寫的原理就是多次調用當前tag的 find() 方法:

soup.head.title

#

The Dormouse's story

soup.find("head").find("title")

#

The Dormouse's story

find_parents() 和 find_parent()

find_parents( name , attrs , recursive , text , **kwargs )

find_parent( name , attrs , recursive , text , **kwargs )

我們已經用了很大篇幅來介紹 find_all() 和 find() 方法,Beautiful Soup中還有10個用於搜索的API.它們中的五個用的是與 find_all() 相同的搜索參數,另外5個與 find() 方法的搜索參數類似.區別僅是它們搜索文檔的不同部分.

記住: find_all() 和 find() 只搜索當前節點的所有子節點,孫子節點等. find_parents() 和 find_parent() 用來搜索當前節點的父輩節點,搜索方法與普通tag的搜索方法相同,搜索文檔搜索文檔包含的內容. 我們從一個文檔中的一個葉子節點開始:

a_string = soup.find(text="Lacie")

a_string

# u'Lacie'

a_string.find_parents("a")

# [ ]

a_string.find_parent("p")

#

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

# ,

# and

# ;

# and they lived at the bottom of a well.

a_string.find_parents("p", class="title")

# []

文檔中的一個

find_previous_siblings() 和 find_previous_sibling()

find_previous_siblings( name , attrs , recursive , text , **kwargs )

find_previous_sibling( name , attrs , recursive , text , **kwargs )

這2個方法通過 .previous_siblings 屬性對當前tag的前面解析 [5] 的兄弟tag節點進行迭代, find_previous_siblings() 方法返回所有符合條件的前面的兄弟節點, find_previous_sibling() 方法返回第一個符合條件的前面的兄弟節點:

last_link = soup.find("a", id="link3")

last_link

#

last_link.find_previous_siblings("a")

# [ ,

# ]

first_story_paragraph = soup.find("p", "story")

first_story_paragraph.find_previous_sibling("p")

#

The Dormouse's story

find_all_next() 和 find_next()

find_all_next( name , attrs , recursive , text , **kwargs )

find_next( name , attrs , recursive , text , **kwargs )

這2個方法通過 .next_elements 屬性對當前tag的之後的 [5] tag和字符串進行迭代, find_all_next() 方法返回所有符合條件的節點, find_next() 方法返回第一個符合條件的節點:

first_link = soup.a

first_link

#

first_link.find_all_next(text=True)

# [u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',

# u';\nand they lived at the bottom of a well.', u'\n\n', u'...', u'\n']

first_link.find_next("p")

#

...

第一個例子中,字符串 “Elsie”也被顯示出來,儘管它被包含在我們開始查找的

#

The Dormouse's story

]

first_link.find_previous("title")

#

The Dormouse's story

find_all_previous("p") 返回了文檔中的第一段(class=”title”的那段),但還返回了第二段,

標籤包含了我們開始查找的

通過tag標籤逐層查找:

soup.select("body a")

# [ ,

# ,

# ]

soup.select("html head title")

# [

The Dormouse's story]

找到某個tag標籤下的直接子標籤 [6] :

soup.select("head > title")

# [

The Dormouse's story]

soup.select("p > a")

# [ ,

# ,

# ]

soup.select("p > a:nth-of-type(2)")

# [ ]

soup.select("p > #link1")

# [ ]

soup.select("body > a")

# []

找到兄弟節點標籤:

soup.select("#link1 ~ .sister")

# [ ,

# ]

soup.select("#link1 + .sister")

# [ ]

通過CSS的類名查找:

soup.select(".sister")

# [ ,

# ,

# ]

soup.select("[class~=sister]")

# [ ,

# ,

# ]

通過tag的id查找:

soup.select("#link1")

# [ ]

soup.select("a#link2")

# [ ]

通過是否存在某個屬性來查找:

soup.select('a[href]')

# [ ,

# ,

# ]

通過屬性的值來查找:

soup.select('a[href="http://example.com/elsie"]')

# [ ]

soup.select('a[href^="http://example.com/"]')

# [ ,

# ,

# ]

soup.select('a[href$="tillie"]')

# [ ]

soup.select('a[href*=".com/el"]')

# [ ]

通過語言設置來查找:

multilingual_markup = """

Hello

Howdy, y'all

Pip-pip, old fruit

Bonjour mes amis

"""

multilingual_soup = BeautifulSoup(multilingual_markup)

multilingual_soup.select('p[lang|=en]')

# [

Hello

,

#

Howdy, y'all

,

#

Pip-pip, old fruit

]

對於熟悉CSS選擇器語法的人來說這是個非常方便的方法.Beautiful Soup也支持CSS選擇器API,如果你僅僅需要CSS選擇器的功能,那麼直接使用 lxml 也可以,而且速度更快,支持更多的CSS選擇器語法,但Beautiful Soup整合了CSS選擇器的語法和自身方便使用API.

修改文檔樹

Beautiful Soup的強項是文檔樹的搜索,但同時也可以方便的修改文檔樹

修改tag的名稱和屬性

在 Attributes 的章節中已經介紹過這個功能,但是再看一遍也無妨. 重命名一個tag,改變屬性的值,添加或刪除屬性:

soup = BeautifulSoup('Extremely bold')

tag = soup.b

tag.name = "blockquote"

tag['class'] = 'verybold'

tag['id'] = 1

tag

#

Extremely bold

del tag['class']

del tag['id']

tag

#

Extremely bold

修改 .string

給tag的 .string 屬性賦值,就相當於用當前的內容替代了原來的內容:

markup = ' '

soup = BeautifulSoup(markup)

tag = soup.a

tag.string = "New link text."

tag

#

注意: 如果當前的tag包含了其它tag,那麼給它的 .string 屬性賦值會覆蓋掉原有的所有內容包括子tag

append()

Tag.append() 方法想tag中添加內容,就好像Python的列表的 .append() 方法:

soup = BeautifulSoup(" ")

soup.a.append("Bar")

soup

#

soup.a.contents

# [u'Foo', u'Bar']

BeautifulSoup.new_string() 和 .new_tag()

如果想添加一段文本內容到文檔中也沒問題,可以調用Python的 append() 方法或調用工廠方法 BeautifulSoup.new_string() :

soup = BeautifulSoup("")

tag = soup.b

tag.append("Hello")

new_string = soup.new_string(" there")

tag.append(new_string)

tag

# Hello there.

tag.contents

# [u'Hello', u' there']

如果想要創建一段註釋,或 NavigableString 的任何子類,將子類作為 new_string() 方法的第二個參數傳入:

from bs4 import Comment

new_comment = soup.new_string("Nice to see you.", Comment)

tag.append(new_comment)

tag

# Hello there

tag.contents

# [u'Hello', u' there', u'Nice to see you.']

# 這是Beautiful Soup 4.2.1 中新增的方法

創建一個tag最好的方法是調用工廠方法 BeautifulSoup.new_tag() :

soup = BeautifulSoup("")

original_tag = soup.b

new_tag = soup.new_tag("a", href="http://www.example.com")

original_tag.append(new_tag)

original_tag

#

new_tag.string = "Link text."

original_tag

#

第一個參數作為tag的name,是必填,其它參數選填

insert()

Tag.insert() 方法與 Tag.append() 方法類似,區別是不會把新元素添加到父節點 .contents 屬性的最後,而是把元素插入到指定的位置.與Python列表總的 .insert() 方法的用法下同:

markup = ' '

soup = BeautifulSoup(markup)

tag = soup.a

tag.insert(1, "but did not endorse ")

tag

#

tag.contents

# [u'I linked to ', u'but did not endorse', example.com

]

insert_before() 和 insert_after()

insert_before() 方法在當前tag或文本節點前插入內容:

soup = BeautifulSoup("stop")

tag = soup.new_tag("i")

tag.string = "Don't"

soup.b.string.insert_before(tag)

soup.b

# Don'tstop

insert_after() 方法在當前tag或文本節點後插入內容:

soup.b.i.insert_after(soup.new_string(" ever "))

soup.b

# Don't ever stop

soup.b.contents

# [Don't, u' ever ', u'stop']

clear()

Tag.clear() 方法移除當前tag的內容:

markup = ' '

soup = BeautifulSoup(markup)

tag = soup.a

tag.clear()

tag

#

extract()

PageElement.extract() 方法將當前tag移除文檔樹,並作為方法結果返回:

markup = ' '

soup = BeautifulSoup(markup)

a_tag = soup.a

i_tag = soup.i.extract()

a_tag

#

i_tag

# example.com

print(i_tag.parent)

None

這個方法實際上產生了2個文檔樹: 一個是用來解析原始文檔的 BeautifulSoup 對象,另一個是被移除並且返回的tag.被移除並返回的tag可以繼續調用 extract 方法:

my_string = i_tag.string.extract()

my_string

# u'example.com'

print(my_string.parent)

# None

i_tag

#

decompose()

Tag.decompose() 方法將當前節點移除文檔樹並完全銷燬:

markup = ' '

soup = BeautifulSoup(markup)

a_tag = soup.a

soup.i.decompose()

a_tag

#

replace_with()

PageElement.replace_with() 方法移除文檔樹中的某段內容,並用新tag或文本節點替代它:

markup = ' '

soup = BeautifulSoup(markup)

a_tag = soup.a

new_tag = soup.new_tag("b")

new_tag.string = "example.net"

a_tag.i.replace_with(new_tag)

a_tag

#

replace_with() 方法返回被替代的tag或文本節點,可以用來瀏覽或添加到文檔樹其它地方

wrap()

PageElement.wrap() 方法可以對指定的tag元素進行包裝 [8] ,並返回包裝後的結果:

soup = BeautifulSoup("

I wish I was bold.

")

soup.p.string.wrap(soup.new_tag("b"))

# I wish I was bold.

soup.p.wrap(soup.new_tag("div"))

#

I wish I was bold.

該方法在 Beautiful Soup 4.0.5 中添加

unwrap()

Tag.unwrap() 方法與 wrap() 方法相反.將移除tag內的所有tag標籤,該方法常被用來進行標記的解包:

markup = ' '

soup = BeautifulSoup(markup)

a_tag = soup.a

a_tag.i.unwrap()

a_tag

#

與 replace_with() 方法相同, unwrap() 方法返回被移除的tag

輸出

格式化輸出

prettify() 方法將Beautiful Soup的文檔樹格式化後以Unicode編碼輸出,每個XML/HTML標籤都獨佔一行

markup = ' '

soup = BeautifulSoup(markup)

soup.prettify()

# '\n

\n \n \n

#

#

BeautifulSoup 對象和它的tag節點都可以調用 prettify() 方法:

print(soup.a.prettify())

#

# I linked to

#

# example.com

#

#

壓縮輸出

如果只想得到結果字符串,不重視格式,那麼可以對一個 BeautifulSoup 對象或 Tag 對象使用Python的 unicode() 或 str() 方法:

str(soup)

# '

'

unicode(soup.a)

# u' '

str() 方法返回UTF-8編碼的字符串,可以指定 編碼 的設置.

還可以調用 encode() 方法獲得字節碼或調用 decode() 方法獲得Unicode.

輸出格式

Beautiful Soup輸出是會將HTML中的特殊字符轉換成Unicode,比如“&lquot;”:

soup = BeautifulSoup("“Dammit!” he said.")

unicode(soup)

# u'

\\u201cDammit!\\u201d he said.'

如果將文檔轉換成字符串,Unicode編碼會被編碼成UTF-8.這樣就無法正確顯示HTML特殊字符了:

str(soup)

# '

\\xe2\\x80\\x9cDammit!\\xe2\\x80\\x9d he said.'

get_text()

如果只想得到tag中包含的文本內容,那麼可以嗲用 get_text() 方法,這個方法獲取到tag中包含的所有文版內容包括子孫tag中的內容,並將結果作為Unicode字符串返回:

markup = ' '

soup = BeautifulSoup(markup)

soup.get_text()

u'\nI linked to example.com\n'

soup.i.get_text()

u'example.com'

可以通過參數指定tag的文本內容的分隔符:

# soup.get_text("|")

u'\nI linked to |example.com|\n'

還可以去除獲得文本內容的前後空白:

# soup.get_text("|", strip=True)

u'I linked to|example.com'

或者使用 .stripped_strings 生成器,獲得文本列表後手動處理列表:

[text for text

in soup.stripped_strings]

# [u'I linked to', u'example.com']

指定文檔解析器

如果僅是想要解析HTML文檔,只要用文檔創建 BeautifulSoup 對象就可以了.Beautiful Soup會自動選擇一個解析器來解析文檔.但是還可以通過參數指定使用那種解析器來解析當前文檔.

BeautifulSoup 第一個參數應該是要被解析的文檔字符串或是文件句柄,第二個參數用來標識怎樣解析文檔.如果第二個參數為空,那麼Beautiful Soup根據當前系統安裝的庫自動選擇解析器,解析器的優先數序: lxml, html5lib, Python標準庫.在下面兩種條件下解析器優先順序會變化:

  • 要解析的文檔是什麼類型: 目前支持, “html”, “xml”, 和 “html5”
  • 指定使用哪種解析器: 目前支持, “lxml”, “html5lib”, 和 “html.parser”

安裝解析器 章節介紹了可以使用哪種解析器,以及如何安裝.

如果指定的解析器沒有安裝,Beautiful Soup會自動選擇其它方案.目前只有 lxml 解析器支持XML文檔的解析,在沒有安裝lxml庫的情況下,創建 beautifulsoup 對象時無論是否指定使用lxml,都無法得到解析後的對象

解析器之間的區別

Beautiful Soup為不同的解析器提供了相同的接口,但解析器本身時有區別的.同一篇文檔被不同的解析器解析後可能會生成不同結構的樹型文檔.區別最大的是HTML解析器和XML解析器,看下面片段被解析成HTML結構:

BeautifulSoup(" ")

#

因為空標籤不符合HTML標準,所以解析器把它解析成

同樣的文檔使用XML解析如下(解析XML需要安裝lxml庫).注意,空標籤依然被保留,並且文檔前添加了XML頭,而不是被包含在標籤內:

BeautifulSoup(" ", "xml")

#

#

HTML解析器之間也有區別,如果被解析的HTML文檔是標準格式,那麼解析器之間沒有任何差別,只是解析速度不同,結果都會返回正確的文檔樹.

但是如果被解析文檔不是標準格式,那麼不同的解析器返回結果可能不同.下面例子中,使用lxml解析錯誤格式的文檔,結果

標籤被直接忽略掉了:

BeautifulSoup("

", "lxml")

#

使用html5lib庫解析相同文檔會得到不同的結果:

BeautifulSoup("

", "html5lib")

#

html5lib庫沒有忽略掉

標籤,而是自動補全了標籤,還給文檔樹添加了標籤.

使用pyhton內置庫解析結果如下:

BeautifulSoup("

", "html.parser")

#

與lxml [7] 庫類似的,Python內置庫忽略掉了

標籤,與html5lib庫不同的是標準庫沒有嘗試創建符合標準的文檔格式或將文檔片段包含在標籤內,與lxml不同的是標準庫甚至連標籤都沒有嘗試去添加.

因為文檔片段“

”是錯誤格式,所以以上解析方式都能算作”正確”,html5lib庫使用的是HTML5的部分標準,所以最接近”正確”.不過所有解析器的結構都能夠被認為是”正常”的.

不同的解析器可能影響代碼執行結果,如果在分發給別人的代碼中使用了 BeautifulSoup ,那麼最好註明使用了哪種解析器,以減少不必要的麻煩.

編碼

任何HTML或XML文檔都有自己的編碼方式,比如ASCII 或 UTF-8,但是使用Beautiful Soup解析後,文檔都被轉換成了Unicode:

markup = "

Sacr\\xc3\\xa9 bleu!

"

soup = BeautifulSoup(markup)

soup.h1

#

Sacré bleu!

soup.h1.string

# u'Sacr\\xe9 bleu!'

這不是魔術(但很神奇),Beautiful Soup用了 編碼自動檢測 子庫來識別當前文檔編碼並轉換成Unicode編碼. BeautifulSoup 對象的 .original_encoding 屬性記錄了自動識別編碼的結果:

soup.original_encoding

'utf-8'

編碼自動檢測 功能大部分時候都能猜對編碼格式,但有時候也會出錯.有時候即使猜測正確,也是在逐個字節的遍歷整個文檔後才猜對的,這樣很慢.如果預先知道文檔編碼,可以設置編碼參數來減少自動檢查編碼出錯的概率並且提高文檔解析速度.在創建 BeautifulSoup 對象的時候設置 from_encoding 參數.

這是我見過最牛逼,最全面的Beautiful Soup 4.2 教程!沒有之一


分享到:


相關文章: