天天看點

Beautiful Soup 4.2.0 文檔

Beautiful Soup 4.2.0 文檔

​​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代碼将作為例子被多次用到.這是 愛麗絲夢遊仙境的 的一段内容(以後内容中簡稱為 愛麗絲 的文檔):

1 html_doc = """
 2 <html><head><title>The Dormouse's story</title></head>
 3 <body>
 4 <p class="title"><b>The Dormouse's story</b></p>
 5 
 6 <p class="story">Once upon a time there were three little sisters; and their names were
 7 <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
 8 <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
 9 <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
10 and they lived at the bottom of a well.</p>
11 
12 <p class="story">...</p>
13 """      

使用BeautifulSoup解析這段代碼,能夠得到一個 BeautifulSoup

1 from bs4 import BeautifulSoup
 2 soup = BeautifulSoup(html_doc)
 3 
 4 print(soup.prettify())
 5 # <html>
 6 #  <head>
 7 #   <title>
 8 #    The Dormouse's story
 9 #   </title>
10 #  </head>
11 #  <body>
12 #   <p class="title">
13 #    <b>
14 #     The Dormouse's story
15 #    </b>
16 #   </p>
17 #   <p class="story">
18 #    Once upon a time there were three little sisters; and their names were
19 #    <a class="sister" href="http://example.com/elsie" id="link1">
20 #     Elsie
21 #    </a>
22 #    ,
23 #    <a class="sister" href="http://example.com/lacie" id="link2">
24 #     Lacie
25 #    </a>
26 #    and
27 #    <a class="sister" href="http://example.com/tillie" id="link2">
28 #     Tillie
29 #    </a>
30 #    ; and they lived at the bottom of a well.
31 #   </p>
32 #   <p class="story">
33 #    ...
34 #   </p>
35 #  </body>
36 # </html>      

幾個簡單的浏覽結構化資料的方法:

1 soup.title
 2 # <title>The Dormouse's story</title>
 3 
 4 soup.title.name
 5 # u'title'
 6 
 7 soup.title.string
 8 # u'The Dormouse's story'
 9 
10 soup.title.parent.name
11 # u'head'
12 
13 soup.p
14 # <p class="title"><b>The Dormouse's story</b></p>
15 
16 soup.p['class']
17 # u'title'
18 
19 soup.a
20 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
21 
22 soup.find_all('a')
23 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
24 #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
25 #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
26 
27 soup.find(id="link3")
28 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>      

從文檔中找到所有<a>标簽的連結:

for link in soup.find_all('a'):
    print(link.get('href'))
    # http://example.com/elsie
    # http://example.com/lacie
    # http://example.com/tillie      

從文檔中擷取所有文字内容:

1 print(soup.get_text())
 2 # The Dormouse's story
 3 #
 4 # The Dormouse's story
 5 #
 6 # Once upon a time there were three little sisters; and their names were
 7 # Elsie,
 8 # Lacie and
 9 # Tillie;
10 # and they lived at the bottom of a well.
11 #
12 # ...      

這是你想要的嗎?别着急,還有更好用的

安裝 Beautiful Soup

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

$ apt-get install Python-bs4      

Beautiful Soup 4 通過PyPi釋出,是以如果你無法使用系統包管理安裝,那麼也可以通過 easy_install 或 pip 來安裝.包的名字是 beautifulsoup4

$ 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

如果代碼抛出了 ImportError

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

如果在ROOT_TAG_NAME = u’[document]’代碼處遇到 SyntaxError

$ 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語言庫
html5lib BeautifulSoup(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("<html>data</html>")      

首先,文檔被轉換成Unicode,并且HTML的執行個體都被轉換成Unicode編碼

BeautifulSoup("Sacré bleu!")
<html><head></head><body>Sacré bleu!</body></html>      

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

對象的種類

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

Tag

Tag

soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b
type(tag)
# <class 'bs4.element.Tag'>      

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

Name

每個tag都有自己的名字,通過 .name

tag.name
# u'b'      

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

tag.name = "blockquote"
tag
# <blockquote class="boldest">Extremely bold</blockquote>      

Attributes

一個tag可能有很多個屬性. tag <b class="boldest">

tag['class']
# u'boldest'      

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

tag.attrs
# {u'class': u'boldest'}      

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

tag['class'] = 'verybold'
tag['id'] = 1
tag
# <blockquote class="verybold" id="1">Extremely bold</blockquote>

del tag['class']
del tag['id']
tag
# <blockquote>Extremely bold</blockquote>

tag['class']
# KeyError: 'class'
print(tag.get('class'))
# None      
多值屬性

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

css_soup = BeautifulSoup('<p class="body strikeout"></p>')
css_soup.p['class']
# ["body", "strikeout"]

css_soup = BeautifulSoup('<p class="body"></p>')
css_soup.p['class']
# ["body"]      

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

id_soup = BeautifulSoup('<p id="my id"></p>')
id_soup.p['id']
# 'my id'      

将tag轉換成字元串時,多值屬性會合并為一個值

rel_soup = BeautifulSoup('<p>Back to the <a rel="index">homepage</a></p>')
rel_soup.a['rel']
# ['index']
rel_soup.a['rel'] = ['index', 'contents']
print(rel_soup.p)
# <p>Back to the <a rel="index contents">homepage</a></p>      

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

xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml')
xml_soup.p['class']
# u'body strikeout'      

可以周遊的字元串

字元串常被包含在tag内.Beautiful Soup用 NavigableString

tag.string
# u'Extremely bold'
type(tag.string)
# <class 'bs4.element.NavigableString'>      

一個 NavigableString 字元串與Python中的Unicode字元串相同,并且還支援包含在 ​​周遊文檔樹​​​ 和 ​​搜尋文檔樹​​ 中的一些特性. 通過 unicode() 方法可以直接将 NavigableString

unicode_string = unicode(tag.string)
unicode_string
# u'Extremely bold'
type(unicode_string)
# <type 'unicode'>      

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

tag.string.replace_with("No longer bold")
tag
# <blockquote>No longer bold</blockquote>      

NavigableString 對象支援 ​​周遊文檔樹​​​ 和 ​​搜尋文檔樹​​ 中定義的大部分屬性, 并非全部.尤其是,一個字元串不能包含其它内容(tag能夠包含字元串或是其它tag),字元串不支援 .contents 或 .string 屬性或 find()

如果想在Beautiful Soup之外使用 NavigableString 對象,需要調用 unicode()

BeautifulSoup

BeautifulSoup 對象表示的是一個文檔的全部内容.大部分時候,可以把它當作 Tag 對象,它支援 ​​周遊文檔樹​​​ 和 ​​搜尋文檔樹​​ 中描述的大部分的方法.

因為 BeautifulSoup 對象并不是真正的HTML或XML的tag,是以它沒有name和attribute屬性.但有時檢視它的 .name 屬性是很友善的,是以 BeautifulSoup 對象包含了一個值為 “[document]” 的特殊屬性 .name

soup.name
# u'[document]'      

注釋及特殊字元串

Tag , NavigableString , BeautifulSoup

markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup)
comment = soup.b.string
type(comment)
# <class 'bs4.element.Comment'>      

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

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

