Beautiful Soup 文档

“鱼仆人从胳膊下掏出一封大信,几乎和他本人一样大。”

Beautiful Soup是一个用于从 HTML 和 XML 文件中提取数据的 Python 库。它可以与您常用的解析器配合使用,提供惯用的方式导航、搜索和修改解析树。它通常可以为程序员节省数小时甚至数天的工作时间。

这些说明通过示例阐述了 Beautiful Soup 4 的所有主要功能。我会向您展示这个库的用途、工作原理、使用方法、如何让它执行您想要的操作,以及当它不符合您的预期时该如何处理。

本文档涵盖 Beautiful Soup 4.13.3 版本。本文档中的示例基于 Python 3.8 编写。

您可能正在寻找Beautiful Soup 3的文档。如果是这样,您应该知道 Beautiful Soup 3 已停止开发,并且所有对它的支持已于 2020 年 12 月 31 日停止。如果您想了解 Beautiful Soup 3 和 Beautiful Soup 4 之间的区别,请参阅将代码移植到 BS4

本文档已被 Beautiful Soup 用户翻译成其他语言:

获取帮助

如果您对 Beautiful Soup 有任何疑问,或者遇到问题,请发送邮件至讨论组。如果您的问题涉及解析 HTML 文档,请务必提及诊断 () 函数对该文档的解析结果

当报告本文档中的错误时,请提及您正在阅读的翻译。

API 文档

本文档的编写方式类似于使用手册,但您也可以阅读由 Beautiful Soup 源代码生成的传统 API 文档。如果您想了解 Beautiful Soup 的内部结构,或本文档未涵盖的功能,请尝试 API 文档。

快速入门

下面是一个 HTML 文档,我将在本文档中使用它作为示例。它是《爱丽丝梦游仙境》故事的一部分:

html_doc = """<html><head><title>The Dormouse's story</title></head>
<body>
<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>
"""

通过 Beautiful Soup 运行“三姐妹”文档会给我们一个 BeautifulSoup 对象,该对象将文档表示为嵌套数据结构:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc, 'html.parser')

print(soup.prettify())
# <html>
#  <head>
#   <title>
#    The Dormouse's story
#   </title>
#  </head>
#  <body>
#   <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 class="sister" href="http://example.com/elsie" id="link1">
#     Elsie
#    </a>
#    ,
#    <a class="sister" href="http://example.com/lacie" id="link2">
#     Lacie
#    </a>
#    and
#    <a class="sister" href="http://example.com/tillie" id="link3">
#     Tillie
#    </a>
#    ; and they lived at the bottom of a well.
#   </p>
#   <p class="story">
#    ...
#   </p>
#  </body>
# </html>

以下是一些浏览该数据结构的简单方法:

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

soup.title.name
# u'title'

soup.title.string
# u'The Dormouse's story'

soup.title.parent.name
# u'head'

soup.p
# <p class="title"><b>The Dormouse's story</b></p>

soup.p['class']
# u'title'

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

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

一个常见的任务是提取页面 <a> 标签内的所有 URL:

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

如果您使用的是最新版本的 Debian 或 Ubuntu Linux,则可以使用系统包管理器安装 Beautiful Soup:

$ apt-get install python3-bs4

Beautiful Soup 4 通过 PyPi 发布,因此如果您无法使用系统软件包安装,可以使用 easy_install 或 pip 安装。软件包名称为 beautifulsoup4。请确保使用与您的 Python 版本匹配的 pip 或 easy_install 版本(它们可能分别名为 pip3 和 easy_install3)。

$ easy_install beautifulsoup4

$ pip install beautifulsoup4

(BeautifulSoup 包不是您想要的。那是上一个主要版本, Beautiful Soup 3。许多软件使用 BS3,因此它仍然可用,但如果您正在编写新代码,则应该安装 beautifulsoup4。)

如果您没有安装 easy_install 或 pip,您可以下载 Beautiful Soup 4 源 tarball并使用 setup.py 安装它。

$ python setup.py install

如果其他方法都失败了,Beautiful Soup 的许可证允许您将整个库与您的应用程序打包在一起。您可以下载 tarball,将其 bs4 目录复制到应用程序的代码库中,然后无需安装即可使用 Beautiful Soup。

我使用 Python 3.10 开发 Beautiful Soup,但它应该可以与其他较新版本兼容。

安装解析器

Beautiful Soup 支持 Python 标准库中的 HTML 解析器,但也支持许多第三方 Python 解析器。其中一个是lxml 解析器。根据您的设置,您可以使用以下命令之一安装 lxml:

$ apt-get install python-lxml

$ easy_install lxml

$ pip install lxml

另一个选择是纯 Python 的html5lib 解析器,它像 Web 浏览器一样解析 HTML。根据你的设置,你可以使用以下命令之一安装 html5lib:

$ apt-get install python3-html5lib

$ pip install html5lib

下表总结了每个解析器库的优点和缺点:

解析器 典型用法 优势 缺点
Python 的 html.parser BeautifulSoup(标记,“html.parser”) 包含电池,速度不错 不如 lxml 快,不如 html5lib 宽松。
lxml的HTML解析器 BeautifulSoup(标记,“lxml”) 非常快 外部 C 依赖项
lxml 的 XML 解析器 BeautifulSoup(标记,“lxml-xml”)BeautifulSoup(标记,“xml”) 非常快目前唯一支持的 XML 解析器 外部 C 依赖项
html5lib BeautifulSoup(标记,“html5lib”) 极其宽松以与 Web 浏览器相同的方式解析页面创建有效的 HTML5 非常慢 外部 Python 依赖

如果可以,我建议您安装并使用 lxml 以提高速度。

请注意,如果文档无效,不同的解析器将为其生成不同的 Beautiful Soup 树。详情请参阅解析器之间的差异

如何使用

要解析文档,请将其传递给 BeautifulSoup 构造函数。您可以传入一个字符串或一个打开的文件句柄:

from bs4 import BeautifulSoup

with open("index.html") as fp:
    soup = BeautifulSoup(fp, 'html.parser')

soup = BeautifulSoup("<html>a web page</html>", 'html.parser')

首先,将文档转换为 Unicode,并将 HTML 实体转换为 Unicode 字符:

print(BeautifulSoup("<html><head></head><body>Sacr&eacute; bleu!</body></html>", "html.parser"))
# <html><head></head><body>Sacré bleu!</body></html>

然后,Beautiful Soup 会使用最佳可用的解析器来解析文档。除非你明确指定使用 XML 解析器,否则它将使用 HTML 解析器。(请参阅解析 XML 。)

对象种类

Beautiful Soup 可以将复杂的 HTML 文档转换为复杂的 Python 对象树。但你只需要处理大约四种对象: TagNavigableString 、BeautifulSoup 和Comment 。这些对象代表了构成页面的 HTML 元素。

class Tag

Tag对象对应于原始文档中的 XML 或 HTML 标签。

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

标签有很多属性和方法,我将在“浏览树”“搜索树”中介绍其中的大部分内容。目前,标签最重要的方法是访问其名称和属性。

name

每个标签都有一个名称:

tag.name
# 'b'

如果您更改标签的名称,则更改将反映在 Beautiful Soup 生成的任何标记中:

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

HTML 或 XML 标签可以包含任意数量的属性。标签 <b id="boldest"> 有一个属性“id”,其值为“boldest”。你可以像字典一样访问标签的属性:

tag = BeautifulSoup('<b id="boldest">bold</b>', 'html.parser').b
tag['id']
# 'boldest'

您可以直接以 .attrs 的形式访问属性字典:

tag.attrs
# {'id': 'boldest'}
tag.attrs.keys()
# dict_keys(['id'])

您可以添加、删除和修改标签的属性。同样,这可以通过将标签视为字典来实现:

tag['id'] = 'verybold'
tag['another-attribute'] = 1
tag
# <b another-attribute="1" id="verybold"></b>

del tag['id']
del tag['another-attribute']
tag
# <b>bold</b>

tag['id']
# KeyError: 'id'
tag.get('id')
# None

多值属性

HTML 4 定义了一些可以有多个值的属性。HTML 5 删除了其中几个,但又定义了一些。最常见的多值属性是 class(也就是说,一个标签可以有多个 CSS 类)。其他属性包括 rel、rev、accept-charset、headers 和 accesskey。默认情况下,Beautiful Soup 将多值属性的值存储为列表:

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

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

当您将标签重新转换为字符串时,任何多值属性的值都会被合并:

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

如果某个属性看起来有多个值,但它不是任何版本的 HTML 标准定义的多值属性,则 Beautiful Soup 会将其存储为一个简单的字符串:

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

您可以通过将 multi_valued_attributes=None 作为关键字参数传递到 BeautifulSoup 构造函数中,强制所有属性存储为字符串:

no_list_soup = BeautifulSoup('<p class="body strikeout"></p>', 'html.parser', multi_valued_attributes=None)
no_list_soup.p['class']
# 'body strikeout'

您可以使用 get_attribute_list 始终返回列表容器中的值,无论它是字符串还是多值属性值:

id_soup.p['id']
# 'my id'
id_soup.p.get_attribute_list('id')
# ["my id"]

如果将文档解析为 XML,则不存在多值属性:

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

同样,您可以使用 multi_valued_attributes 参数进行配置:

class_is_multi= { '*' : 'class'}
xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml', multi_valued_attributes=class_is_multi)
xml_soup.p['class']
# ['body', 'strikeout']

您可能不需要这样做,但如果需要,请使用默认值作为指导。它们实现了 HTML 规范中描述的规则:

