Beautiful Soup 4.12.0 文档

Beautiful Soup 是一个 可以从 HTML 或 XML 文件中提取数据的 Python 库。它能用你喜欢的解析器和习惯的方式实现 文档树的导航、查找、和修改。它会帮你节省数小时甚至数天的工作时间。

这篇文档介绍了 Beautiful Soup 4 中所有主要特性,并附带例子。文档会展示这个库的适合场景, 工作原理,怎样使用,如何达到预期效果,以及如何处理异常情况。

文档覆盖了 Beautful Soup 4.12.0 版本,文档中的例子使用 Python 3.8 版本编写。

你可能在寻找 Beautiful Soup3 的文档,Beautiful Soup 3 目前已经停止开发,并且自 2020年12月31日以后就停止维护了。 如果想要了解 Beautiful Soup 3 和 Beautiful Soup 4 的不同,参考 迁移到 BS4

这篇文档已经被翻译成多种语言:

寻求帮助

如果有关于 Beautiful Soup 4 的疑问,或遇到了问题,可以发送邮件到 讨论组

如果问题中包含要解析的 HTML 代码,那么请在你的问题描述中附带这段HTML文档的 代码诊断 [1]

如果报告文档中的错误,请指出具体文档的语言版本。

快速开始

下面的一段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="link2">
#     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> 标签的链接:

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

另一种常见任务,是从文档中获取所有文字内容:

print(soup.get_text())
# The Dormouse's story
#
# The Dormouse's story
#
# Once upon a time there were three little sisters; and their names were
# Elsie,
# Lacie and
# Tillie;
# and they lived at the bottom of a well.
#
# ...

这是你想要的吗?是的话,继续看下去。

安装 Beautiful Soup

如果你用的是新版的 Debain 或 Ubuntu,那么可以通过系统的软件包管理来安装:

$ apt-get install python3-bs4

Beautiful Soup 4 通过 PyPi 发布,所以如果无法使用系统包管理安装,那么 也可以通过 easy_installpip 来安装。包的名字是 beautifulsoup4。 确保使用的是与 Python 版本对应的 pipeasy_install 版本 (他们的名字也可能是 pip3easy_install )。

$ easy_install beautifulsoup4

$ pip install beautifulsoup4

(在 PyPi 中还有一个名字是 BeautifulSoup 的包,但那可能不是你想要的,那是 Beautiful Soup3 版本。因为很多项目还在使用BS3, 所以 BeautifulSoup 包依然有效。但是新项目中,应该安装 beautifulsoup4。)

如果没有安装 easy_installpip ,那也可以 下载 BS4 的源码 , 然后通过 setup.py 来安装。

$ Python setup.py install

如果上述安装方法都行不通,根据 Beautiful Soup 的协议,可以将项目的代码打包在 你的项目中,这样无须安装即可使用。

Beautiful Soup 用 Python 3.10 版本开发,但也可以在当前的其它版本中运行。

安装解析器

Beautiful Soup 支持 Python 标准库中的 HTML 解析器,还支持一些第三方的解析器, 其中一个是 lxml parser 。根据安装方法的不同, 可以选择下列方法来安装 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的内置标准库
- 执行速度较快
- 容错能力强
- 速度没有 lxml 快,容错没有 html5lib强


lxml HTML 解析器


BeautifulSoup(markup, "lxml")


- 速度快
- 容错能力强

- 额外的 C 依赖


lxml XML 解析器

BeautifulSoup(markup, ["lxml-xml"])
BeautifulSoup(markup, "xml")
- 速度快
- 唯一支持 XML 的解析器
- 额外的 C 依赖

html5lib


BeautifulSoup(markup, "html5lib")


- 最好的容错性
- 以浏览器的方式解析文档
- 生成 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 选择最合适的解析器来解析这段文档。如果指定了解析器那么 Beautiful Soup 会选择指定的解析器来解析文档。(参考 解析成XML )。

对象的种类

Beautiful Soup 将复杂的 HTML 文档转换成一个复杂的由 Python 对象构成的树形结构,但处理对象 的过程只包含 4 种类型的对象: Tag, NavigableString, BeautifulSoup, 和 Comment

Tag Tag 对象与 XML 或 HTML 原生文档中的 tag 相同:

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

Tag有很多属性和方法,在 遍历文档树搜索文档树 中有详细解释。 现在介绍一下 tag 中最重要的属性: name 和 attributes。

bs4.name

每个 tag 都有一个名字:

tag.name
# u'b'

如果改变了 tag 的 name,那将影响所有通过当前 Beautiful Soup 对象生成的HTML文档:

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

一个 HTML 或 XML 的 tag 可能有很多属性。tag <b id="boldest"> 有 一个 “id” 的属性,值为 “boldest”。你可以想处理一个字段一样来处理 tag 的属性:

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

也可以直接”点”取属性,比如: .attrs :

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

tag 的属性可以被添加、删除或修改。再说一次,tag的属性操作方法与字典一样

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 定义了一系列可以包含多个值的属性。在 HTML5 中移除了一些,却增加更多。 最常见的多值的属性是 class (一个 tag 可以有多个 CSS class)。还有一些 属性 relrevaccept-charsetheadersaccesskey。 默认情况,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']