但是當它出現在HTML文檔中時, Comment

print(soup.b.prettify())
# <b>
#  <!--Hey, buddy. Want to buy a used parser?-->
# </b>      

Beautiful Soup中定義的其它類型都可能會出現在XML的文檔中: CData , ProcessingInstruction , Declaration , Doctype .與 Comment 對象類似,這些類都是 NavigableString

from bs4 import CData
cdata = CData("A CDATA block")
comment.replace_with(cdata)

print(soup.b.prettify())
# <b>
#  <![CDATA[A CDATA block]]>
# </b>      

周遊文檔樹

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

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p class="title"><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_doc)      

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

子節點

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

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

tag的名字

操作文檔樹最簡單的方法就是告訴它你想擷取的tag的name.如果想擷取 <head> 标簽,隻要用 soup.head

soup.head
# <head><title>The Dormouse's story</title></head>

soup.title
# <title>The Dormouse's story</title>      

這是個擷取tag的小竅門,可以在文檔樹的tag中多次調用這個方法.下面的代碼可以擷取<body>标簽中的第一個<b>标簽:

soup.body.b
# <b>The Dormouse's story</b>      

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

soup.a
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>      

如果想要得到所有的<a>标簽,或是通過名字得到比一個tag更多的内容的時候,就需要用到 Searching the tree

soup.find_all('a')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]      

.contents 和 .children

tag的 .contents

head_tag = soup.head
head_tag
# <head><title>The Dormouse's story</title></head>