from bs4.builder import builder_registry
builder_registry.lookup('html').DEFAULT_CDATA_LIST_ATTRIBUTES

标签可以包含字符串形式的文本。Beautiful Soup 使用NavigableString类来包含以下文本:

soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
tag = soup.b
tag.string
# 'Extremely bold'
type(tag.string)
# <class 'bs4.element.NavigableString'>

NavigableString与 Python Unicode 字符串类似,但它还支持导航树搜索树中描述的一些功能。您可以使用 str 将NavigableString转换为 Unicode 字符串:

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

您不能就地编辑字符串,但可以使用replace_with()将一个字符串替换为另一个字符串:

tag.string.replace_with("No longer bold")
tag
# <b class="boldest">No longer bold</b>

NavigableString支持“导航树”“搜索树”中描述的大部分功能,但并非全部。具体来说,由于字符串不能包含任何内容(而标签可以包含一个字符串或另一个标签),因此字符串不支持 .contents 或 .string 属性,也不支持 find() 方法。

如果您想在 Beautiful Soup 之外使用NavigableString ,您应该对其调用 unicode() 将其转换为普通的 Python Unicode 字符串。如果不这样做,即使您已经使用完 Beautiful Soup,您的字符串仍会携带对整个 Beautiful Soup 解析树的引用。这会浪费大量内存。


BeautifulSoup 对象代表了已解析文档的整体。在大多数情况下,你可以将其视为Tag对象。这意味着它支持“浏览树”“搜索树”中描述的大多数方法。

你也可以将 BeautifulSoup 对象传递给修改树中定义的方法之一,就像传递Tag一样。这可以让你执行诸如合并两个已解析文档之类的操作:

doc = BeautifulSoup("<document><content/>INSERT FOOTER HERE</document", "xml")
footer = BeautifulSoup("<footer>Here's the footer</footer>", "xml")
doc.find(text="INSERT FOOTER HERE").replace_with(footer)
# 'INSERT FOOTER HERE'
print(doc)
# <?xml version="1.0" encoding="utf-8"?>
# <document><content/><footer>Here's the footer</footer></document>

由于 BeautifulSoup 对象并不对应实际的 HTML 或 XML 标签,因此它没有名称,也没有属性。但有时引用它的 .name 会很有用(例如,在编写同时使用Tag和 BeautifulSoup 对象的代码时),因此它被赋予了特殊的 .name “[document]”:

soup.name
# '[document]'

特殊字符串

TagNavigableString和 BeautifulSoup 几乎涵盖了你在 HTML 或 XML 文件中看到的所有内容,但还有一些剩余的部分。你最可能遇到的是Comment

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

Comment对象只是NavigableString的一种特殊类型:

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

但当它作为 HTML 文档的一部分出现时,注释会以特殊格式显示:

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

对于 HTML 文档

Beautiful Soup 定义了一些NavigableString子类,用于包含特定 HTML 标签内的字符串。这样,通过忽略页面中可能代表编程指令的字符串,可以更轻松地识别页面主体。(这些类是 Beautiful Soup 4.9.0 中新增的,html5lib 解析器不使用它们。)

class Stylesheet

NavigableString子类代表嵌入式 CSS 样式表;即在文档解析期间在 <style> 标签内找到的任何字符串。

class Script

NavigableString子类代表嵌入式 Javascript;即在文档解析期间在 <script> 标签内找到的任何字符串。

class Template

NavigableString子类代表嵌入式 HTML 模板;即在文档解析期间在 <template> 标签内找到的任何字符串。

对于 XML 文档

Beautiful Soup 定义了一些NavigableString类,用于保存 XML 文档中的特殊类型的字符串。与Comment类似,这些类是NavigableString的子类,用于在输出时为字符串添加一些额外的内容。

class Declaration

NavigableString子类表示 XML 文档开头的声明

class Doctype

NavigableString子类表示文档类型声明,可以在 XML 文档的开头附近找到。

class CData

代表CData 部分的NavigableString子类。

class ProcessingInstruction

表示XML 处理指令内容的NavigableString子类。

搜索树

Beautiful Soup 定义了很多用于搜索解析树的方法,但它们都非常相似。我将花大量时间解释两个最常用的方法:find() 和 find_all()。其他方法的参数几乎完全相同,因此我只会简要介绍一下。

再次,我将以“三姐妹”文档为例:

html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<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, 'html.parser')

通过将过滤器传递给 find_all() 之类的方法,您可以放大您感兴趣的文档部分。

过滤器种类

在详细讨论 find_all() 及其类似方法之前,我想先展示一些可以传入这些方法的不同过滤器的示例。这些过滤器会在整个搜索 API 中反复出现。您可以使用它们根据标签名称、标签属性、字符串文本或这些内容的组合进行过滤。

字符串

最简单的过滤器是字符串。将字符串传递给搜索方法,Beautiful Soup 就会根据该字符串执行标签名匹配。以下代码查找文档中所有的 <b> 标签:

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

如果传入的是字节字符串,Beautiful Soup 会默认该字符串采用 UTF-8 编码。你可以传入 Unicode 字符串来避免这种情况。

正则表达式

如果你传入一个正则表达式对象,Beautiful Soup 会使用其 search() 方法根据该正则表达式进行过滤。这段代码会查找所有以字母“b”开头的标签;在本例中,就是 <body> 标签和 <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

真的

值 True 匹配所有能匹配的标签。以下代码会查找文档中的所有标签,但不会查找任何文本字符串:

for tag in soup.find_all(True):
    print(tag.name)
# html
# head
# title
# body
# p
# b
# p
# a
# a
# a
# p

一个函数

如果其他匹配都不适用,请定义一个函数,该函数以元素作为唯一参数。如果参数匹配,该函数应返回 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() 中,您将获取所有 <p> 标签:

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…bottom of a well.</p>,
#  <p class="story">...</p>]

此函数仅拾取 <p> 标签。它不会拾取 <a> 标签,因为这些标签同时定义了“class”和“id”。它也不会拾取 <html> 和 <title> 之类的标签,因为这些标签没有定义“class”。

这个函数可以根据你的需要而变得非常复杂。下面这个函数如果标签被字符串对象包围,则返回 True:

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)
# body
# p
# a
# a
# a
# p

列表

如果你传入一个列表,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>]

现在我们准备详细了解搜索方法。

find_all()

方法签名: find_all( name , attrs , recursive , string , limit , **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(string=re.compile("sisters"))
# 'Once upon a time there were three little sisters; and their names were\n'

其中一些看起来应该很熟悉,但其他一些则是新的。传递字符串或 id 的值是什么意思?为什么 find_all("p", "title") 会查找带有 CSS 类“title”的 <p> 标签?让我们看看 find_all() 的参数。

名称参数

传入一个 name 值,即可让 Beautiful Soup 只考虑特定名称的标签。文本字符串将被忽略,名称不匹配的标签也将被忽略。

这是最简单的用法:

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

回想一下过滤器的种类,要命名的值可以是字符串正则表达式列表函数值 True

关键字参数

任何无法识别的关键字参数都将转换为根据其属性匹配标签的过滤器。

如果您传入一个名为 id 的参数的值,Beautiful Soup 将根据每个标签的“id”属性值进行过滤:

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

与标签一样,您可以根据字符串正则表达式列表函数值 True过滤属性。

如果你传入 href 的正则表达式对象,Beautiful Soup 将对每个标签的“href”属性值进行模式匹配:

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

值 True 匹配所有定义该属性的标签。以下代码查找所有具有 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>]

对于更复杂的匹配,可以定义一个函数,该函数以属性值作为其唯一参数。如果值匹配,则函数返回 True,否则返回 False。

这是一个查找所有 href 属性与正则表达式不匹配的标签的函数:

import re
def not_lacie(href):
    return href and not re.compile("lacie").search(href)

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

如果你传入一个列表作为参数,Beautiful Soup 会根据该列表中的任意字符串、正则表达式或函数查找属性值匹配项。以下代码会查找第一个和最后一个链接:

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

您可以通过传递多个关键字参数来同时过滤多个属性:

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

某些属性(例如 HTML 5 中的 data-* 属性)的名称不能用作关键字参数的名称:

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

您可以在搜索中使用这些属性,方法是将它们放入字典并将字典作为 attrs 参数传递到 find_all() 中:

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

类似地,你不能使用关键字参数来搜索 HTML 的 name 属性,因为 Beautiful Soup 使用 name 参数来包含标签本身的名称。不过,你可以在 attrs 参数中为 name 属性赋值:

name_soup = BeautifulSoup('<input name="email"/>', 'html.parser')
name_soup.find_all(name="email")
# []
name_soup.find_all(attrs={"name": "email"})
# [<input name="email"/>]

按 CSS 类搜索

搜索具有特定 CSS 类的标签非常有用,但 CSS 属性的名称“class”在 Python 中是保留字。使用 class 作为关键字参数会导致语法错误。从 Beautiful Soup 4.1.2 开始,你可以使用关键字参数 class_ 按 CSS 类进行搜索:

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>]

请记住,单个标签的“class”属性可以有多个值。当您搜索与某个 CSS 类匹配的标签时,实际上是在匹配其任意 CSS 类:

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

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

您还可以搜索类属性的精确字符串值:

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

但搜索字符串值的变体将不起作用:

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

在旧版本的 Beautiful Soup 中,没有 class_ 快捷键,你可以使用上面提到的 attrs 参数技巧。创建一个字典,其中“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>]

要一次搜索与两个或多个 CSS 类匹配的标签,请使用此处描述的Tag.select() CSS 选择器方法:

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