If an attribute `looks` like it has more than one value, but it's not
a multi-valued attribute as defined by any version of the HTML
standard, Beautiful Soup will leave the attribute alone::

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

如果某个属性看起来好像有多个值,但在任何版本的 HTML 定义中都没有将其定义为多值属性, 那么 Beautiful Soup 会将这个属性作为单值返回

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

将 tag 转换成字符串时,多值属性会合并为一个值

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>

若想强制将所有属性当做多值进行解析,可以在 BeautifulSoup 构造方法中设置 multi_valued_attributes=None 参数:

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.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
class bs4.NavigableString

可遍历的字符串

字符串对应 tag 中的一段文本。Beautiful Soup 用 NavigableString 类来包装 tag 中的字符串:

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

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() 方法,将该对象转换成普通的Unicode字符串,否则就算 Beautiful Soup 方法已经执行结束,该对象的输出 也会带有对象的引用地址。这样会浪费内存。

class bs4.BeautifulSoup

BeautifulSoup 对象表示的是一个文档的全部内容。大部分时候,可以把它当作 Tag 对象, 它支持 遍历文档树搜索文档树 中描述的大部分的方法。

因为 BeautifulSoup 对象并不是真正的HTML或XML的tag,所以它没有name和attribute属性。 但有时查看它的 .name 属性是很方便的,所以 BeautifulSoup 对象包含了一个 值为 “[document]” 的特殊属性 .name

soup.name
# u'[document]'

注释及特殊字符串

Tag, NavigableString, BeautifulSoup 几乎覆盖了html和xml中的所有内容,但是还有一些特殊对象。容易让人担心的内容是文档的注释部分:

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

针对 HTML 文档

Beautiful Soup 定义了一些 NavigableString 子类来处理特定的 HTML 标签。 通过忽略页面中表示程序指令的字符串,可以更容易挑出页面的 body 内容。 (这些类是在 Beautiful Soup 4.9.0 版本中添加的,html5lib 解析器不会使用它们)

class bs4.Stylesheet

有一种 NavigableString 子类表示嵌入的 CSS 脚本; 内容是 <style> 标签内部的所有字符串。

class bs4.Script

有一种 NavigableString 子类表示嵌入的 JavaScript 脚本; 内容是 <script> 标签内部的所有字符串。

class bs4.Template

有一种 NavigableString 子类表示嵌入的 HTML 模板, 内容是 <template> 标签内部的所有字符串。

针对 XML 文档

Beautiful Soup 定义了一些 NavigableString 子类来处理 XML 文档中的特定 字符串。比如 Comment,这些 NavigableString 的子类生成字符 串时会添加额外内容。

class bs4.Declaration

有一种 NavigableString 子类表示 XML 文档开头的 declaration

class bs4.Doctype

有一种 NavigableString 子类表示可能出现在 XML 文档开头的 document type declaration

class bs4.CData

有一种 NavigableString 子类表示 CData section

class bs4.ProcessingInstruction

有一种 NavigableString 子类表示 XML 处理指令

遍历文档树

还是用”爱丽丝”的文档来做例子:

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

通过这段例子来演示怎样从文档的一段内容找到另一段内容

子节点

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> 标签,或是比通过名字获取内容更复杂的方法时,就需要用到 搜索文档树 中描述的方法,比如: find_all()

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 属性可以将 tag 的全部子节点以列表的方式输出:

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 生成器,可以对 tag 的子节点进行循环:

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

如果想要修改 tag 的子节点,使用 修改文档树 中描述的方法。不要直接修改 contents 列表: 那样会导致细微且难以定位的问题。

.descendants

.contents.children 属性仅包含 tag 的直接子节点。例如,<head> 标签只有一个直接 子节点 <title>

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 对象只有一个直接子节点(<html> 节点),却有很多子孙节点:

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 属性来获取某个元素的父节点。在例子“爱丽丝”的文档中,<head> 标签是 <title> 标签的父节点:

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 是None:

print(soup.parent)
# None

.parents

通过元素的 .parents 属性可以递归得到元素的所有父辈节点,下面的例子使用了 .parents 方法遍历了 <a> 标签到根节点的所有节点。

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></a>", 'html.parser')
print(sibling_soup.prettify())
#   <a>
#    <b>
#     text1
#    </b>
#    <c>
#     text2
#    </c>
#   </a>

因为 <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 结果是第二个 <a> 标签,那就错了, 真实结果是第一个 <a> 标签和第二个<a> 标签之间的顿号和换行符:

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))
# ',\n'
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
# ' and\n'
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
# '; and they lived at the bottom of a well.'

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>
# ',\n'
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
# 'Once upon a time there were three little sisters; and their names were\n'

回退和前进

看一下“爱丽丝” 文档:

<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 属性结果是在 <a> 标签被解析之后的解析内容, 不是 <a> 标签后的句子部分,而是字符串 “Tillie”:

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>
<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() 或类似方法前,先介绍一下这些方法可以使用哪些过滤器的类型 [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() 来匹配内容。 下面例子中找出所有以 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

列表

如果传入列表参数,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 可以匹配任何值,下面代码查找到所有的 tag,但是不会返回字符串节点

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

函数

如果没有合适过滤器,那么还可以定义一个函数方法,参数是一个元素 [4] , 如果这个方法返回 True 表示当前元素匹配并且被找到,如果不是则反回 False

下面方法实现的匹配功能是,如果包含 class 属性却不包含 id 属性, 那么将返回 True:

def has_class_but_no_id(tag):
    return tag.has_attr('class') and not tag.has_attr('id')

将这个方法作为参数传入 find_all() 方法,将得到所有 <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...</p>,
#  <p class="story">...</p>]

返回结果中只有 <p> 标签,没有 <a> 标签,因为 <a> 标签还定义了”id”, 没有返回 <html> 和 <head>,因为 <html> 和 <head> 中没有定义 “class” 属性。

如果通过方法来筛选特殊属性,比如 href,传入方法的参数应该是对应属性的值, 而不是整个元素。下面的例子是找出那些 a 标签中的 href 属性不匹配指定正则:

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

标签过滤方法可以使用复杂方法。下面的例子可以过滤出前后都有文字的标签。

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 , string , **kwargs )

find_all() 方法搜索当前 tag 的所有子节点,并判断是否符合过滤器的条件。 过滤器类型 中已经举过几个例子,这里再展示几个新例子:

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"))
# u'Once upon a time there were three little sisters; and their names were\n'

有几个方法很相似,还有几个方法是新的,参数中的 stringid 是什么含义? 为什么 find_all("p", "title") 返回的是CSS Class为”title”的<p>标签? 我们来仔细看一下 find_all() 的参数

name 参数

传一个值给 name 参数,就可以查找所有名字为 name 的 tag。所有文本都会被忽略掉, 因为它们不匹配标签名字。

简单的用法如下:

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

回忆 过滤器类型 中描述的内容,搜索 name 的参数值可以是: 字符串、正则表达式、列表、方法或是 True

keyword 参数

如果动态参数中出现未能识别的参数名,搜索时会把该参数当作 tag 属性来搜索, 比如搜索参数中包含一个名字为 id 的参数,Beautiful Soup 会搜索每个 tag 上的 id 属性

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

如果传入 href 参数,Beautiful Soup会搜索每个 tag 的 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 参数定义一个字典参数 来搜索包含特殊属性的 tag:

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

不要使用 “name” 作为关键字参数搜索 HTML 元素,因为 Beautiful Soup 用 name 来识别 tag 本身的名字。换一种方法,你可以这样搜索属性中的 “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类名的 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 中任意 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 值进行完全匹配:

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

完全匹配 class 的值时,如果CSS类名的顺序与实际不符,将搜索不到结果:

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

如果想要通过多个 CSS 类型来搜索 tag,应该使用 CSS 选择器

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

在旧版本的 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>]

string 参数

通过 string 参数可以搜索文档中的字符串内容。与 name 参数接受的值一样, string 参数接受 字符串 , 正则表达式 , 列表, 函数, True 。看例子:

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

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

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

虽然 string 参数用于搜索字符串,同时也以与其它参数混合使用来搜索 tag。 Beautiful Soup 会过滤那些 string 值与 .string 参数相符的 tag。 下面代码用来搜索内容里面包含 “Elsie” 的 <a> 标签:

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

string 参数是在 4.4.0 中新增的。早期版本中该参数名为 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 参数

如果调用 mytag.find_all() 方法,Beautiful Soup 会检索 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> 标签之下,但并不是直接子节点,<head> 标签才是直接子节点。 在允许查询所有后代节点时 Beautiful Soup 能够查找到 <title> 标签。 但是使用了 recursive=False 参数之后,只能查找直接子节点,这样就查不到 <title> 标签了。

Beautiful Soup 提供了多种 DOM 树搜索方法。这些方法都使用了类似的参数定义。 比如这些方法: find_all(): name, attrs, text, limit. 但是只有 find_all()find() 支持 recursive 参数。

像调用 find_all() 一样调用tag

find_all() 几乎是 Beautiful Soup 中最常用的搜索方法,所以我们定义了它的简写方法。 BeautifulSoup 对象和 Tag 对象可以被当作一个方法来使用,这个方法的执行结果与 调用这个对象的 find_all() 方法相同,下面两行代码是等价的:

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

这两行代码也是等价的:

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

find()

find(name , attrs , recursive , string , **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.titleTag 的名字 方法的简写。这个简写就是通过多次调用 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 , string , **kwargs )

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

我们已经用了很大篇幅来介绍 find_all()find() 方法,Beautiful Soup 中 还有 10 个用于搜索的 API。它们中有 5 个用的是与 find_all() 相同的搜索参数, 另外 5 个与 find() 方法的搜索参数类似。区别仅是它们搜索文档的位置不同。

首先来看看 find_parents()find_parent()。 记住: find_all()find() 只搜索当前节点的所有子节点,孙子节点等。 而这 2 个方法刚好相反,它们用来搜索当前节点的父辈节点。 我们来试试看,从例子文档中的一个深层叶子节点开始:

a_string = soup.find(string="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 , string , **kwargs )

find_next_sibling( name , attrs , recursive , string , **kwargs )

这 2 个方法通过 .next_siblings 属性对当 tag 的所有后面解析 [5] 的兄弟tag节点进行迭代, find_next_siblings() 方法返回所有符合条件的后面的兄弟节点, find_next_sibling() 只返回符合条件的后面的第一个 tag 节点。

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 , string , **kwargs )