head_tag.contents
[<title>The Dormouse's story</title>]

title_tag = head_tag.contents[0]
title_tag
# <title>The Dormouse's story</title>
title_tag.contents
# [u'The Dormouse's story']      

BeautifulSoup 對象本身一定會包含子節點,也就是說<html>标簽也是 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

for child in title_tag.children:
    print(child)
    # The Dormouse's story      

.descendants

.contents 和 .children

head_tag.contents
# [<title>The Dormouse's story</title>]      

但是<title>标簽也包含一個子節點:字元串 “The Dormouse’s story”,這種情況下字元串 “The Dormouse’s story”也屬于<head>标簽的子孫節點. .descendants 屬性可以對所有tag的子孫節點進行遞歸循環 ​​[5]​​ :

for child in head_tag.descendants:
    print(child)
    # <title>The Dormouse's story</title>
    # The Dormouse's story      

上面的例子中, <head>标簽隻有一個子節點,但是有2個子孫節點:<head>節點和<head>的子節點, 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
# [<title>The Dormouse's story</title>]

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

title_tag = soup.title
title_tag
# <title>The Dormouse's story</title>
title_tag.parent
# <head><title>The Dormouse's story</title></head>      

文檔title的字元串也有父節點:<title>标簽

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

文檔的頂層節點比如<html>的父節點是 BeautifulSoup

html_tag = soup.html
type(html_tag.parent)
# <class 'bs4.BeautifulSoup'>      

BeautifulSoup 對象的 .parent

print(soup.parent)
# None      

.parents

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

link = soup.a
link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
for parent in link.parents:
    if parent is None:
        print(parent)
    else:
        print(parent.name)
# p
# body
# html
# [document]
# None      

兄弟節點

看一段簡單的例子:

sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>")
print(sibling_soup.prettify())
# <html>
#  <body>
#   <a>
#    <b>
#     text1
#    </b>
#    <c>
#     text2
#    </c>
#   </a>
#  </body>
# </html>      

因為<b>标簽和<c>标簽是同一層:他們是同一個元素的子節點,是以<b>和<c>可以被稱為兄弟節點.一段文檔以标準格式輸出時,兄弟節點有相同的縮進級别.在代碼中也可以使用這種關系.

.next_sibling 和 .previous_sibling

在文檔樹中,使用 .next_sibling 和 .previous_sibling

sibling_soup.b.next_sibling
# <c>text2</c>

sibling_soup.c.previous_sibling
# <b>text1</b>      

<b>标簽有 .next_sibling 屬性,但是沒有 .previous_sibling 屬性,因為<b>标簽在同級節點中是第一個.同理,<c>标簽有 .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

<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a>
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>      

如果以為第一個<a>标簽的 .next_sibling

link = soup.a
link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

link.next_sibling
# u',\n'      

第二個<a>标簽是頓号的 .next_sibling

link.next_sibling.next_sibling
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>      

.next_siblings 和 .previous_siblings

通過 .next_siblings 和 .previous_siblings

for sibling in soup.a.next_siblings:
    print(repr(sibling))
    # u',\n'
    # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
    # u' and\n'
    # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
    # 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'
    # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
    # u',\n'
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
    # u'Once upon a time there were three little sisters; and their names were\n'
    # None      

回退和前進

看一下“愛麗絲” 文檔:

<html><head><title>The Dormouse's story</title></head>
<p class="title"><b>The Dormouse's story</b></p>      

HTML解析器把這段字元串轉換成一連串的事件: “打開<html>标簽”,”打開一個<head>标簽”,”打開一個<title>标簽”,”添加一段字元串”,”關閉<title>标簽”,”打開<p>标簽”,等等.Beautiful Soup提供了重制解析器初始化過程的方法.

.next_element 和 .previous_element

.next_element 屬性指向解析過程中下一個被解析的對象(字元串或tag),結果可能與 .next_sibling

這是“愛麗絲”文檔中最後一個<a>标簽,它的 .next_sibling 結果是一個字元串,因為目前的解析過程 ​​[2]​​ 因為目前的解析過程因為遇到了<a>标簽而中斷了:

last_a_tag = soup.find("a", id="link3")
last_a_tag
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

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

但這個<a>标簽的 .next_element

last_a_tag.next_element
# u'Tillie'      

這是因為在原始文檔中,字元串“Tillie” 在分号前出現,解析器先進入<a>标簽,然後是字元串“Tillie”,然後關閉</a>标簽,然後是分号和剩餘部分.分号與<a>标簽在同一層級,但是字元串“Tillie”會被先解析.

.previous_element 屬性剛好與 .next_element

last_a_tag.previous_element
# u' and\n'
last_a_tag.previous_element.next_element
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>      

.next_elements 和 .previous_elements

通過 .next_elements 和 .previous_elements

for element in last_a_tag.next_elements:
    print(repr(element))
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'\n\n'
# <p class="story">...</p>
# u'...'
# u'\n'
# None      

搜尋文檔樹

Beautiful Soup定義了很多搜尋方法,這裡着重介紹2個: find() 和 find_all()

再以“愛麗絲”文檔作為例子:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p class="title"><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_doc)      

使用 find_all()

過濾器

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

字元串

最簡單的過濾器是字元串.在搜尋方法中傳入一個字元串參數,Beautiful Soup會查找與字元串完整比對的内容,下面的例子用于查找文檔中所有的<b>标簽:

soup.find_all('b')
# [<b>The Dormouse's story</b>]      

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

正規表達式

如果傳入正規表達式作為參數,Beautiful Soup會通過正規表達式的 match()

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會将與清單中任一進制素比對的内容傳回.下面代碼找到文檔中所有<a>标簽和<b>标簽:

soup.find_all(["a", "b"])
# [<b>The Dormouse's story</b>,
#  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]      

True

True

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)
# [<p class="title"><b>The Dormouse's story</b></p>,
#  <p class="story">Once upon a time there were...</p>,
#  <p class="story">...</p>]      

傳回結果中隻有<p>标簽沒有<a>标簽,因為<a>标簽還定義了”id”,沒有傳回<html>和<head>,因為<html>和<head>中沒有定義”class”屬性.

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

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()

soup.find_all("title")
# [<title>The Dormouse's story</title>]

soup.find_all("p", "title")
# [<p class="title"><b>The Dormouse's story</b></p>]

soup.find_all("a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.find_all(id="link2")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

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”的<p>标簽? 我們來仔細看一下 find_all()

name 參數

name 參數可以查找所有名字為 name

簡單的用法如下:

soup.find_all("title")
# [<title>The Dormouse's story</title>]      

重申: 搜尋 name 參數的值可以使任一類型的 ​​過濾器​​ ,字元竄,正規表達式,清單,方法或是 True

keyword 參數

如果一個指定名字的參數不是搜尋内置的參數名,搜尋時會把該參數當作指定名字tag的屬性來搜尋,如果包含一個名字為 id

soup.find_all(id='link2')
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]      

如果傳入 href

soup.find_all(href=re.compile("elsie"))
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]      

搜尋指定名字的屬性時可以使用的參數值包括 ​​字元串​​​ , ​​正規表達式​​​ , ​​清單​​​, ​​True​​ .

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

soup.find_all(id=True)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]      

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

soup.find_all(href=re.compile("elsie"), id='link1')
# [<a class="sister" href="http://example.com/elsie" id="link1">three</a>]      

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

data_soup = BeautifulSoup('<div data-foo="value">foo!</div>')
data_soup.find_all(data-foo="value")
# SyntaxError: keyword can't be an expression      

但是可以通過 find_all() 方法的 attrs

data_soup.find_all(attrs={"data-foo": "value"})
# [<div data-foo="value">foo!</div>]      

按CSS搜尋

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

soup.find_all("a", class_="sister")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]      

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