字符串参数

使用字符串参数,您可以搜索字符串而不是标签。与名称和属性关键字参数一样,您可以传入字符串正则表达式函数列表值 True 。以下是一些示例:

soup.find_all(string="Elsie")
# ['Elsie']

soup.find_all(string=["Tillie", "Elsie", "Lacie"])
# ['Elsie', 'Lacie', 'Tillie']

soup.find_all(string=re.compile("Dormouse"))
# ["The Dormouse's story", "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(string=is_the_only_string_within_a_tag)
# ["The Dormouse's story", "The Dormouse's story", 'Elsie', 'Lacie', 'Tillie', '...']

如果你在标签搜索中使用字符串参数,Beautiful Soup 会查找所有 .string 值与你指定的字符串匹配的标签。以下代码查找 .string 为“Elsie”的 <a> 标签:

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

string 参数是 Beautiful Soup 4.4.0 中的新增参数。在早期版本中,它被称为 text:

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

极限论证

find_all() 返回所有符合筛选条件的标签和字符串。如果文档很大,这可能需要一些时间。如果您不需要所有结果,可以传入一个数字作为限制。它的工作原理类似于 SQL 中的 LIMIT 关键字。它告诉 Beautiful Soup 在找到一定数量的结果后停止收集。

“三姐妹”文档中有三个链接,但此代码仅找到前两个:

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>]

递归论证

默认情况下,mytag.find_all() 会检查 mytag 的所有后代:其子代、子代的子代等等。如果只考虑直系子代,可以传入 recursive=False。区别如下:

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

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

以下是该文件的一部分:

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

<title> 标签位于 <html> 标签下方,但它并非直接位于 <html> 标签下方:<head> 标签挡住了它的路。当 Beautiful Soup 允许查看 <html> 标签的所有子标签时,它会找到 <title> 标签;但当 recursive=False 将其限制在 <html> 标签的直接子标签范围内时,它什么也找不到。

Beautiful Soup 提供了许多树形搜索方法(下文会介绍),它们大多采用与 find_all() 相同的参数:name、attrs、string、limit 和 attribute 关键字参数。但 recursive 参数是 find_all() 和 find() 方法所特有的。将 recursive=False 传递给 find_parents() 之类的方法并没有什么用。

调用标签就像调用 find_all()

为了方便起见,将 BeautifulSoup 对象或Tag对象作为函数调用,相当于调用 find_all() (如果没有内置方法包含您要查找的标签名称)。以下两行代码是等效的:

soup.find_all("a")
soup("a")

这两行也是等效的:

soup.title.find_all(string=True)
soup.title(string=True)

寻找()

方法签名: find( name , attrs , recursive , string , **kwargs )

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 技巧吗?这个技巧通过反复调用 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 , string , limit , **kwargs )

方法签名: find_parent( name , attrs , string , **kwargs )

上面我花了很多时间介绍 find_all() 和 find()。Beautiful Soup API 定义了另外十种用于搜索树的方法,但不必担心。其中五个方法与 find_all() 基本相同,另外五个方法也与 find() 基本相同。唯一的区别在于它们如何从树的一个部分移动到另一部分。

首先,我们来考虑 find_parents() 和 find_parent()。记住,find_all() 和 find() 会沿着树向下查找标签的后代。这两个方法则相反:它们会沿着树向上查找标签(或字符串)的父级。让我们尝试一下,从“three daughters”文档深处的一个字符串开始:

a_string = soup.find(string="Lacie")
a_string
# '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> 标签中有一个是该字符串的间接父级(祖先),我们的搜索也找到了它。文档中某个地方有一个带有 CSS 类“title”的 <p> 标签,但它不是该字符串的父级之一,因此我们无法使用 find_parents() 找到它。

你可能已经注意到 find_parent() 和 find_parents() 以及前面提到的.parent.parents属性之间的相似之处。这些搜索方法实际上使用 .parents 属性遍历所有父级(未经过滤),并根据提供的过滤器检查每个父级是否匹配。

find_next_siblings() 和 find_next_sibling()

方法签名: find_next_siblings( name , attrs , string , limit , **kwargs )

方法签名: find_next_sibling( name , attrs , string , **kwargs )

这些方法使用.next_siblings来遍历元素树中剩余的兄弟元素。 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 , string , limit , **kwargs )

方法签名: find_previous_sibling( name , attrs , string , **kwargs )

这些方法使用.previous_siblings来遍历元素在树中位于其之前的兄弟元素。 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 , string , limit , **kwargs )

方法签名: find_next( name , attrs , string , **kwargs )

这些方法使用.next_elements来迭代文档中其后的所有标签和字符串。 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(string=True)
# ['Elsie', ',\n', 'Lacie', ' and\n', 'Tillie',
#  ';\nand they lived at the bottom of a well.', '\n', '...', '\n']

first_link.find_next("p")
# <p class="story">...</p>

在第一个示例中,字符串“Elsie”出现了,即使它包含在我们开始的 <a> 标签中。在第二个示例中,文档中的最后一个 <p> 标签出现了,即使它与我们开始的 <a> 标签不在树的同一位置。对于这些方法,重要的是元素与过滤器匹配,并且它按文档顺序显示在文档的后面。

find_all_previous() 和 find_previous()

方法签名: find_all_previous( name , attrs , string , limit , **kwargs )

方法签名: find_previous( name , attrs , string , **kwargs )

这些方法使用.previous_elements来迭代文档中它之前的标签和字符串。 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") 调用找到了文档中的第一个段落(class=”title”),但它也找到了第二个段落,即包含我们开始使用的 <a> 标签的 <p> 标签。这并不奇怪:我们查找的是所有按文档顺序排列、比开始使用的标签更早出现的标签。包含 <a> 标签的 <p> 标签必然出现在它所包含的 <a> 标签之前。

通过 .css 属性的 CSS 选择器

BeautifulSoup 和Tag对象通过其 .css 属性支持 CSS 选择器。实际的选择器实现由Soup Sieve包处理,该包在 PyPI 上以 soupsieve 的形式提供。如果您通过 pip 安装了 Beautiful Soup,Soup Sieve 也会同时安装,因此您无需执行任何额外操作。

Soup Sieve 文档列出了所有当前支持的 CSS 选择器,以下仅列出一些基本选择器。您可以按名称查找标签:

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

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

通过 ID 查找标签:

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

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

查找包含在其他标签内任意位置的标签:

soup.css.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.css.select("html head title")
# [<title>The Dormouse's story</title>]

直接在其他标签中查找标签:

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

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

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

查找所有匹配的标签的下一个兄弟:

soup.css.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.css.select("#link1 + .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

按 CSS 类查找标签:

soup.css.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.css.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>]

从选择器列表中查找与任何选择器匹配的标签:

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

测试属性是否存在:

soup.css.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.css.select('a[href="http://example.com/elsie"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.css.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.css.select('a[href$="tillie"]')
# [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

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

还有一个名为 select_one() 的方法,它只查找与选择器匹配的第一个标签:

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

为了方便起见,您可以直接在 BeautifulSoup 或Tag对象上调用 select() 和 select_one(),省略 .css 属性:

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

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

CSS 选择器支持为已经了解 CSS 选择器语法的用户提供了便利。您可以使用 Beautiful Soup API 完成所有这些操作。如果您只需要 CSS 选择器,则应该完全跳过 Beautiful Soup,直接使用 lxml 解析文档:lxml 的速度更快。但 Soup Sieve 允许您将 CSS 选择器与 Beautiful Soup API 结合使用。

高级汤筛功能

除了 select() 和 select_one() 方法之外,Soup Sieve 还提供了丰富的 API,您可以通过Tag或 BeautifulSoup 的 .css 属性访问其中的大部分 API。以下仅列出了部分受支持的方法;完整文档请参阅Soup Sieve 文档

iselect() 方法的作用与 select() 相同,但它返回一个生成器而不是列表:

[tag['id'] for tag in soup.css.iselect(".sister")]
# ['link1', 'link2', 'link3']

closest() 方法返回与 CSS 选择器匹配的给定标签的最近父级,类似于 Beautiful Soup 的 find_parent() 方法:

elsie = soup.css.select_one(".sister")
elsie.css.closest("p.story")
# <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>

match() 方法根据特定标签是否与选择器匹配返回布尔值:

# elsie.css.match("#link1")
True

# elsie.css.match("#link2")
False

filter() 方法返回与选择器匹配的标签的直接子集:

[tag.string for tag in soup.find('p', 'story').css.filter('a')]
# ['Elsie', 'Lacie', 'Tillie']

escape() 方法可以转义 CSS 标识符,否则这些标识符将会无效:

soup.css.escape("1-strange-identifier")
# '\\31 -strange-identifier'

CSS 选择器中的命名空间

如果您已经解析了定义命名空间的 XML,则可以在 CSS 选择器中使用它们:

from bs4 import BeautifulSoup
xml = """<tag xmlns:ns1="http://namespace1/" xmlns:ns2="http://namespace2/">
 <ns1:child>I'm in namespace 1</ns1:child>
 <ns2:child>I'm in namespace 2</ns2:child>
</tag> """
namespace_soup = BeautifulSoup(xml, "xml")

namespace_soup.css.select("child")
# [<ns1:child>I'm in namespace 1</ns1:child>, <ns2:child>I'm in namespace 2</ns2:child>]

namespace_soup.css.select("ns1|child")
# [<ns1:child>I'm in namespace 1</ns1:child>]

Beautiful Soup 尝试使用根据解析文档时看到的内容有意义的命名空间前缀,但您始终可以提供自己的缩写词典:

namespaces = dict(first="http://namespace1/", second="http://namespace2/")
namespace_soup.css.select("second|child", namespaces=namespaces)
# [<ns1:child>I'm in namespace 2</ns1:child>]

CSS 选择器支持的历史

.css 属性是在 Beautiful Soup 4.12.0 版本中添加的。在此之前,仅支持 .select() 和 .select_one() 便捷方法。

Soup Sieve 集成已于 Beautiful Soup 4.7.0 版本中推出。早期版本虽然有 .select() 方法,但仅支持最常用的 CSS 选择器。

修改树

Beautiful Soup 的主要优势在于搜索解析树,但您也可以修改树并将更改写为新的 HTML 或 XML 文档。

更改标签名称和属性

我之前在Tag.attrs中介绍过这一点,但值得重复一遍。您可以重命名标签、更改其属性值、添加新属性以及删除属性:

soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
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

如果将标签的 .string 属性设置为新字符串,则标签的内容将被替换为该字符串:

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

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

注意:如果标签包含其他标签,则它们及其所有内容都将被破坏。

附加()

你可以使用 Tag.append() 向标签添加内容。其工作原理类似于在 Python 列表中调用 .append():

soup = BeautifulSoup("<a>Foo</a>", 'html.parser')
new_string = soup.a.append("Bar")

soup
# <a>FooBar</a>
soup.a.contents
# ['Foo', 'Bar']
new_string
# 'Bar'

Tag.append() 返回新附加的元素。

延长()

从 Beautiful Soup 4.7.0 开始, Tag还支持一种名为 .extend() 的方法,该方法将列表的每个元素按顺序添加到Tag中:

soup = BeautifulSoup("<a>Soup</a>", 'html.parser')
soup.a.extend(["'s", " ", "on"])

soup
# <a>Soup's on</a>
soup.a.contents
# ['Soup', ''s', ' ', 'on']

Tag.extend() 返回附加元素的列表。

插入()

Tag.insert() 与 Tag.append() 类似,不同之处在于新元素不一定位于其父级 .contents 的末尾。它会被插入到您指定的数字位置,类似于 Python 列表中的 .insert():

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

new_string = 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
# ['I linked to ', 'but did not endorse ', <i>example.com</i>]
new_string
# 'but did not endorse '

您可以将多个元素传递给 Tag.insert()。所有元素都将从您提供的数字位置开始插入。

Tag.insert() 返回新插入元素的列表。

insert_before() 和 insert_after()

insert_before() 方法在解析树中的其他内容之前立即插入标签或字符串:

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

insert_after() 方法在解析树中的其他内容之后立即插入标签或字符串:

div = soup.new_tag('div')
div.string = 'ever'
soup.b.i.insert_after(" you ", div)
soup.b
# <b><i>Don't</i> you <div>ever</div> leave</b>
soup.b.contents
# [<i>Don't</i>, ' you', <div>ever</div>, 'leave']

两种方法都返回新插入元素的列表。

清除()

Tag.clear() 删除标签的内容:

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

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

提炼()

PageElement.extract() 从树中移除一个标签或字符串。它返回提取出来的标签或字符串:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup, 'html.parser')
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

此时,你实际上已经有了两棵解析树:一棵以你用于解析文档的 BeautifulSoup 对象为根,另一棵以提取的标签为根。你可以继续对提取出的元素的子元素调用 extract() 方法:

my_string = i_tag.string.extract()
my_string
# 'example.com'

print(my_string.parent)
# None
i_tag
# <i></i>

分解()

Tag.decompose() 从树中删除一个标签,然后彻底销毁它及其内容:

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

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

已分解的TagNavigableString的行为尚未定义,您不应将其用于任何用途。如果您不确定某些内容是否已分解,可以检查其 .decomposed 属性(Beautiful Soup 4.9.0 中的新功能):

i_tag.decomposed
# True

a_tag.decomposed
# False

替换()

PageElement.replace_with() 从树中提取标签或字符串,然后用您选择的一个或多个标签或字符串替换它:

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

new_tag = soup.new_tag("b")
new_tag.string = "example.com"
a_tag.i.replace_with(new_tag)

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

bold_tag = soup.new_tag("b")
bold_tag.string = "example"
i_tag = soup.new_tag("i")
i_tag.string = "net"
a_tag.b.replace_with(bold_tag, ".", i_tag)

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

replace_with() 返回被替换的标签或字符串,以便您可以检查它或将其添加回树的另一部分。

将多个参数传递到 replace_with() 的功能是 Beautiful Soup 4.10.0 中的新功能。

裹()

PageElement.wrap() 将元素包装到您指定的Tag对象中。它返回新的包装器:

soup = BeautifulSoup("<p>I wish I was bold.</p>", 'html.parser')
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 中的新功能。

解包()

Tag.unwrap() 与 wrap() 相反。它会用标签内部的内容替换标签。它适合用于剥离标记:

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

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

与 replace_with() 类似,unwrap() 返回被替换的标签。

光滑的()

在调用一系列修改解析树的方法后,你可能会得到两个或多个相邻的NavigableString对象。Beautiful Soup 对此没有任何问题,但由于这种情况不会发生在新解析的文档中,因此你可能不会期望出现以下行为:

soup = BeautifulSoup("<p>A one</p>", 'html.parser')
soup.p.append(", a two")

soup.p.contents
# ['A one', ', a two']

print(soup.p.encode())
# b'<p>A one, a two</p>'

print(soup.p.prettify())
# <p>
#  A one
#  , a two
# </p>

您可以调用 Tag.smooth() 通过合并相邻的字符串来清理解析树:

soup.smooth()

soup.p.contents
# ['A one, a two']

print(soup.p.prettify())
# <p>
#  A one, a two
# </p>

此方法是 Beautiful Soup 4.8.0 中的新功能。

输出

美观打印

prettify() 方法将 Beautiful Soup 解析树转换为格式良好的 Unicode 字符串,每个标签和每个字符串都有单独的行:

markup = '<html><head><body><a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup, 'html.parser')
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>

由于 prettify() 会添加空格(以换行符的形式),因此它会改变 HTML 文档的含义,因此不应用于重新格式化文档。prettify() 的目标是帮助您直观地理解所处理文档的结构。

打印效果不佳

如果您只是想要一个字符串,而不需要特殊的格式,您可以在 BeautifulSoup 对象上或其中的Tag上调用 str() :

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

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

str() 函数返回以 UTF-8 编码的字符串。其他选项请参阅编码

您还可以调用 encode() 来获取字节串,调用 decoder() 来获取 Unicode。

输出格式化程序

如果你给 Beautiful Soup 一个包含 HTML 实体(如“&lquot;”)的文档,它们将被转换为 Unicode 字符:

soup = BeautifulSoup("&ldquo;Dammit!&rdquo; he said.", 'html.parser')
str(soup)
# '“Dammit!” he said.'

如果随后将文档转换为字节字符串,Unicode 字符将被编码为 UTF-8。您将无法恢复 HTML 实体:

soup.encode("utf8")
# b'\xe2\x80\x9cDammit!\xe2\x80\x9d he said.'

默认情况下,输出时唯一需要转义的字符是裸 & 符号和尖括号。它们会被转义为 “&amp;”、“&lt;” 和 “&gt;”,这样 Beautiful Soup 就不会无意中生成无效的 HTML 或 XML:

soup = BeautifulSoup("<p>The law firm of Dewey, Cheatem, & Howe</p>", 'html.parser')
soup.p
# <p>The law firm of Dewey, Cheatem, &amp; Howe</p>

soup = BeautifulSoup('<a href="http://example.com/?foo=val1&bar=val2">A link</a>', 'html.parser')
soup.a
# <a href="http://example.com/?foo=val1&amp;bar=val2">A link</a>

您可以通过为 prettify()、encode() 或 decoder() 的 formatter 参数指定值来更改此行为。Beautiful Soup 识别 formatter 的五个可能值。

默认值为 formatter="minimal"。字符串只会被处理到足以确保 Beautiful Soup 生成有效的 HTML/XML 的程度:

french = "<p>Il a dit &lt;&lt;Sacr&eacute; bleu!&gt;&gt;</p>"
soup = BeautifulSoup(french, 'html.parser')
print(soup.prettify(formatter="minimal"))
# <p>
#  Il a dit &lt;&lt;Sacré bleu!&gt;&gt;
# </p>

如果你传入 formatter="html",Beautiful Soup 会尽可能将 Unicode 字符转换为 HTML 实体:

print(soup.prettify(formatter="html"))
# <p>
#  Il a dit &lt;&lt;Sacr&eacute; bleu!&gt;&gt;
# </p>

如果传入 formatter="html5",则与 formatter="html" 类似,但 Beautiful Soup 会省略 HTML 空标签(如“br”)中的结束斜杠:

br = BeautifulSoup("<br>", 'html.parser').br

print(br.encode(formatter="html"))
# b'<br/>'

print(br.encode(formatter="html5"))
# b'<br>'

此外,任何值为空字符串的属性都将成为 HTML 样式的布尔属性:

option = BeautifulSoup('<option selected=""></option>').option
print(option.encode(formatter="html"))
# b'<option selected=""></option>'

print(option.encode(formatter="html5"))
# b'<option selected></option>'

(此行为是 Beautiful Soup 4.10.0 中的新行为。)

如果传入 formatter=None,Beautiful Soup 将不会在输出中修改任何字符串。这是最快的选项,但可能会导致 Beautiful Soup 生成无效的 HTML/XML,例如:

print(soup.prettify(formatter=None))
# <p>
#  Il a dit <<Sacré bleu!>>
# </p>

link_soup = BeautifulSoup('<a href="http://example.com/?foo=val1&bar=val2">A link</a>', 'html.parser')
print(link_soup.a.encode(formatter=None))
# b'<a href="http://example.com/?foo=val1&bar=val2">A link</a>'

格式化程序对象

如果您需要对输出进行更复杂的控制,您可以实例化 Beautiful Soup 的格式化程序类之一,并将该对象作为格式化程序传递。

class HTMLFormatter

用于自定义HTML文档的格式规则。

这是一个将字符串转换为大写的格式化程序,无论它们出现在字符串对象还是属性值中:

from bs4.formatter import HTMLFormatter
def uppercase(str):
    return str.upper()

formatter = HTMLFormatter(uppercase)

print(soup.prettify(formatter=formatter))
# <p>
#  IL A DIT <<SACRÉ BLEU!>>
# </p>

print(link_soup.a.prettify(formatter=formatter))
# <a href="HTTP://EXAMPLE.COM/?FOO=VAL1&BAR=VAL2">
#  A LINK
# </a>

这是一个在漂亮打印时增加缩进宽度的格式化程序:

formatter = HTMLFormatter(indent=8)
print(link_soup.a.prettify(formatter=formatter))
# <a href="http://example.com/?foo=val1&bar=val2">
#         A link
# </a>
class XMLFormatter

用于自定义 XML 文档的格式规则。

编写自己的格式化程序

继承HTMLFormatterXMLFormatter可以让你更好地控制输出。例如,Beautiful Soup 默认对每个标签中的属性进行排序:

attr_soup = BeautifulSoup(b'<p z="1" m="2" a="3"></p>', 'html.parser')
print(attr_soup.p.encode())
# <p a="3" m="2" z="1"></p>

要关闭此功能,可以子类化 Formatter.attributes() 方法,该方法控制输出哪些属性以及输出顺序。此实现还会过滤掉名为“m”的属性:

class UnsortedAttributes(HTMLFormatter):
    def attributes(self, tag):
        for k, v in tag.attrs.items():
            if k == 'm':
                continue
            yield k, v

print(attr_soup.p.encode(formatter=UnsortedAttributes()))
# <p z="1" a="3"></p>

最后一点需要注意:如果你创建了一个CData对象,该对象中的文本将始终保持原样,不带任何格式。Beautiful Soup 会调用你的实体替换函数,以防你自定义了一个函数来统计文档中所有字符串的数量,但它会忽略返回值:

from bs4.element import CData
soup = BeautifulSoup("<a></a>", 'html.parser')
soup.a.string = CData("one < three")
print(soup.a.prettify(formatter="html"))
# <a>
#  <![CDATA[one < three]]>
# </a>

获取文本()

如果您只想获取文档或标签内可读的文本,可以使用 get_text() 方法。该方法会将文档或标签下的所有文本以单个 Unicode 字符串的形式返回:

markup = '<a href="http://example.com/">\nI linked to <i>example.com</i>\n</a>'
soup = BeautifulSoup(markup, 'html.parser')

soup.get_text()
'\nI linked to example.com\n'
soup.i.get_text()
'example.com'

您可以指定一个字符串来将文本位连接在一起:

# soup.get_text("|")
'\nI linked to |example.com|\n'

您可以告诉 Beautiful Soup 从每段文本的开头和结尾去除空格:

# soup.get_text("|", strip=True)
'I linked to|example.com'

但此时您可能想使用.stripped_strings生成器,并自己处理文本:

[text for text in soup.stripped_strings]
# ['I linked to', 'example.com']

从 Beautiful Soup 版本 4.9.0 开始,当使用 lxml 或 html.parser 时,<script>、<style> 和 <template> 标签的内容通常不被视为“文本”,因为这些标签不是页面中人类可见内容的一部分。

从 Beautiful Soup 4.10.0 版本开始,你可以对 NavigableString 对象调用 get_text()、.strings 或 .stripped_strings。它要么返回对象本身,要么不返回任何内容,所以这样做的唯一原因是当你在混合列表中进行迭代时。

从 Beautiful Soup 4.13.0 版本开始,你可以对 NavigableString 对象调用 .string 方法。它会返回对象本身,所以,再次强调,这样做的唯一原因是当你在混合列表中进行迭代时。

指定要使用的解析器

如果您只需要解析一些 HTML,可以将标记转储到 BeautifulSoup 构造函数中,这样应该没问题。Beautiful Soup 会为您选择一个解析器并解析数据。但是,您可以向构造函数传递一些额外的参数来更改所使用的解析器。

BeautifulSoup 构造函数的第一个参数是一个字符串或一个打开的文件句柄——即你想要解析的标记的来源。第二个参数是你希望如何解析标记。

如果您未指定任何内容,您将获得已安装的最佳 HTML 解析器。Beautiful Soup 将 lxml 的解析器评为最佳,其次是 html5lib 的解析器,最后是 Python 的内置解析器。您可以通过指定以下任一选项来覆盖此设置:

  • 您要解析的标记类型。目前支持的值为“html”、“xml”和“html5”。
  • 您要使用的解析器库的名称。目前支持的选项包括“lxml”、“html5lib”和“html.parser”(Python 内置的 HTML 解析器)。

安装解析器部分对比了受支持的解析器。

如果您请求的解析器尚未安装,Beautiful Soup 会抛出异常,以免您无意中根据一组未知的规则解析文档。例如,目前唯一支持的 XML 解析器是 lxml。如果您没有安装 lxml,请求 XML 解析器将不会返回结果,请求“lxml”也不会成功。

解析器之间的差异

Beautiful Soup 为许多不同的解析器提供了相同的接口,但每个解析器都各不相同。不同的解析器会从同一文档创建不同的解析树。最大的区别在于 HTML 解析器和 XML 解析器之间。以下是一个简短的文档,使用 Python 自带的解析器解析为 HTML:

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

由于独立的 <b/> 标签不是有效的 HTML,html.parser 将其转换为 <b></b> 标签对。

以下是解析为 XML 的同一篇文档(运行此程序需要安装 lxml)。请注意,独立的 <b/> 标签保留了下来,并且文档被赋予了 XML 声明,而不是放在 <html> 标签中。

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

HTML 解析器之间也存在差异。如果您给 Beautiful Soup 一个格式完美的 HTML 文档,这些差异就无关紧要了。一个解析器的速度会比另一个快,但它们都会返回与原始 HTML 文档完全相同的数据结构。

但如果文档格式不完美,不同的解析器会给出不同的结果。以下是使用 lxml 的 HTML 解析器解析的简短且无效的文档。请注意, <a> 标签被包裹在 <body> 和 <html> 标签中,而悬空的 </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> 标签,而是将其与打开的 <p> 标签配对。html5lib 还添加了一个空的 <head> 标签;lxml 则不这么做。

以下是使用 Python 内置 HTML 解析器解析的同一文档:

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

与 lxml 类似,此解析器会忽略结束的 </p> 标签。与 html5lib 或 lxml 不同,此解析器不会尝试通过添加 <html> 或 <body> 标签来创建格式良好的 HTML 文档。

由于文档“<a></p>”无效,以上三种技术都不是处理该文档的“正确”方法。html5lib 解析器使用的是 HTML5 标准中的技术,因此它最有资格被称为“正确”方法,但这三种技术都是合法的。

解析器之间的差异可能会影响您的脚本。如果您计划将脚本分发给其他人,或在多台机器上运行,则应在 BeautifulSoup 构造函数中指定解析器。这将降低用户解析文档的方式与您解析方式不同的可能性。

编码

任何 HTML 或 XML 文档都是用特定的编码(例如 ASCII 或 UTF-8)编写的。但是,当你将该文档加载到 Beautiful Soup 中时,你会发现它已经被转换为 Unicode 了:

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

这可不是魔术。(要是真有魔术就好了。)Beautiful Soup 使用一个名为Unicode, Dammit 的子库来检测文档的编码并将其转换为 Unicode。自动检测出的编码可以通过 BeautifulSoup 对象的 .original_encoding 属性获取:

soup.original_encoding
# 'utf-8'

如果 .original_encoding 为 None,则表示文档在传递到 Beautiful Soup 时已经是 Unicode 了:

markup = "<h1>Sacré bleu!</h1>"
soup = BeautifulSoup(markup, 'html.parser')
print(soup.original_encoding)
# None

Dammit 在大多数情况下都能正确猜测 Unicode,但有时也会出错。有时它会猜对,但需要逐字节搜索文档,这需要很长时间。如果你恰好提前知道文档的编码,可以将其作为 from_encoding 传递给 BeautifulSoup 构造函数,从而避免错误和延迟。

这是一份用 ISO-8859-8 编码的文档。由于文档太短,Unicode 代码 Dammit 无法读取,所以误将其识别为 ISO-8859-7:

markup = b"<h1>\xed\xe5\xec\xf9</h1>"
soup = BeautifulSoup(markup, 'html.parser')
print(soup.h1)
# <h1>νεμω</h1>
print(soup.original_encoding)
# iso-8859-7

我们可以通过传递正确的 from_encoding 来解决这个问题:

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

如果你不知道正确的编码是什么,但你知道 Unicode,Dammit 猜错了,你可以将错误的猜测作为 exclude_encodings 传递:

soup = BeautifulSoup(markup, 'html.parser', exclude_encodings=["iso-8859-7"])
print(soup.h1)
# <h1>םולש</h1>
print(soup.original_encoding)
# WINDOWS-1255