find_previous_sibling( name , attrs , recursive , string , **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 , string , **kwargs )

find_next( name , attrs , recursive , string , **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(string=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 , string , **kwargs )

find_previous( name , attrs , recursive , string , **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") 既返回了文档中的第一段(class=”title”的那段),还返回了第二段, 包含了我们开始查找的 <a> 标签的那段。不用惊讶,这段代码的功能是查找所有出现在指定 <a> 标签之前 的 <p> 标签,因为这个 <p> 标签包含了开始的 <a> 标签,所以 <p> 标签当然是在 <a> 之前出现的。

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

查找指定层级的 tag:

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

找到某个 tag 标签下的直接子标签 [6] :

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

通过 id 查找 tag:

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

查找符合列表中任意一个选择器的 tag:

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 中使用相同的方法。 但是如果你只需要使用 CSS 选择器就够了,那么应该 lxml 作为文档解析器:因为速度快很多。 但是 Soup Sieve 也有优势,它允许 组合 使用 CSS 选择器和 Beautiful Soup 的 API。

Soup Sieve 高级特性

Soup Sieve 提供的是比 select()select_one() 更底层的方法,通过 Tag 或 Beautiful Soup 对象的 .css 属性,可以调用大部分的 API。下面是支持这种调用方式的方法列表, 查看 Soup Sieve 文档了解全部细节。

iselect() 方法与 select() 效果相同,区别是返回的结果是迭代器。

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

closest() 方法与 find_parent() 方法相似,返回符合 CSS 选择器的 Tag 对象的最近父级。

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() 方法返回布尔结果,标记指定 Tag 是否符合指定筛选器

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

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

filter() 方法返回 tag 直接子节点中符合筛选器的节点列表

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

escape() 方法可以对 CSS 标识符中的特殊字符进行转义,否则是非法 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 的名称和属性

Tag.attrs 的章节中已经介绍过这个功能,但是再看一遍也无妨。重命名一个 tag, 改变属性的值,添加或删除属性

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

如果设置 tag 的 .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 原本包含了其它子节点,原有的所有内容包括子 tag 都会被覆盖掉。

append()

向 tag 中添加内容可以使用 Tag.append() 方法,就好像调用 Python 列表的 .append() 方法:

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

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

extend()

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

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

insert_before() 和 insert_after()

insert_before() 方法可以在文档树中直接在目标之前添加 tag 或文本

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() 方法可以在文档树中直接在目标之后添加 tag 或文本

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

clear()

Tag.clear() 方法可以移除 tag 的内容:

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>

extract()

PageElement.extract() 方法将当前 tag 或文本从文档树中移除,并返回被删除的内容:

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

这个方法实际上产生了 2 个文档树: 一个是原始文档的 BeautifulSoup 对象, 另一个是被移除并且返回的文档树。还可以在新生成的文档树上继续调用 extract 方法:

my_string = i_tag.string.extract()
my_string
# '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, 'html.parser')
a_tag = soup.a
i_tag = soup.i

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

被 decompose 的 Tag 或者 NavigableString 是不稳定的,什么时候都不要使用它。如果不确定 某些内容是否被 decompose 了,可以通过 .decomposed 属性进行检查 (Beautiful Soup 4.9.0 新增)

i_tag.decomposed
# True

a_tag.decomposed
# False

replace_with()

PageElement.replace_with() 方法移除文档树中的某段内容,并用新 tag 或文本节点替代它:

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() 方法返回被替代的 tag 或文本节点,可以用来检查或添加到文档树其它地方。

传递多个参数给 replace_with() 方法在 Beautiful Soup 4.10.0 版本中新增

wrap()

PageElement.wrap() 方法可以对指定的tag元素进行包装 [8] ,并返回包装后的结果:

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 中添加。

unwrap()

Tag.unwrap() 方法与 wrap() 方法相反。它将用 tag 内内容来替换 tag 本身, 该方法常被用来解包内容:

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() 方法会返回被移除的 tag。

smooth()

调用了一堆修改文档树的方法后,可能剩下的是 2 个或更多个彼此衔接的 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 编码输出, 每个 XML/HTML 标签都独占一行

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 对象 使用 Python 的 unicode()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() 方法获得字节码或调用 decode() 方法获得Unicode。

输出格式

Beautiful Soup 输出是会将 HTML 中的特殊字符编码转换成 Unicode, 比如 “&lquot;”:

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(), 或 decode() 方法的 formatter 参数。Beautiful Soup 可以识别 5 种 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 风格的 boolean 属性

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 在输出时不会修改任何字符串内容。这是效率最高的选项, 但可能导致输出非法的 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 的 formatter 实例, 然后用作 formatter 参数。

class bs4.HTMLFormatter

可以用来自定义 HTML 文档的格式化规则。

下面的 formatter 例子,可以将字符串全部转化为大写,不论是文字节点中的字符还是属性值

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 例子,在美化文档时增加缩进长度

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

可以用来自定义 XML 文档的格式化规则。

编写自定义 formatter

HTMLFormatter or XMLFormatter 的子类可以控制更多的输出过程。 例如,Beautiful Soup 默认情况下会对属性中的 tag 进行排序

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

如果只想得到 tag 中包含的文本内容,那么可以调用 get_text() 方法,这个方法获取到 tag 包含的所有文本内容,包括子孙 tag 中的可读内容,并将结果作为单独的一个 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''

可以通过参数指定 tag 的文本内容的连接符:

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

还可以去除每一个文本片段内容的前后空白:

# 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 属性,结果会返回对象本身或空,这种用法只有在对混合类型列表迭代时才会用到。

指定文档解析器

如果仅是想要解析HTML文档,只需要创建 BeautifulSoup 对象时传入文档就可以了。Beautiful Soup 会自动选择一个解析器来解析文档。同时还可以使用额外参数,来指定文档解析器。

BeautifulSoup 第一个参数应该是要被解析的文档字符串或是文件句柄 – 待解析文件的句柄, 第二个参数用来标识怎样解析文档。

如果不指定解析器,默认使用已安装的 最佳 HTML 解析器。Beautiful Soup 把 lxml 解析器排在第一, 然后是 html5lib, 然后是 Python 标准库。在下面两种条件下解析器优先顺序会变化:

  • 要解析的文档是什么类型: 目前支持, “html”,“xml”,和 “html5”

  • 指定使用哪种解析器: 目前支持,“lxml”,“html5lib”,和 “html.parser”(Python 标准库)

安装解析器 章节介绍了可以使用哪种解析器,以及如何安装。

如果指定的解析器没有安装,Beautiful Soup会自动选择其它方案。目前只有 lxml 解析器支持XML文档的解析, 在没有安装 lxml 库的情况下,无法自动选择 XML 文档解析器,手动指定 lxml 也不行。

解析器之间的区别

Beautiful Soup 为不同的解析器提供了相同的接口,但解析器本身时有区别的。同一篇文档被不同的解析器解析后 可能会生成不同结构的文档。区别最大的是 HTML 解析器和 XML 解析器,看下面片段被解析成 HTML 结构:

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

因为空标签 <b /> 不符合 HTML 标准,html.parser 解析器把它解析成一对儿 <b></b>。

同样的文档使用 XML 解析结果如下(解析 XML 需要安装 lxml 库)。注意,空标签 <b /> 依然被保留, 并且文档前添加了 XML 头,而不是被包含在 <html> 标签内:

print(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, 'html.parser')
soup.h1
# <h1>Sacré bleu!</h1>
soup.h1.string
# '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, '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 编码,但不知道具体编码。可以先自己猜测,猜测错误(依旧是乱码)时, 可以把错误编码作为 exclude_encodings 参数,这样文档就不会尝试使用这种编码了解码了。

译者备注: 在没有指定编码的情况下,BS会自己猜测编码,把不正确的编码排除掉,BS就更容易猜到正确编码。

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

猜测的结果 Windows-1255 可能不是 100% 准确,但是 Windows-1255 编码是 ISO-8859-8 的扩展集, 所以猜测结果已经十分接近了,并不影响使用。(exclude_encodings 参数是 4.4.0版本的新功能)

少数情况下(通常是UTF-8编码的文档中包含了其它编码格式的文件),想获得正确的 Unicode 编码就不得不将 文档中少数特殊编码字符替换成特殊 Unicode 编码,“REPLACEMENT CHARACTER” (U+FFFD, �) [9] 。 如果 Beautifu Soup 猜测文档编码时作了特殊字符的替换,那么 Beautiful Soup 会把 UnicodeDammitBeautifulSoup 对象的 .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, '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 的字符串 调用 encode() 方法一样:

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 编码中可以正常显示(看上去是 ☃),但有些编码不支持 SNOWMAN 字符,比如 ISO-Latin-1 或 ASCII,那么在这些编码中 SNOWMAN 字符会被转换成 “&#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, Dammit

译者备注: Unicode Dammit 是 Beautiful Soup 内置库,主要用来猜测文档编码。

编码自动检测 功能可以在 Beautiful Soup 以外使用。当遇到一段未知编码 的文档时,可以通过下面方法把它转换为 Unicode 编码

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 的 chardetcchardet 库,那么编码检测功能的准确率将大大提高。 输入的字符越多,检测结果越准确,如果事先猜测到一些可能编码,那么可以将猜测的编码作为参数, 这样将优先检测这些编码:

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
# '<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>'

也可以把引号转换为 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 把引号转换成 Unicode:

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

矛盾的编码

有时文档的大部分都是用 UTF-8,但同时还包含了 Windows-1252 编码的字符,就像微软的智能引号 [10] 一样。 一些包含多个信息的来源网站容易出现这种情况。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")

这段文档很杂乱,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() 方法会把这段字符串转换成 UTF-8 编码,允许我们同时显示出文档中的 snowmen 和引号:

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

UnicodeDammit.detwingle() 方法只能解码包含在 UTF-8 编码中的 Windows-1252 编码内容, (反过来的话,大概也可以)但这是最常见的用法。

在创建 BeautifulSoupUnicodeDammit 对象前一定要先对文档调用 UnicodeDammit.detwingle() 确保文档的编码方式正确。Beautiful Soup 会假设文档只包含一种编码,如果尝试去解析一段同时包含 UTF-8 和 Windows-1252 编码的文档, 就有可能被误判成整个文档都是 Windows-1252 编码,解析结果就会得到一堆乱码, 比如: ☃☃☃“I like snowmen!”。

UnicodeDammit.detwingle() 方法在 Beautiful Soup 4.1.0 版本中新增。

行编号

html.parserhtml5lib 解析器可以跟踪原始文档中发现的每个 Tag。查看原始信息可以 使用 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')

注意,这两个解析器的 sourcelinesourcepos 会有些许的不同。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')

可以在 BeautifulSoup 构造函数中配置 store_line_numbers=False 来关闭这个功能

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 解析器不支持这个功能。

比较对象是否相同

两个 NavigableStringTag 对象具有相同的 HTML 或 XML 结构时, Beautiful Soup就判断这两个对象相同。这个例子中,2个 <b> 标签在 BS 中是相同的, 尽管他们在文档树的不同位置,但是具有相同的表象: “<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

如果想判断两个对象是否严格的指向同一个对象可以通过 is 来判断

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

源对象和复制对象的区别是源对象在文档树中,而复制后的对象是独立的还没有添加到文档树中。 复制后对象的效果跟调用了 extract() 方法相同。

print p_copy.parent
# None

这是因为相等的对象不能同时插入相同的位置

高级自定义解析

Beautiful Soup 提供多种途径自定义解析器如果解析 HTML 和 XML。本章覆盖了最常用的自定义方法。

解析部分文档

如果仅仅因为想要查找文档中的 <a> 标签而将整片文档进行解析,实在是浪费内存和时间。最快的方法 是从一开始 就把 <a> 标签以外的东西都忽略掉。 SoupStrainer 类可以选择解析哪部分文档内容, 创建一个 SoupStrainer 对象并作为 parse_only 参数给 BeautifulSoup 的构造 方法即可。

(注意,这个功能在 html5lib 解析器中无法使用。如果使用 html5lib 解析器,整篇文档都会被解析, 这是因为 html5lib 会重新排列文档树的结构,如果部分节点不在文档树中,会导致崩溃。为了避免混淆, 下面的例子中 Beautiful Soup 都强制指定使用了 Python 内置解析器。)

SoupStrainer

SoupStrainer 类接受与典型搜索方法相同的参数: name , attrs , recursive , string , **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 作为参数传入 搜索文档树 中提到的方法。虽然不常用,但还是提一下:

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

自定义包含多个值的属性

在 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 在 tag 中发现重复的属性名字时如何处理

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 到底怎样处理一份文档,可以将文档传入 diagnose() 方法(Beautiful Soup 4.2.0中新增), 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:
# ...

diagnose() 方法的输出结果可能帮助你找到问题的原因,如果不行,还可以把结果复制出来以 便寻求他人的帮助。

文档解析错误

文档解析错误有两种。一种是崩溃,Beautiful Soup 尝试解析一段文档结果却抛除了异常,通常是 HTMLParser.HTMLParseError 。还有一种异常情况,是Beautiful Soup 解析后的文档树 看起来与原来的内容相差很多。

这些错误几乎都不是 Beautiful Soup 的原因,这不是因为 Beautiful Soup 的代码写得多优秀, 而是因为 Beautiful Soup 没有包含任何文档解析代码。异常产生自被依赖的解析器,如果解析器不能 很好的解析出当前的文档,那么最好的办法是换一个解析器。更多细节查看 安装解析器 章节。

最常见的解析错误是 HTMLParser.HTMLParseError: malformed start tagHTMLParser.HTMLParseError: bad end tag 。这都是由Python内置的解析器引起的, 解决方法是 安装 lxml 或 html5lib

最常见的非预期行为是发现不了一个确定存在稳当中的 Tag。光是用眼睛就能轻易发现,但用 find_all() 方法返回 [] ,用 find() 方法返回 None 。这是 Python 内置解析器的又一个问题: 解析器会跳过那些 它不知道的 tag。解决方法还是 安装 lxml 或 html5lib

版本错误

  • SyntaxError: Invalid syntax (异常位置在代码行: ROOT_TAG_NAME = u'[document]' ), 原因是用 Python2 版本的 Beautiful Soup 未经过代码转换,直接在 Python3 中运行。

  • ImportError: No module named HTMLParser 因为在 Python3 中执行 Python2 版本的 Beautiful Soup。

  • ImportError: No module named html.parser 因为在 Python2 中执行 Python3 版本的 Beautiful Soup

  • ImportError: No module named BeautifulSoup 因为在没有安装 Beautiful Soup3 库的 Python 环境下执行代码, 或忘记了 Beautiful Soup4 的代码需要从 bs4 包中引入。

  • ImportError: No module named bs4 因为当前 Python 环境下还没有安装 Beautiful Soup4。

解析成XML

默认情况下,Beautiful Soup 会将当前文档作为 HTML 格式解析,如果要解析 XML 文档,要在 BeautifulSoup 构造方法中加入第二个参数 “xml”:

soup = BeautifulSoup(markup, "xml")

当然,还需要 安装 lxml

其它解析器的错误

  • 如果同样的代码在不同环境下结果不同,可能是因为两个环境下使用不同的解析器造成的。 例如这个环境中安装了 lxml,而另一个环境中只有 html5lib, 解析器之间的区别 中说明了原因。 修复方法是在 BeautifulSoup 的构造方法中中指定解析器。

  • 因为HTML标签是 大小写敏感 的, 所以解析器会将 tag 和属性都转换成小写。例如文档中的 <TAG></TAG> 会被转换为 <tag></tag> 。 如果想要保留 tag 的大写的话,那么应该将文档 解析成XML

杂项错误

  • UnicodeEncodeError: 'charmap' codec can't encode character '\xfoo' in position bar (或其它类型的 UnicodeEncodeError )的错误, 主要是两方面的原因,第一种是正在使用的终端(console)无法显示部分Unicode,参考 Python wiki ,第二种是向文件 写入时,被写入文件不支持部分 Unicode,这时需要用 u.encode("utf8") 方法将 编码转换为UTF-8。

  • KeyError: [attr] 因为调用 tag['attr'] 方法而引起,因为这个 tag 没有定义 该属性。出错最多的是 KeyError: 'href'KeyError: 'class' 。如果不确定 某个属性是否存在时,用 tag.get('attr') 方法去获取它,跟获取 Python 字典的 key 一样。

  • 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

  • AttributeError: 'NavigableString' object has no attribute 'foo' 这种问题 通常是因为吧一个字符串当做一个 tag 来处理。可能在迭代一个列表时,期望其中都是 tag,但实际上 列表里既包含 tag 也包含字符串。

如何提高效率

Beautiful Soup对文档的解析速度不会比它所依赖的解析器更快,如果对计算时间要求很高或者 计算机的时间比程序员的时间更值钱,那么就应该直接使用 lxml

换句话说,还有提高 Beautiful Soup 效率的办法,使用lxml作为解析器。Beautiful Soup 用 lxml 做解析器比用 html5lib 或 Python 内置解析器速度快很多。

安装 cchardet 后文档的解码的编码检测 速度会更快。

解析部分文档 不会节省多少解析时间,但是会节省很多内存,并且搜索时也会变得更快。

翻译这篇文档

非常感谢欢迎翻译 Beautiful Soup 的文档。翻译内容应当基于 MIT 协议,就像 Beautiful Soup 和 英文文档一样。

有两种方式将翻译内容添加到 Beautiful Soup 的网站上:

  1. 在 Beautiful Soup 代码库上创建一个分支,添加翻译,然后合并到主分支。就像修改源代码一样。

  2. 向 Beautiful Soup 讨论组里发送一个消息,带上翻译的链接,或翻译内容的附件。

使用中文或葡萄牙语的翻译作为模型。尤其注意,请翻译源文件 doc/source/index.rst, 而不是 HTML 版本的文档。这样才能将文档发布为多种格式,而不局限于 HTML。

Beautiful Soup 3

Beautiful Soup 3 是上一个发布版本,目前已经停止维护。Beautiful Soup 3 库目前已经被 几个主要的 linux 发行版添加到源里:

$ apt-get install Python-beautifulsoup

在 PyPi 中分发的包名字是 BeautifulSoup :

$ easy_install BeautifulSoup

$ pip install BeautifulSoup

或通过 Beautiful Soup 3.2.0源码包 安装。

如果是通过 easy_install beautifulsoupeasy_install BeautifulSoup 安装,然后代码无法运行,那么可能是安装了错误的 Beautiful Soup 3 版本。 应该这样安装 easy_install beautifulsoup4

Beautiful Soup 3 的在线文档查看 这里 .

迁移到 BS4

大部分使用 Beautiful Soup 3 编写的代码都可以在 Beautiful Soup 4 上运行, 只有一个小变动。只要把引用包的名字从 BeautifulSoup 改为 bs4,比如:

from BeautifulSoup import BeautifulSoup

修改为:

from bs4 import BeautifulSoup
  • 如果代码抛出 ImportError 异常 “No module named BeautifulSoup”, 原因可能是尝试执行 Beautiful Soup 3,但环境中只安装了 Beautiful Soup 4。

  • 如果代码抛出 ImportError 异常 “No module named bs4”,原因可能是尝试 运行 Beautiful Soup 4 的代码,但环境中只安装了Beautiful Soup 3。

尽管 BS4 兼容绝大部分 BS3 的功能,但 BS3 中的大部分方法已经不推荐使用了,旧方法标记废弃, 并按照 PEP8标准 重新命名了新方法。 虽然有大量的重命名和修改,但只有少数几个方法没有向下兼容。

下面内容就是 BS3 迁移到 BS4 的注意事项:

解析器的变化

Beautiful Soup 3 曾使用 Python 的 SGMLParser 解析器,这个模块在 Python3 中已经被移除了。Beautiful Soup 4 默认使用系统的 html.parser , 也可以使用 lxml 或 html5lib 扩展库代替。查看 安装解析器 章节。

因为解析器 html.parserSGMLParser 不同。BS4 和 BS3 处理相同的文档会 产生不同的对象结构。使用 lxml 或 html5lib 解析文档的时候,如果添加了 html.parser 参数,解析的对象又会发生变化。如果发生了这种情况,只能修改对应的文档处理代码了。

方法名的变化

  • renderContents -> encode_contents

  • replaceWith -> replace_with

  • replaceWithChildren -> unwrap

  • findAll -> find_all

  • findAllNext -> find_all_next

  • findAllPrevious -> find_all_previous

  • findNext -> find_next

  • findNextSibling -> find_next_sibling

  • findNextSiblings -> find_next_siblings

  • findParent -> find_parent

  • findParents -> find_parents

  • findPrevious -> find_previous

  • findPreviousSibling -> find_previous_sibling

  • findPreviousSiblings -> find_previous_siblings

  • nextSibling -> next_sibling

  • previousSibling -> previous_sibling

Beautiful Soup 构造方法的参数部分也有名字变化:

  • BeautifulSoup(parseOnlyThese=...) -> BeautifulSoup(parse_only=...)

  • BeautifulSoup(fromEncoding=...) -> BeautifulSoup(from_encoding=...)

为了适配 Python3,修改了一个方法名:

  • Tag.has_key() -> Tag.has_attr()

修改了一个属性名,让它看起来更专业点:

  • Tag.isSelfClosing -> Tag.is_empty_element

修改了下面 3 个属性的名字,以免与 Python 保留字冲突。这些变动不是向下兼容的,如果在 BS3 中使用了这些属性,那么在 BS4 中这些代码无法执行。

  • UnicodeDammit.Unicode -> UnicodeDammit.Unicode_markup

  • Tag.next -> Tag.next_element

  • Tag.previous -> Tag.previous_element

生成器

将下列生成器按照 PEP8 标准重新命名,并转换成对象的属性:

  • childGenerator() -> children

  • nextGenerator() -> next_elements

  • nextSiblingGenerator() -> next_siblings

  • previousGenerator() -> previous_elements

  • previousSiblingGenerator() -> previous_siblings

  • recursiveChildGenerator() -> descendants

  • parentGenerator() -> parents

所以要把这样的代码:

for parent in tag.parentGenerator():
    ...

替换为:

for parent in tag.parents:
    ...

(其实老方法也可以继续使用)

有的生成器循环结束后会返回 None 然后结束。这是个 bug。新版生成器不再返回 None

BS4 中增加了 2 个新的生成器, .strings 和 stripped_strings.strings 生成器 返回 NavigableString 对象, .stripped_strings 方法返回去除前后空白的 Python 的 string 对象。

XML

BS4 中移除了解析 XML 的 BeautifulStoneSoup 类。如果要解析一段 XML 文档,使用 BeautifulSoup 构造方法并在第二个参数设置为“xml”。同时 BeautifulSoup 构造 方法也不再识别 isHTML 参数。

Beautiful Soup 处理 XML 空标签的方法升级了。旧版本中解析 XML 时必须指明哪个标签是空标签。 构造方法的 selfClosingTags 参数已经不再使用。新版 Beautiful Soup 将所有空标签解析 为空元素,如果向空元素中添加子节点,那么这个元素就不再是空元素了。

实体

输入的 HTML 或 XML 实体都会被解析成 Unicode 字符。Beautiful Soup 3 版本中有很多相似处理 实体的方法,在新版中都被移除了。 BeautifulSoup 构造方法也不再接受 smartQuotesToconvertEntities 参数。 编码自动检测 方法依然有 smart_quotes_to 参数,但是默认会将引号转换成 Unicode。内容配置项 HTML_ENTITIES , XML_ENTITIESXHTML_ENTITIES 在新版中被移除。因为它们代表的特性(转换部分而不是全部实体到 Unicode 字符) 已经不再支持。

如果在输出文档时想把 Unicode 字符转回 HTML 实体,而不是输出成 UTF-8 编码,那就需要用到 输出格式 的方法。

迁移杂项

Tag.string 属性现在是一个递归操作。如果 A 标签只包含了一个 B 标签,那么 A 标签的。 string 属性值与 B 标签的 string 属性值相同。

多值属性 比如 class 属性包含一个他们的值的列表,而不是一个字符串。这可能会影响到如何按照 CSS 类名哦搜索 tag。

Tag 对象实现了一个 __hash__ 方法,这样当两个 Tag 对象生成相同的摘要时会被认为相等。这可能会 改变你的脚本行为,如果你吧 Tag 对象放到字典或集合中。

如果使用 find* 方法时同时传入了 string 参数 和一个指定 tag 的参数比如 name 参数 。 Beautiful Soup 会搜索符合指定参数的 tag,并且这个 tag 的 Tag.string 属性包含 string 参数 参数的内容。结果中 不会 包含字符串本身。旧版本中 Beautiful Soup 会忽略掉指定 tag 的参数,只搜索符合 string 的内容。

BeautifulSoup 构造方法不再支持 markupMassage 参数。现在由解析器负责文档的解析正确性。

很少被用到的几个解析器方法在新版中被移除,比如 ICantBelieveItsBeautifulSoupBeautifulSOAP 。 现在由解析器完全负责如何解释模糊不清的文档标记。

prettify() 方法在新版中返回 Unicode 字符串,不再返回字节串。

附录