soup.find_all(class_=re.compile("itl"))
# [<p class="title"><b>The Dormouse's story</b></p>]

def has_six_characters(css_class):
    return css_class is not None and len(css_class) == 6

soup.find_all(class_=has_six_characters)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]      

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

css_soup = BeautifulSoup('<p class="body strikeout"></p>')
css_soup.find_all("p", class_="strikeout")
# [<p class="body strikeout"></p>]

css_soup.find_all("p", class_="body")
# [<p class="body strikeout"></p>]      

搜尋 class

css_soup.find_all("p", class_="body strikeout")
# [<p class="body strikeout"></p>]      

完全比對 class

soup.find_all("a", attrs={"class": "sister"})
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]      

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

soup.find_all("a", text="Elsie")
# [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]      

limit

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

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

soup.find_all("a", limit=2)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]      

recursive

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

一段簡單的文檔:

<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
...      

是否使用 recursive

soup.html.find_all("title")
# [<title>The Dormouse's story</title>]

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

像調用 find_all()

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,盡管有時候我們隻想得到一個結果.比如文檔中隻有一個<body>标簽,那麼使用 find_all() 方法來查找<body>标簽就不太合适, 使用 find_all 方法并設定 limit=1 參數不如直接使用 find()

soup.find_all('title', limit=1)
# [<title>The Dormouse's story</title>]

soup.find('title')
# <title>The Dormouse's story</title>      

唯一的差別是 find_all() 方法的傳回結果是值包含一個元素的清單,而 find()

find_all() 方法沒有找到目标是傳回空清單, find() 方法找不到目标時,傳回 None

print(soup.find("nosuchtag"))
# None      

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

soup.head.title
# <title>The Dormouse's story</title>

soup.find("head").find("title")
# <title>The Dormouse's story</title>      

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()

a_string = soup.find(text="Lacie")
a_string
# u'Lacie'

a_string.find_parents("a")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