Windows-1255 并非 100% 正确,但该编码是 ISO-8859-8 的兼容超集,因此已经足够接近。(exclude_encodings 是 Beautiful Soup 4.4.0 中的一项新功能。)

在极少数情况下(通常是当 UTF-8 文档包含以完全不同的编码编写的文本时),获取 Unicode 的唯一方法可能是用特殊的 Unicode 字符“替换字符”(U+FFFD,�)替换某些字符。如果 Unicode Dammit 需要执行此操作,它会将 UnicodeDammit 或 BeautifulSoup 对象上的 .contains_replacement_characters 属性设置为 True。这让你知道 Unicode 表示并非原始表示——一些数据丢失了。如果文档包含�,但 .contains_replacement_characters 为 False,你就会知道� 最初就在那里(就像本段中一样),并且不是用来代替丢失的数据的。

输出编码

当你用 Beautiful Soup 写出一个输出文档时,即使输入文档本身不是 UTF-8 编码的,你也会得到一个 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, 'html.parser')
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 字符串一样:

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

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

任何无法用你选择的编码表示的字符都将转换为数字 XML 实体引用。以下文档包含 Unicode 字符“SNOWMAN”:

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

SNOWMAN 字符可以是 UTF-8 文档的一部分(它看起来像 ☃),但是 ISO-Latin-1 或 ASCII 中没有该字符的表示,因此对于这些编码,它被转换为“&#9731”:

print(tag.encode("utf-8"))
# b'<b>\xe2\x98\x83</b>'

print(tag.encode("latin-1"))
# b'<b>&#9731;</b>'

print(tag.encode("ascii"))
# b'<b>&#9731;</b>'

Unicode,该死

不用 Beautiful Soup,你也能用 Unicode 编码。当你的数据编码不明,只想转换成 Unicode 编码时,Beautiful Soup 就派上用场了:

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

如果你安装了以下 Python 库之一:charset-normalizer、chardet 或 cchardet,Unicode Dammit 的猜测会更加准确。你给 Unicode Dammit 的数据越多,它猜测的就越准确。如果你对编码方式有自己的猜测,可以将其作为列表传递:

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

Unicode,Dammit 有两个 Beautiful Soup 未使用的特殊功能。

智能引号

您可以使用 Unicode、Dammit 将 Microsoft 智能引号转换为 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
# '<p>I just &ldquo;love&rdquo; Microsoft Word&rsquo;s smart quotes</p>'

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
# '<p>I just &#x201C;love&#x201D; Microsoft Word&#x2019;s smart quotes</p>'

您还可以将 Microsoft 智能引号转换为 ASCII 引号:

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

希望您觉得这个功能有用,但是 Beautiful Soup 并没有使用它。Beautiful Soup 倾向于默认行为,即将 Microsoft 智能引号以及其他所有内容都转换为 Unicode 字符:

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

编码不一致

有时,文档主要采用 UTF-8 编码,但包含 Windows-1252 字符,例如(再次)Microsoft 智能引号。当网站包含来自多个来源的数据时,可能会发生这种情况。您可以使用 UnicodeDammit.detwingle() 将此类文档转换为纯 UTF-8 编码。以下是一个简单的示例:

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

这份文档乱糟糟的。雪人是 UTF-8 编码的,引号是 Windows-1252 编码的。你可以显示雪人或引号,但不能同时显示两者:

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

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

将文档解码为 UTF-8 编码会引发 UnicodeDecodeError 错误,而将其解码为 Windows-1252 编码则会输出乱码。幸运的是,UnicodeDammit.detwingle() 会将字符串转换为纯 UTF-8 编码,这样您就可以将其解码为 Unicode 格式,并同时显示雪人和引号:

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

UnicodeDammit.detwingle() 只知道如何处理嵌入在 UTF-8 中的 Windows-1252(或者反之亦然,我想),但这是最常见的情况。

请注意,在将数据传递给 BeautifulSoup 或 UnicodeDammit 构造函数之前,必须先调用 UnicodeDammit.detwingle() 函数。Beautiful Soup 假设文档采用单一编码,无论其编码方式如何。如果您传递一个同时包含 UTF-8 和 Windows-1252 的文档,它很可能会认为整个文档都是 Windows-1252 编码,最终文档看起来会像“我喜欢雪人!”。

UnicodeDammit.detwingle() 是 Beautiful Soup 4.1.0 中的新功能。

行号

html.parser 和 html5lib 解析器可以追踪每个标签在原始文档中的位置。您可以通过 Tag.sourceline(行号)和 Tag.sourcepos(起始标签在行内的位置)来访问这些信息:

markup = "<p\n>Paragraph 1</p>\n    <p>Paragraph 2</p>"
soup = BeautifulSoup(markup, 'html.parser')
for tag in soup.find_all('p'):
    print(repr((tag.sourceline, tag.sourcepos, tag.string)))
# (1, 0, 'Paragraph 1')
# (3, 4, 'Paragraph 2')

请注意,这两个解析器中的 sourceline 和 sourcepos 的含义略有不同。对于 html.parser,这些数字表示起始小于号的位置。对于 html5lib,这些数字表示终止大于号的位置:

soup = BeautifulSoup(markup, 'html5lib')
for tag in soup.find_all('p'):
    print(repr((tag.sourceline, tag.sourcepos, tag.string)))
# (2, 0, 'Paragraph 1')
# (3, 6, 'Paragraph 2')

您可以通过将 store_line_numbers=False 传递到 BeautifulSoup 构造函数来关闭此功能:

markup = "<p\n>Paragraph 1</p>\n    <p>Paragraph 2</p>"
soup = BeautifulSoup(markup, 'html.parser', store_line_numbers=False)
print(soup.p.sourceline)
# None

此功能是 4.8.1 中的新功能,基于 lxml 的解析器不支持它。

比较对象是否相等

Beautiful Soup 认为,当两个NavigableStringTag对象表示相同的 HTML 或 XML 标记时,它们就是相等的,即使它们的属性顺序不同,或者位于对象树的不同位置。在本例中,两个 <b> 标签被视为相等,因为它们看起来都像“<b>pizza</b>”。

markup = "<p>I want <b>pizza</b> and more <b>pizza</b>!</p>"
soup = BeautifulSoup(markup, 'html.parser')
first_b, second_b = soup.find_all('b')
print(first_b == second_b)
# True

print(first_b.previous_element == second_b.previous_element)
# False

如果要查看两个变量是否引用完全相同的对象,请使用:

print(first_b is second_b)
# False

复制 Beautiful Soup 对象

您可以使用 copy.copy() 创建任何TagNavigableString的副本:

import copy
p_copy = copy.copy(soup.p)
print(p_copy)
# <p>I want <b>pizza</b> and more <b>pizza</b>!</p>

副本被认为与原始副本相同,因为它代表与原始副本相同的标记,但它不是同一个对象:

print(soup.p == p_copy)
# True

print(soup.p is p_copy)
# False

唯一真正的区别在于,副本完全脱离了原始 Beautiful Soup 对象树,就像调用了 extract() 一样。这是因为两个不同的Tag对象不能同时占据同一空间。

您可以使用 Tag.copy_self() 创建标签的副本,而不复制其内容。

(Tag.copy_self() 在 Beautiful Soup 4.13.0 中引入。)

低级搜索界面

几乎所有使用 Beautiful Soup 从文档中提取信息的人都可以使用“搜索树”中描述的方法获取所需信息。然而,它有一个底层接口,允许您定义任何所需的匹配行为。在幕后,大多数人使用的 Beautiful Soup API 中的那些部分——find_all() 之类的——实际上都在使用这个底层接口,您可以直接使用它。

(访问低级搜索界面是 Beautiful Soup 4.13.0 中的新功能。)

自定义元素过滤

class ElementFilter

ElementFilter类是底层接口的入口。要使用它,请定义一个函数,该函数接受一个 PageElement 对象(可以是TagNavigableString )。如果元素符合您的自定义条件,则该函数必须返回 True;否则,返回 False。

此示例函数查找包含内容的标签和字符串,但跳过仅包含空格的字符串:

from bs4 import Tag, NavigableString
def non_whitespace_element_func(tag_or_string):
    """
    return True for:
    * all Tag objects
    * NavigableString objects that contain non-whitespace text
    """
    return (
        isinstance(tag_or_string, Tag) or
        (isinstance(tag_or_string, NavigableString) and
            tag_or_string.strip() != ""))

一旦有了函数,就将其传递到ElementFilter构造函数中:

from bs4.filter import ElementFilter
non_whitespace_filter = ElementFilter(non_whitespace_element_func)

然后,您可以将此ElementFilter对象用作任何搜索树方法的第一个参数。函数中定义的任何条件都将被用来替代默认的 Beautiful Soup 匹配逻辑:

from bs4 import BeautifulSoup
small_doc = """
<p>
  <b>bold</b>
  <i>italic</i>
  and
  <u>underline</u>
</p>
"""
soup = BeautifulSoup(small_doc, 'html.parser')

soup.find('p').find_all(non_whitespace_filter, recursive=False)
# [<b>bold</b>, <i>italic</i>, '\n  and\n  ', <u>underline</u>]

soup.find("b").find_next(non_whitespace_filter)
# 'bold'

soup.find("i").find_next_siblings(non_whitespace_filter)
# ['\n  and\n  ', <u>underline</u>]

每个潜在匹配都将通过您的函数运行,并且返回的唯一 PageElement 对象将是您的函数返回 True 的对象。

总结基于功能的匹配行为,

  • 作为搜索方法的第一个参数传递的函数(或等效地使用名称参数)仅考虑Tag对象。
  • 使用字符串参数传递给搜索方法的函数仅考虑NavigableString对象。
  • 使用ElementFilter对象传递给搜索方法的函数会同时考虑TagNavigableString对象。

自定义元素迭代

ElementFilter.filter()

通过将ElementFilter实例传递给 Beautiful Soup 的树搜索方法,您可以完全自定义 Beautiful Soup 在解析树迭代过程中匹配元素的含义。通过使用ElementFilter.filter()方法,您还可以完全自定义 Beautiful Soup 在解析树迭代过程中的含义。

ElementFilter.filter()方法接受一个生成器,该生成器会生成一个 PageElement 对象流。对于 PageElement 对象的显示类型、显示次数以及显示顺序,没有任何限制。理论上,它们甚至不需要来自同一个 BeautifulSoup 文档。您可以根据自己的需要进行任何操作。

这是一个愚蠢的例子:一个在解析树中随机来回走动的生成器:

import random
def random_walk(starting_location):
    location = starting_location
    while location is not None:
        yield location
        if random.random() < 0.5:
            location = location.next_element
        else:
            location = location.previous_element
        if location is None:
            return

将此生成器传递到示例ElementFilter.filter()中,Beautiful Soup 将在解析树中随机游走,将 non_whitespace_filter 函数应用于它找到的每个元素,并产生所有匹配项 - 可能多次产生给定的对象:

[x for x in non_whitespace_filter.filter(random_walk(soup.b))]
# [<b>bold</b>, 'bold', <b>bold</b>, <p><b>bold</b>...]

[x for x in non_whitespace_filter.filter(random_walk(soup.b))]
# [<b>bold</b>, <b>bold</b>, 'bold', <i>italic</i>, <i>italic</i>, ...]

(请注意,与本文档中的其他代码示例不同,由于随机元素,此示例每次运行时都会给出不同的结果。这种情况不太可能发生,但该函数可能会永远在解析树中徘徊并且永远不会完成。)

高级解析器定制

Beautiful Soup 提供了多种方法来定制解析器处理传入 HTML 和 XML 的方式。本节介绍最常用的定制技巧。

仅解析文档的一部分

假设您想使用 Beautiful Soup 查看文档的 <a> 标签。解析整个文档,然后再遍历一遍寻找 <a> 标签,会浪费时间和内存。相比之下,一开始就忽略所有非 <a> 标签的内容会更快。SoupStrainer 类允许您选择要解析传入文档哪些部分。您只需创建一个SoupStrainer并将其作为 parse_only 参数传递给 BeautifulSoup 构造函数即可。

(请注意,如果您使用的是 html5lib 解析器,则此功能将不起作用。如果您使用 html5lib,无论如何,整个文档都会被解析。这是因为 html5lib 在工作时会不断重新排列解析树,如果文档的某些部分实际上没有进入解析树,它就会崩溃。为了避免混淆,在下面的例子中,我将强制 Beautiful Soup 使用 Python 的内置解析器。)

class SoupStrainer

SoupStrainer类接受的参数与搜索树中的典型方法相同: nameattrsstring**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 string is not None and len(string) < 10

only_short_strings = SoupStrainer(string=is_short_string)

我将再次带回“三姐妹”文档,我们将看看使用这三个SoupStrainer对象解析该文档时是什么样子:

html_doc = """<html><head><title>The Dormouse's story</title></head>
<body>
<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 的行为如下:

  • 当标签匹配时,它会被保留(包括其所有内容,无论它们是否也匹配)。
  • 当标签不匹配时,标签本身不会被保留,但解析会继续进入其内容以寻找其他匹配的标签。

自定义多值属性

在 HTML 文档中,像 class 这样的属性会被赋予一个值列表,而像 id 这样的属性则会被赋予单个值,因为 HTML 规范对这些属性的处理方式不同:

markup = '<a class="cls1 cls2" id="id1 id2">'
soup = BeautifulSoup(markup, 'html.parser')
soup.a['class']
# ['cls1', 'cls2']
soup.a['id']
# 'id1 id2'

你可以通过传入 multi_valued_attributes=None 来关闭此功能。这样所有属性都会被赋予一个值:

soup = BeautifulSoup(markup, 'html.parser', multi_valued_attributes=None)
soup.a['class']
# 'cls1 cls2'
soup.a['id']
# 'id1 id2'

你可以通过传入 multi_valued_attributes 字典来自定义此行为。如果需要,请查看 HTMLTreeBuilder.DEFAULT_CDATA_LIST_ATTRIBUTES 来了解 Beautiful Soup 默认使用的配置,该配置基于 HTML 规范。

(这是 Beautiful Soup 4.8.0 中的新功能。)

处理重复属性

当使用 html.parser 解析器时,你可以使用 on_duplicate_attribute 构造函数参数来自定义 Beautiful Soup 在遇到多次定义相同属性的标签时所采取的操作:

markup = '<a href="http://url1/" href="http://url2/">'

默认行为是使用为标签找到的最后一个值:

soup = BeautifulSoup(markup, 'html.parser')
soup.a['href']
# http://url2/

soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute='replace')
soup.a['href']
# http://url2/

使用 on_duplicate_attribute='ignore' 你可以告诉 Beautiful Soup 使用找到的第一个值并忽略其余值:

soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute='ignore')
soup.a['href']
# http://url1/

(lxml 和 html5lib 总是这样做;它们的行为无法从 Beautiful Soup 内部配置。)

如果您需要更多控制,您可以传递一个在每个重复值上调用的函数:

def accumulate(attributes_so_far, key, value):
    if not isinstance(attributes_so_far[key], list):
        attributes_so_far[key] = [attributes_so_far[key]]
    attributes_so_far[key].append(value)

soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute=accumulate)
soup.a['href']
# ["http://url1/", "http://url2/"]

(这是 Beautiful Soup 4.9.1 中的新功能。)

实例化自定义子类

当解析器向 Beautiful Soup 传入一个标签或字符串时,Beautiful Soup 会实例化一个TagNavigableString对象来包含该信息。除了默认行为之外,您还可以让 Beautiful Soup 实例化TagNavigableString的子类,即您自定义行为的子类:

from bs4 import Tag, NavigableString
class MyTag(Tag):
    pass


class MyString(NavigableString):
    pass


markup = "<div>some text</div>"
soup = BeautifulSoup(markup, 'html.parser')
isinstance(soup.div, MyTag)
# False
isinstance(soup.div.string, MyString)
# False

my_classes = { Tag: MyTag, NavigableString: MyString }
soup = BeautifulSoup(markup, 'html.parser', element_classes=my_classes)
isinstance(soup.div, MyTag)
# True
isinstance(soup.div.string, MyString)
# True

当将 Beautiful Soup 纳入测试框架时,这会很有用。

(这是 Beautiful Soup 4.8.1 中的新功能。)

故障排除

诊断()

如果您不明白 Beautiful Soup 是如何解析文档的,可以将文档传入诊断函数。(该函数是 Beautiful Soup 4.2.0 中的新增函数。)Beautiful Soup 会打印一份报告,展示不同的解析器如何处理文档,并提示您是否遗漏了 Beautiful Soup 可以使用的解析器:

from bs4.diagnose import diagnose
with open("bad.html") as fp:
    data = fp.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:
# ...

仅仅查看诊断函数的输出或许就能帮你解决问题。即使没有,你也可以在寻求帮助时粘贴诊断函数的输出。

解析文档时出错

解析错误有两种。一种是崩溃,当你将文档输入到 Beautiful Soup 时,它会抛出异常(通常是 HTMLParser.HTMLParseError)。另一种是意外行为,当你解析出的 Beautiful Soup 解析树与创建它的文档看起来有很大不同时,它会抛出异常。

这些问题几乎从来不是 Beautiful Soup 本身的问题。这并不是因为 Beautiful Soup 本身写得非常好,而是因为 Beautiful Soup 本身不包含任何解析代码,而是依赖于外部解析器。如果某个解析器无法处理某个文档,最好的解决方案是尝试其他解析器。请参阅安装解析器以了解详细信息和解析器比较。如果这没有帮助,您可能需要检查 BeautifulSoup 对象内的文档树,看看您要查找的标记最终位于何处。

版本不匹配问题

  • SyntaxError:语法无效(在行 ROOT_TAG_NAME = '[document]'):由于在 Python 3 下运行旧版本的 Python 2 版本的 Beautiful Soup 而未转换代码而导致。
  • ImportError:没有名为 HTMLParser 的模块 - 由于在 Python 3 下运行旧版本的 Python 2 版本的 Beautiful Soup 而导致。
  • ImportError:没有名为 html.parser 的模块 - 由于在 Python 2 下运行 Python 3 版本的 Beautiful Soup 而导致。
  • ImportError:没有名为 BeautifulSoup 的模块 - 原因是在未安装 BS3 的环境中运行 Beautiful Soup 3 代码。或者,在编写 Beautiful Soup 4 代码时,不知道包名已更改为 bs4。
  • ImportError:没有名为 bs4 的模块 - 由于在未安装 BS4 的环境中运行 Beautiful Soup 4 代码而导致。

解析 XML

默认情况下,Beautiful Soup 将文档解析为 HTML。要将文档解析为 XML,请将 “xml” 作为第二个参数传递给 BeautifulSoup 构造函数:

soup = BeautifulSoup(markup, "xml")

您需要安装 lxml

其他解析器问题

  • 如果您的脚本在一台计算机上运行,但在另一台计算机上无法运行;或者在一个虚拟环境中运行,但在另一台虚拟环境中无法运行;或者在虚拟环境外部运行,但在虚拟环境内部运行,则可能是因为这两个环境的解析器库不同。例如,您可能在安装了 lxml 的计算机上开发了脚本,然后尝试在仅安装了 html5lib 的计算机上运行它。请参阅解析器之间的差异,了解为什么会出现这种情况,并通过在 BeautifulSoup 构造函数中指定特定的解析器库来解决问题。
  • 由于HTML 标签和属性不区分大小写,因此这三个 HTML 解析器都会将标签和属性名称转换为小写。也就是说,标记 <TAG></TAG> 会被转换为 <tag></tag>。如果您想保留大小写混合或大写的标签和属性,则需要将文档解析为 XML。

各种各样的

  • UnicodeEncodeError: 'charmap' 编解码器无法在位置栏中编码字符 '\xfoo'(或任何其他 UnicodeEncodeError)——此问题主要出现在两种情况下。首先,当您尝试打印控制台无法显示的 Unicode 字符时。(请参阅Python wiki 上的此页面获取帮助。)其次,当您写入文件并传入默认编码不支持的 Unicode 字符时。在这种情况下,最简单的解决方案是使用 u.encode("utf8") 将 Unicode 字符串显式编码为 UTF-8。
  • KeyError: [attr] - 当相关标签未定义 attr 属性时,访问 tag['attr'] 会导致此错误。最常见的错误是 KeyError: 'href' 和 KeyError: 'class'。如果您不确定 attr 是否已定义,请使用 tag.get('attr') ,就像使用 Python 字典一样。
  • AttributeError: 'ResultSet' 对象没有属性 'foo' - 这种情况通常是因为您期望 find_all() 返回单个标签或字符串。但 find_all() 返回的是标签和字符串的列表——一个 ResultSet 对象。您需要遍历该列表并查看每个结果的 .foo 文件。或者,如果您确实只想要一个结果,则需要使用 find() 而不是 find_all()。
  • AttributeError: 'NoneType' 对象没有属性 'foo' - 这通常是因为您调用了 find() 函数,然后尝试访问结果集的 .foo 属性。但在本例中,find() 函数没有找到任何结果,因此返回了 None 而不是标签或字符串。您需要找出 find() 函数没有返回任何结果的原因。
  • AttributeError: 'NavigableString' 对象没有属性 'foo' - 这种情况通常是因为你把字符串当成了标签。你可能在遍历一个列表,期望它只包含标签,但实际上它同时包含标签和字符串。

提高性能

Beautiful Soup 的速度永远不会比它所依赖的解析器快。如果响应时间至关重要,或者你按小时支付电脑时间,或者出于其他原因,电脑时间比程序员的时间更宝贵,那么你应该放弃 Beautiful Soup,直接在lxml上工作。

话虽如此,你还是可以采取一些措施来加速 Beautiful Soup。如果你没有使用 lxml 作为底层解析器,我建议你开始使用。Beautiful Soup 使用 lxml 解析文档的速度比使用 html.parser 或 html5lib 快得多。

通过安装cchardet库,您可以显著加快编码检测速度。

仅解析文档的一部分不会为您节省大量解析文档的时间,但可以节省大量内存,并使搜索文档的速度更快。

翻译本文档

非常感谢 Beautiful Soup 文档的新译本。译本应遵循 MIT 许可证,就像 Beautiful Soup 及其英文文档一样。

有两种方法可以将您的翻译放入主代码库和 Beautiful Soup 网站:

  1. 创建 Beautiful Soup 存储库的一个分支,添加您的翻译,并建议与主分支合并,就像您对源代码的建议更改一样。
  2. 向 Beautiful Soup 讨论组发送一条消息,其中包含您的翻译的链接,或将您的翻译附加到消息中。

使用中文或巴西葡萄牙语翻译作为参考。特别地,请翻译源文件 doc/index.rst,而不是文档的 HTML 版本。这样可以以多种格式发布文档,而不仅仅是 HTML 格式。

美丽汤3

Beautiful Soup 3 是之前的发行版本,现已不再受支持。Beautiful Soup 3 的开发于 2012 年停止,并于 2021 年完全停产。除非您想让非常老旧的软件运行,否则没有必要安装它。但它通过 PyPi 以 BeautifulSoup 的名称发布:

$ pip install BeautifulSoup

您还可以下载最终版本 3.2.2 的 tarball

如果您运行了 pip install beautifulsoup 或 pip install BeautifulSoup,但代码不起作用,则说明您错误地安装了 Beautiful Soup 3。您需要运行 pip install beautifulsoup4。

Beautiful Soup 3 的文档已在线存档

将代码移植到 BS4

大部分针对 Beautiful Soup 3 编写的代码,只需简单修改一下,就能在 Beautiful Soup 4 上运行。你只需要将包名从 BeautifulSoup 改为 bs4 即可。例如:

from BeautifulSoup import BeautifulSoup

变成这样:

from bs4 import BeautifulSoup
  • 如果您收到 ImportError“没有名为 BeautifulSoup 的模块”,则问题在于您正在尝试运行 Beautiful Soup 3 代码,但您只安装了 Beautiful Soup 4。
  • 如果您收到 ImportError“没有名为 bs4 的模块”,则问题在于您正在尝试运行 Beautiful Soup 4 代码,但您只安装了 Beautiful Soup 3。

虽然 BS4 基本与 BS3 向后兼容,但为了符合 PEP 8 规范, BS4 的大部分方法已被弃用并被赋予新名称。此外,还有许多其他重命名和更改,其中一些破坏了向后兼容性。

以下是将 BS3 代码和习惯转换为 BS4 所需了解的内容:

你需要一个解析器

Beautiful Soup 3 使用了 Python 的 SGMLParser 模块,该模块在 Python 3.0 中已被弃用并移除。Beautiful Soup 4 默认使用 html.parser 模块,但您可以插入 lxml 或 html5lib 模块来代替。请参阅“安装解析器”以进行比较。

由于 html.parser 与 SGMLParser 不是同一个解析器,您可能会发现 Beautiful Soup 4 针对相同的标记生成的解析树与 Beautiful Soup 3 生成的解析树不同。如果您将 html.parser 替换为 lxml 或 html5lib,您可能会发现解析树再次发生变化。如果发生这种情况,您需要更新您的抓取代码以处理新的解析树。

属性名称

我重命名了三个属性,以避免使用对 Python 有特殊含义的词语。与我对方法名称的更改(您将以弃用警告的形式看到)不同,这些更改并未保留向后兼容性。如果您在 BS3 中使用这些属性,您的代码将在 BS4 中崩溃,除非您更改它们。

  • UnicodeDammit.unicode -> UnicodeDammit.unicode_markup
  • 标签.next -> 标签.next_element
  • 标签.previous -> 标签.previous_element

生成器

一些生成器在执行完成后会返回 None 并停止。这是一个 bug。现在生成器会直接停止。

XML

不再有 BeautifulStoneSoup 类用于解析 XML。要解析 XML,您需要将“xml”作为第二个参数传递给 BeautifulSoup 构造函数。出于同样的原因,BeautifulSoup 构造函数不再识别 isHTML 参数。

Beautiful Soup 对空元素 XML 标签的处理已得到改进。以前,解析 XML 时必须明确指定哪些标签被视为空元素标签。构造函数的 selfClosingTags 参数不再被识别。取而代之的是,Beautiful Soup 将任何空标签视为空元素标签。如果为空元素标签添加子标签,该标签将不再被视为空元素标签。

实体

传入的 HTML 或 XML 实体始终会被转换为相应的 Unicode 字符。Beautiful Soup 3 中曾存在许多处理实体的重叠方法,这些方法已被移除。BeautifulSoup 的构造函数不再识别 smartQuotesTo 或 convertEntities 参数。( Unicode 方面,Dammit仍然有 smart_quotes_to,但现在默认将智能引号转换为 Unicode。)常量 HTML_ENTITIES、XML_ENTITIES 和 XHTML_ENTITIES 已被移除,因为它们配置了一个不再存在的功能(将部分(而非全部)实体转换为 Unicode 字符)。

如果要在输出时将 Unicode 字符转换回 HTML 实体,而不是将其转换为 UTF-8 字符,则需要使用输出格式化程序

各种各样的

Tag.string现在可递归操作。如果标签 A 仅包含一个标签 B,则 A.string 与 B.string 相同。(之前为 None。)

像 class 这样的多值属性,其值是字符串列表,而不是简单的字符串。这可能会影响您按 CSS class 搜索的方式。

标签对象现在实现了 __hash__ 方法,这样如果两个标签对象生成相同的标记,它们就会被视为相等。如果您将标签对象放入字典或集合中,这可能会改变脚本的行为。

如果您向 find* 方法传递字符串和特定于标签的参数(例如name ) ,Beautiful Soup 将搜索符合特定于标签的条件且其Tag.string值与字符串值匹配的标签。它不会查找字符串本身。之前,Beautiful Soup 会忽略特定于标签的参数,直接查找字符串。

BeautifulSoup 构造函数不再识别 markupMassage 参数。现在,正确处理标记的责任由解析器承担。

一些很少使用的替代解析器类(例如 ICantBelieveItsBeautifulSoup 和 BeautifulSOAP)已被移除。现在,如何处理歧义标记由解析器决定。

prettify() 方法现在返回 Unicode 字符串,而不是字节字符串。