a_string.find_parent("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>

a_string.find_parents("p", class="title")
# []      

文檔中的一個<a>标簽是是目前葉子節點的直接父節點,是以可以被找到.還有一個<p>标簽,是目标葉子節點的間接父輩節點,是以也可以被找到.包含class值為”title”的<p>标簽不是不是目标葉子節點的父輩節點,是以通過 find_parents()

find_parent() 和 find_parents() 方法會讓人聯想到 ​​.parent​​​ 和 ​​.parents​​ 屬性.它們之間的聯系非常緊密.搜尋父輩節點的方法實際上就是對 .parents

find_next_siblings() 合 find_next_sibling()

find_next_siblings( ​​name​​​ , ​​attrs​​​ , ​​recursive​​​ , ​​text​​​ , ​​**kwargs​​ )

find_next_sibling( ​​name​​​ , ​​attrs​​​ , ​​recursive​​​ , ​​text​​​ , ​​**kwargs​​ )

這2個方法通過 ​​.next_siblings​​​ 屬性對當tag的所有後面解析 ​​[5]​​ 的兄弟tag節點進行疊代, find_next_siblings() 方法傳回所有符合條件的後面的兄弟節點, find_next_sibling()

first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_next_siblings("a")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

first_story_paragraph = soup.find("p", "story")
first_story_paragraph.find_next_sibling("p")
# <p class="story">...</p>      

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
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

last_link.find_previous_siblings("a")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

first_story_paragraph = soup.find("p", "story")
first_story_paragraph.find_previous_sibling("p")
# <p class="title"><b>The Dormouse's story</b></p>      

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
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

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")
# <p class="story">...</p>      

第一個例子中,字元串 “Elsie”也被顯示出來,盡管它被包含在我們開始查找的<a>标簽的裡面.第二個例子中,最後一個<p>标簽也被顯示出來,盡管它與我們開始查找位置的<a>标簽不屬于同一部分.例子中,搜尋的重點是要比對過濾器的條件,并且在文檔中出現的順序而不是開始查找的元素的位置.

find_all_previous() 和 find_previous()

find_all_previous( ​​name​​​ , ​​attrs​​​ , ​​recursive​​​ , ​​text​​​ , ​​**kwargs​​ )

find_previous( ​​name​​​ , ​​attrs​​​ , ​​recursive​​​ , ​​text​​​ , ​​**kwargs​​ )

這2個方法通過 ​​.previous_elements​​​ 屬性對目前節點前面 ​​[5]​​ 的tag和字元串進行疊代, find_all_previous() 方法傳回所有符合條件的節點, find_previous()

first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_all_previous("p")
# [<p class="story">Once upon a time there were three little sisters; ...</p>,
#  <p class="title"><b>The Dormouse's story</b></p>]

first_link.find_previous("title")
# <title>The Dormouse's story</title>      

find_all_previous("p")

CSS選擇器

Beautiful Soup支援大部分的CSS選擇器 ​​[6]​​ ,在 Tag 或 BeautifulSoup 對象的 .select()

soup.select("title")
# [<title>The Dormouse's story</title>]

soup.select("p nth-of-type(3)")
# [<p class="story">...</p>]      

通過tag标簽逐層查找:

soup.select("body a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie"  id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("html head title")
# [<title>The Dormouse's story</title>]      

找到某個tag标簽下的直接子标簽 ​​[6]​​ :

soup.select("head > title")
# [<title>The Dormouse's story</title>]

soup.select("p > a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie"  id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("p > a:nth-of-type(2)")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

soup.select("p > #link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select("body > a")
# []      

找到兄弟節點标簽:

soup.select("#link1 ~ .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie"  id="link3">Tillie</a>]

soup.select("#link1 + .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]      

通過CSS的類名查找:

soup.select(".sister")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("[class~=sister]")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]      

通過tag的id查找:

soup.select("#link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select("a#link2")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]      

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

soup.select('a[href]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]      

通過屬性的值來查找:

soup.select('a[href="http://example.com/elsie"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select('a[href^="http://example.com/"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select('a[href$="tillie"]')
# [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select('a[href*=".com/el"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]      

通過語言設定來查找:

multilingual_markup = """
 <p lang="en">Hello</p>
 <p lang="en-us">Howdy, y'all</p>
 <p lang="en-gb">Pip-pip, old fruit</p>
 <p lang="fr">Bonjour mes amis</p>
"""
multilingual_soup = BeautifulSoup(multilingual_markup)
multilingual_soup.select('p[lang|=en]')
# [<p lang="en">Hello</p>,
#  <p lang="en-us">Howdy, y'all</p>,
#  <p lang="en-gb">Pip-pip, old fruit</p>]      

對于熟悉CSS選擇器文法的人來說這是個非常友善的方法.Beautiful Soup也支援CSS選擇器API,如果你僅僅需要CSS選擇器的功能,那麼直接使用 lxml

修改文檔樹

Beautiful Soup的強項是文檔樹的搜尋,但同時也可以友善的修改文檔樹

修改tag的名稱和屬性

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

soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b

tag.name = "blockquote"
tag['class'] = 'verybold'
tag['id'] = 1
tag
# <blockquote class="verybold" id="1">Extremely bold</blockquote>

del tag['class']
del tag['id']
tag
# <blockquote>Extremely bold</blockquote>      

修改 .string

給tag的 .string

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)

tag = soup.a
tag.string = "New link text."
tag
# <a href="http://example.com/">New link text.</a>      

注意: 如果目前的tag包含了其它tag,那麼給它的 .string

append()

Tag.append() 方法想tag中添加内容,就好像Python的清單的 .append()

soup = BeautifulSoup("<a>Foo</a>")
soup.a.append("Bar")

soup
# <html><head></head><body><a>FooBar</a></body></html>
soup.a.contents
# [u'Foo', u'Bar']      

BeautifulSoup.new_string() 和 .new_tag()

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

soup = BeautifulSoup("<b></b>")
tag = soup.b
tag.append("Hello")
new_string = soup.new_string(" there")
tag.append(new_string)
tag
# <b>Hello there.</b>
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
# <b>Hello there<!--Nice to see you.--></b>
tag.contents
# [u'Hello', u' there', u'Nice to see you.']      

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

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

soup = BeautifulSoup("<b></b>")
original_tag = soup.b

new_tag = soup.new_tag("a", href="http://www.example.com")
original_tag.append(new_tag)
original_tag
# <b><a href="http://www.example.com"></a></b>

new_tag.string = "Link text."
original_tag
# <b><a href="http://www.example.com">Link text.</a></b>      

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

insert()

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

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a

tag.insert(1, "but did not endorse ")
tag
# <a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
tag.contents
# [u'I linked to ', u'but did not endorse', <i>example.com</i>]      

insert_before() 和 insert_after()

insert_before()

soup = BeautifulSoup("<b>stop</b>")
tag = soup.new_tag("i")
tag.string = "Don't"
soup.b.string.insert_before(tag)
soup.b
# <b><i>Don't</i>stop</b>      

insert_after()

soup.b.i.insert_after(soup.new_string(" ever "))
soup.b
# <b><i>Don't</i> ever stop</b>
soup.b.contents
# [<i>Don't</i>, u' ever ', u'stop']      

clear()

Tag.clear()

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a

tag.clear()
tag
# <a href="http://example.com/"></a>      

extract()

PageElement.extract()

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

i_tag = soup.i.extract()

a_tag
# <a href="http://example.com/">I linked to</a>

i_tag
# <i>example.com</i>

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
# <i></i>      

decompose()

Tag.decompose()

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

soup.i.decompose()

a_tag
# <a href="http://example.com/">I linked to</a>      

replace_with()

PageElement.replace_with()

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
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
# <a href="http://example.com/">I linked to <b>example.net</b></a>      

replace_with()

wrap()

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

soup = BeautifulSoup("<p>I wish I was bold.</p>")
soup.p.string.wrap(soup.new_tag("b"))
# <b>I wish I was bold.</b>

soup.p.wrap(soup.new_tag("div"))
# <div><p><b>I wish I was bold.</b></p></div>      

該方法在 Beautiful Soup 4.0.5 中添加

unwrap()

Tag.unwrap() 方法與 wrap()

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

a_tag.i.unwrap()
a_tag
# <a href="http://example.com/">I linked to example.com</a>      

與 replace_with() 方法相同, unwrap()

輸出

格式化輸出

prettify()

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
soup.prettify()
# '<html>\n <head>\n </head>\n <body>\n  <a href="http://example.com/">\n...'

print(soup.prettify())
# <html>
#  <head>
#  </head>
#  <body>
#   <a href="http://example.com/">
#    I linked to
#    <i>
#     example.com
#    </i>
#   </a>
#  </body>
# </html>      

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

print(soup.a.prettify())
# <a href="http://example.com/">
#  I linked to
#  <i>
#   example.com
#  </i>
# </a>      

壓縮輸出

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

str(soup)
# '<html><head></head><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'

unicode(soup.a)
# u'<a href="http://example.com/">I linked to <i>example.com</i></a>'      

str() 方法傳回UTF-8編碼的字元串,可以指定 ​​編碼​​ 的設定.

還可以調用 encode() 方法獲得位元組碼或調用 decode()

輸出格式

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

soup = BeautifulSoup("“Dammit!” he said.")
unicode(soup)
# u'<html><head></head><body>\u201cDammit!\u201d he said.</body></html>'      

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

str(soup)
# '<html><head></head><body>\xe2\x80\x9cDammit!\xe2\x80\x9d he said.</body></html>'      

get_text()

如果隻想得到tag中包含的文本内容,那麼可以嗲用 get_text()

markup = '<a href="http://example.com/">\nI linked to <i>example.com</i>\n</a>'
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

BeautifulSoup

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

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

如果指定的解析器沒有安裝,Beautiful Soup會自動選擇其它方案.目前隻有 lxml 解析器支援XML文檔的解析,在沒有安裝lxml庫的情況下,建立 beautifulsoup

解析器之間的差別

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

BeautifulSoup("<a><b /></a>")
# <html><head></head><body><a><b></b></a></body></html>      

因為空标簽<b />不符合HTML标準,是以解析器把它解析成<b></b>

同樣的文檔使用XML解析如下(解析XML需要安裝lxml庫).注意,空标簽<b />依然被保留,并且文檔前添加了XML頭,而不是被包含在<html>标簽内:

BeautifulSoup("<a><b /></a>", "xml")
# <?xml version="1.0" encoding="utf-8"?>
# <a><b/></a>      

HTML解析器之間也有差別,如果被解析的HTML文檔是标準格式,那麼解析器之間沒有任何差别,隻是解析速度不同,結果都會傳回正确的文檔樹.

但是如果被解析文檔不是标準格式,那麼不同的解析器傳回結果可能不同.下面例子中,使用lxml解析錯誤格式的文檔,結果</p>标簽被直接忽略掉了:

BeautifulSoup("<a></p>", "lxml")
# <html><body><a></a></body></html>      

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

BeautifulSoup("<a></p>", "html5lib")
# <html><head></head><body><a><p></p></a></body></html>      

html5lib庫沒有忽略掉</p>标簽,而是自動補全了标簽,還給文檔樹添加了<head>标簽.

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

BeautifulSoup("<a></p>", "html.parser")
# <a></a>      

與lxml ​​[7]​​ 庫類似的,Python内置庫忽略掉了</p>标簽,與html5lib庫不同的是标準庫沒有嘗試建立符合标準的文檔格式或将文檔片段包含在<body>标簽内,與lxml不同的是标準庫甚至連<html>标簽都沒有嘗試去添加.

因為文檔片段“<a></p>”是錯誤格式,是以以上解析方式都能算作”正确”,html5lib庫使用的是HTML5的部分标準,是以最接近”正确”.不過所有解析器的結構都能夠被認為是”正常”的.

不同的解析器可能影響代碼執行結果,如果在分發給别人的代碼中使用了 BeautifulSoup

編碼

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

markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
soup = BeautifulSoup(markup)
soup.h1
# <h1>Sacré bleu!</h1>
soup.h1.string
# u'Sacr\xe9 bleu!'      

這不是魔術(但很神奇),Beautiful Soup用了 ​​編碼自動檢測​​ 子庫來識别目前文檔編碼并轉換成Unicode編碼. BeautifulSoup 對象的 .original_encoding

soup.original_encoding
'utf-8'      

​​編碼自動檢測​​ 功能大部分時候都能猜對編碼格式,但有時候也會出錯.有時候即使猜測正确,也是在逐個位元組的周遊整個文檔後才猜對的,這樣很慢.如果預先知道文檔編碼,可以設定編碼參數來減少自動檢查編碼出錯的機率并且提高文檔解析速度.在建立 BeautifulSoup 對象的時候設定 from_encoding

下面一段文檔用了ISO-8859-8編碼方式,這段文檔太短,結果Beautiful Soup以為文檔是用ISO-8859-7編碼:

markup = b"<h1>\xed\xe5\xec\xf9</h1>"
soup = BeautifulSoup(markup)
soup.h1
<h1>νεμω</h1>
soup.original_encoding
'ISO-8859-7'      

通過傳入 from_encoding

soup = BeautifulSoup(markup, from_encoding="iso-8859-8")
soup.h1
<h1>םולש</h1>
soup.original_encoding
'iso8859-8'      

少數情況下(通常是UTF-8編碼的文檔中包含了其它編碼格式的檔案),想獲得正确的Unicode編碼就不得不将文檔中少數特殊編碼字元替換成特殊Unicode編碼,“REPLACEMENT CHARACTER” (U+FFFD, �) ​​[9]​​ . 如果Beautifu Soup猜測文檔編碼時作了特殊字元的替換,那麼Beautiful Soup會把 UnicodeDammit 或 BeautifulSoup 對象的 .contains_replacement_characters 屬性标記為 True .這樣就可以知道目前文檔進行Unicode編碼後丢失了一部分特殊内容字元.如果文檔中包含�而 .contains_replacement_characters 屬性是 False

輸出編碼

通過Beautiful Soup輸出文檔時,不管輸入文檔是什麼編碼方式,輸出編碼均為UTF-8編碼,下面例子輸入文檔是Latin-1編碼:

markup = b'''
<html>
  <head>
    <meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type" />
  </head>
  <body>
    <p>Sacr\xe9 bleu!</p>
  </body>
</html>
'''

soup = BeautifulSoup(markup)
print(soup.prettify())
# <html>
#  <head>
#   <meta content="text/html; charset=utf-8" http-equiv="Content-type" />
#  </head>
#  <body>
#   <p>
#    Sacré bleu!
#   </p>
#  </body>
# </html>      

注意,輸出文檔中的<meta>标簽的編碼設定已經修改成了與輸出編碼一緻的UTF-8.

如果不想用UTF-8編碼輸出,可以将編碼方式傳入 prettify()

print(soup.prettify("latin-1"))
# <html>
#  <head>
#   <meta content="text/html; charset=latin-1" http-equiv="Content-type" />
# ...      

還可以調用 BeautifulSoup 對象或任意節點的 encode() 方法,就像Python的字元串調用 encode()

soup.p.encode("latin-1")
# '<p>Sacr\xe9 bleu!</p>'

soup.p.encode("utf-8")
# '<p>Sacr\xc3\xa9 bleu!</p>'      

如果文檔中包含目前編碼不支援的字元,那麼這些字元将呗轉換成一系列XML特殊字元引用,下面例子中包含了Unicode編碼字元SNOWMAN:

markup = u"<b>\N{SNOWMAN}</b>"
snowman_soup = BeautifulSoup(markup)
tag = snowman_soup.b      

SNOWMAN字元在UTF-8編碼中可以正常顯示(看上去像是☃),但有些編碼不支援SNOWMAN字元,比如ISO-Latin-1或ASCII,那麼在這些編碼中SNOWMAN字元會被轉換成“&#9731”:

print(tag.encode("utf-8"))
# <b>☃</b>

print tag.encode("latin-1")
# <b>☃</b>

print tag.encode("ascii")
# <b>☃</b>      

Unicode, dammit! (靠!)

​​編碼自動檢測​​ 功能可以在Beautiful Soup以外使用,檢測某段未知編碼時,可以使用這個方法:

from bs4 import UnicodeDammit
dammit = UnicodeDammit("Sacr\xc3\xa9 bleu!")
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'utf-8'      

如果Python中安裝了 chardet 或 cchardet

dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'latin-1'      

​​編碼自動檢測​​ 功能中有2項功能是Beautiful Soup庫中用不到的

智能引号

使用Unicode時,Beautiful Soup還會智能的把引号 ​​[10]​​ 轉換成HTML或XML中的特殊字元:

markup = b"<p>I just \x93love\x94 Microsoft Word\x92s smart quotes</p>"

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="html").unicode_markup
# u'<p>I just “love” Microsoft Word’s smart quotes</p>'

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
# u'<p>I just “love” Microsoft Word’s smart quotes</p>'      

也可以把引号轉換為ASCII碼:

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
# u'<p>I just "love" Microsoft Word\'s smart quotes</p>'      

很有用的功能,但是Beautiful Soup沒有使用這種方式.預設情況下,Beautiful Soup把引号轉換成Unicode:

UnicodeDammit(markup, ["windows-1252"]).unicode_markup
# u'<p>I just \u201clove\u201d Microsoft Word\u2019s smart quotes</p>'      

沖突的編碼

有時文檔的大部分都是用UTF-8,但同時還包含了Windows-1252編碼的字元,就像微軟的智能引号 ​​[10]​​ 一樣.一些包含多個資訊的來源網站容易出現這種情況. UnicodeDammit.detwingle()

snowmen = (u"\N{SNOWMAN}" * 3)
quote = (u"\N{LEFT DOUBLE QUOTATION MARK}I like snowmen!\N{RIGHT DOUBLE QUOTATION MARK}")
doc = snowmen.encode("utf8") + quote.encode("windows_1252")      

這段文檔很雜亂,snowmen是UTF-8編碼,引号是Windows-1252編碼,直接輸出時不能同時顯示snowmen和引号,因為它們編碼不同:

print(doc)
# ☃☃☃�I like snowmen!�

print(doc.decode("windows-1252"))
# ☃☃☃“I like snowmen!”      

如果對這段文檔用UTF-8解碼就會得到 UnicodeDecodeError 異常,如果用Windows-1252解碼就回得到一堆亂碼.幸好, UnicodeDammit.detwingle()

new_doc = UnicodeDammit.detwingle(doc)
print(new_doc.decode("utf8"))
# ☃☃☃“I like snowmen!”      

UnicodeDammit.detwingle()

在建立 BeautifulSoup 或 UnicodeDammit 對象前一定要先對文檔調用 UnicodeDammit.detwingle()

UnicodeDammit.detwingle()

解析部分文檔

如果僅僅因為想要查找文檔中的<a>标簽而将整片文檔進行解析,實在是浪費記憶體和時間.最快的方法是從一開始就把<a>标簽以外的東西都忽略掉. SoupStrainer 類可以定義文檔的某段内容,這樣搜尋文檔時就不必先解析整篇文檔,隻會解析在 SoupStrainer 中定義過的文檔. 建立一個 SoupStrainer 對象并作為 parse_only 參數給 BeautifulSoup

SoupStrainer

SoupStrainer 類接受與典型搜尋方法相同的參數:​​name​​​ , ​​attrs​​​ , ​​recursive​​​ , ​​text​​​ , ​​**kwargs​​ 。下面舉例說明三種 SoupStrainer

from bs4 import SoupStrainer

only_a_tags = SoupStrainer("a")

only_tags_with_id_link2 = SoupStrainer(id="link2")

def is_short_string(string):
    return len(string) < 10

only_short_strings = SoupStrainer(text=is_short_string)      

再拿“愛麗絲”文檔來舉例,來看看使用三種 SoupStrainer

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p class="title"><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>
"""

print(BeautifulSoup(html_doc, "html.parser", parse_only=only_a_tags).prettify())
# <a class="sister" href="http://example.com/elsie" id="link1">
#  Elsie
# </a>
# <a class="sister" href="http://example.com/lacie" id="link2">
#  Lacie
# </a>
# <a class="sister" href="http://example.com/tillie" id="link3">
#  Tillie
# </a>

print(BeautifulSoup(html_doc, "html.parser", parse_only=only_tags_with_id_link2).prettify())
# <a class="sister" href="http://example.com/lacie" id="link2">
#  Lacie
# </a>

print(BeautifulSoup(html_doc, "html.parser", parse_only=only_short_strings).prettify())
# Elsie
# ,
# Lacie
# and
# Tillie
# ...
#      

還可以将 SoupStrainer 作為參數傳入 ​​搜尋文檔樹​​ 中提到的方法.這可能不是個常用用法,是以還是提一下:

soup = BeautifulSoup(html_doc)
soup.find_all(only_short_strings)
# [u'\n\n', u'\n\n', u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
#  u'\n\n', u'...', u'\n']      

常見問題

代碼診斷

如果想知道Beautiful Soup到底怎樣處理一份文檔,可以将文檔傳入 diagnose()

from bs4.diagnose import diagnose
data = open("bad.html").read()
diagnose(data)

# Diagnostic running on Beautiful Soup 4.2.0
# Python version 2.7.3 (default, Aug  1 2012, 05:16:07)
# I noticed that html5lib is not installed. Installing it may help.
# Found lxml version 2.3.2.0
#
# Trying to parse your data with html.parser
# Here's what html.parser did with the document:
# ...      

diagnose()

文檔解析錯誤

文檔解析錯誤有兩種.一種是崩潰,Beautiful Soup嘗試解析一段文檔結果卻抛除了異常,通常是 HTMLParser.HTMLParseError

這些錯誤幾乎都不是Beautiful Soup的原因,這不會是因為Beautiful Soup得代碼寫的太優秀,而是因為Beautiful Soup沒有包含任何文檔解析代碼.異常産生自被依賴的解析器,如果解析器不能很好的解析出目前的文檔,那麼最好的辦法是換一個解析器.更多細節檢視 ​​安裝解析器​​ 章節.

最常見的解析錯誤是 HTMLParser.HTMLParseError: malformed start tag 和 HTMLParser.HTMLParseError: bad end tag .這都是由Python内置的解析器引起的,解決方法是 ​​安裝lxml或html5lib​​

最常見的異常現象是目前文檔找不到指定的Tag,而這個Tag光是用眼睛就足夠發現的了. find_all() 方法傳回 [] ,而 find() 方法傳回 None .這是Python内置解析器的又一個問題: 解析器會跳過那些它不知道的tag.解決方法還是 ​​安裝lxml或html5lib​​

版本錯誤

  • SyntaxError: Invalid syntax (異常位置在代碼行: ROOT_TAG_NAME = u'[document]'
  • ImportError: No module named HTMLParser
  • ImportError: No module named html.parser
  • ImportError: No module named BeautifulSoup 因為在沒有安裝BeautifulSoup3庫的Python環境下執行代碼,或忘記了BeautifulSoup4的代碼需要從 bs4
  • ImportError: No module named bs4

解析成XML

預設情況下,Beautiful Soup會将目前文檔作為HTML格式解析,如果要解析XML文檔,要在 BeautifulSoup

soup = BeautifulSoup(markup, "xml")      

解析器的錯誤

  • 如果同樣的代碼在不同環境下結果不同,可能是因為兩個環境下使用不同的解析器造成的.例如這個環境中安裝了lxml,而另一個環境中隻有html5lib,​​解析器之間的差別​​ 中說明了原因.修複方法是在 BeautifulSoup
  • 因為HTML标簽是​​大小寫敏感​​​ 的,是以3種解析器再出來文檔時都将tag和屬性轉換成小寫.例如文檔中的 <TAG></TAG> 會被轉換為 <tag></tag> .如果想要保留tag的大寫的話,那麼應該将文檔​​解析成XML​​ .

雜項錯誤

  • UnicodeEncodeError: 'charmap' codec can't encode character u'\xfoo' in position bar (或其它類型的 UnicodeEncodeError )的錯誤,主要是兩方面的錯誤(都不是Beautiful Soup的原因),第一種是正在使用的終端(console)無法顯示部分Unicode,參考​​Python wiki​​ ,第二種是向檔案寫入時,被寫入檔案不支援部分Unicode,這時隻要用 u.encode("utf8")
  • KeyError: [attr] 因為調用 tag['attr'] 方法而引起,因為這個tag沒有定義該屬性.出錯最多的是 KeyError: 'href' 和 KeyError: 'class' .如果不确定某個屬性是否存在時,用 tag.get('attr')
  • AttributeError: 'ResultSet' object has no attribute 'foo' 錯誤通常是因為把 find_all() 的傳回結果當作一個tag或文本節點使用,實際上傳回結果是一個清單或 ResultSet 對象的字元串,需要對結果進行循環才能得到每個節點的 .foo 屬性.或者使用 find()
  • AttributeError: 'NoneType' object has no attribute 'foo' 這個錯誤通常是在調用了 find() 方法後直節點取某個屬性 .foo 但是 find() 方法并沒有找到任何結果,是以它的傳回值是 None .需要找出為什麼 find() 的傳回值是 None