原文出处 -> Beautiful Soup Documentation
目录
5.4 Comments and other special strings(注释及特殊字符串)
6.1.5 .strings 和 stripped_strings
6.3.1 .next_sibling 和 .previous_sibling
6.3.2 .next_siblings 和 .previous_siblings
6.4.1 .next_element 和 .previous_element
6.4.2 .next_elements 和 .previous_elements
7.5 find_parents() 和 find_parent()
7.6 find_next_siblings() 和 find_next_sibling()
7.7 find_previous_siblings() 和 find_previous_sibling()
7.8 find_all_next() 和 find_next()
7.9 find_all_previous() 和 find_previous()
8.5 NevigableString() 和 .new_tag()
8.7 insert_before() 和 insert_after()
一、前言
BeautifulSoup 其实官方已经有了比较完善的中文文档,但对初学者可能不是特别友好,所以这里试图通过“添盐加醋”的方式给大家二次解读该文档。
BeautifulSoup 是一个可以从 HTML 和 XML 文件中提取数据的 Python 库。
它可以通过你最喜欢的解析器实现遍历、查找和修改网页数据的功能。
使用 BeautifulSoup 进行工作,至少可以帮你节省数小时甚至是数天的时间。
这篇文档介绍了 BeautifulSoup4 中的所有主要特性,并附有生动的小例子。
在这里我(作者,下同)准备告诉你这个库擅长做什么工作,它的原理是怎样的,以及如何使用它……
反正你看完这篇文档,就可以做到人汤合一的境界(BeautifulSoup 直译过来就是美妙的汤)。
文档中出现的例子在 Python2.7 和 Python3.x 中的执行结果相同。
呃……你可能还在找 BeautifulSoup3 的文档,不够很遗憾,那个已经过时了,我们推荐你在现有的项目中使用 BeautifulSoup4,参考 移植到 BS4 章节内容。
1.1 寻求帮助
如果你有关于BeautifulSoup的问题,可以发送邮件到 讨论组 .如果你的问题包含了一段需要转换的HTML代码,那么确保你提的问题描述中附带这段HTML文档的 代码诊断 [1]
二、快速开始
下面的一段 HTML 将作为例子在本文中被多次引用。这是《爱丽丝梦游仙境》的一段内容:
>>> html_doc = """<html><head><title>睡鼠的故事</title></head>
<body>
<p class="title"><b>睡鼠的故事</b></p>
<p class="story">从前有三位小姐姐,她们的名字是:
<a href="http://example.com/elsie" class="sister" id="link1">埃尔西</a>,
<a href="http://example.com/lacie" class="sister" id="link2">莱斯</a>和
<a href="http://example.com/tillie" class="sister" id="link3">蒂尔莉</a>;
她们住在一个井底下面。</p>
<p class="story">...</p>
"""
使用 BeautifulSoup 解析上面文档,从而够得到一个 BeautifulSoup 对象,它能够按照文档的嵌套结构输出:
>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup(html_doc, "html.parser")
>>> print(soup.prettify())
<html>
<head>
<title>
睡鼠的故事
</title>
</head>
<body>
<p class="title">
<b>
睡鼠的故事
</b>
</p>
<p class="story">
从前有三位小姐姐,她们的名字是:
<a class="sister" href="http://example.com/elsie" id="link1">
埃尔西
</a>
,
<a class="sister" href="http://example.com/lacie" id="link2">
莱斯
</a>
和
<a class="sister" href="http://example.com/tillie" id="link3">
蒂尔莉
</a>
;
她们住在一个井底下面。
</p>
<p class="story">
...
</p>
</body>
</html>
下面是几种简单地遍历结构化数据的方法:
>>> soup.title
<title>睡鼠的故事</title>
>>> soup.title.name
'title'
>>> soup.title.string
'睡鼠的故事'
>>> soup.title.parent.name
'head'
>>> soup.p
<p class="title"><b>睡鼠的故事</b></p>
>>> soup.p['class']
['title']
>>> soup.a
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>
>>> soup.find_all('a')
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
>>> soup.find(id='link3')
<a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</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())
睡鼠的故事
睡鼠的故事
从前有三位小姐姐,她们的名字是:
埃尔西,
莱斯和
蒂尔莉;
她们住在一个井底下面。
...
这是你想要的操作吗?如果是,请继续往下学习……
三、安装 BeautifulSoup
如果你使用的是新版的 Debain 或 ubuntu,那么可以通过系统的软件包管理来进行安装:
$ apt-get install Python-bs4
BeautifulSoup4 通过 PyPi 发布,所以如果你无法使用系统包管理安装,那么也可以通过 easy_install 或 pip 来进行安装。包的名字是 beautifulsoup4,这个包同时兼容 Python2.x 和 Python3.x。
请确保使用与 Python 版本相匹配的 pip 或 easy_install 命令(如果你使用的是 Python3,那么应该使用 pip3 和 easy_install3 命令)。
$ easy_install beautifulsoup4
$ pip install beautifulsoup4
(注意:在 PyPi 中还有一个名字是 BeautifulSoup 的包,但那个是 BeautifulSoup3 的发布版本,因为很多旧的项目还在使用它,所以 BeautifulSoup 包依然有效……但是如果你在编写新项目,那么请应该安装的 beautifulsoup4 )
如果你没有安装 easy_install 或 pip,那你也可以下载 BS4 的源码,然后通过 setup.py 来安装。
$ Python setup.py install
如果上述安装方法都行不通,BeautifulSoup 的发布协议还允许你将 BS4 的代码打包在你的项目中,这样无须安装即可使用。你可以下载压缩包,并将 BS4 目录拷贝到你的应用程序代码库,这样就可以在不安装的情况下直接使用 BeautifulSoup 了。
我在 Python2.7 和 Python3.2 的版本下开发 BeautifulSoup, 理论上 Beautiful Soup 在所有当前的 Python 版本中均可正常工作(在本文档中的所有案例是使用 Python3.6 演示的)。
3.1 安装完成后的问题
BeautifulSoup 包是以 Python2 代码的形式打包的,在 Python3 环境下安装时,会自动转换成 Python3 的代码,如果没有一个安装的过程,那么代码就不会被转换。还有一些是在 Windows 操作系统上安装了错误版本的报告。
如果代码抛出了 ImportError 的异常:"No module named HTMLParser",这是因为你在 Python3 版本中执行 Python2 版本的代码。
如果代码抛出了 ImportError 的异常:"No module named html.parser",这是因为你在 Python2 版本中执行 Python3 版本的代码。
如果遇到上述 2 种情况,最好的解决方法是先完全卸载 BeautifulSoup(包括解压时创建的任何文件夹)后再重新安装 BeautifulSoup4。
如果在 ROOT_TAG_NAME = u'[document]' 代码处遇到 SyntaxError "Invalid syntax" 错误,则需要将 BS4 的 Python 代码版本从 Python2 转换到 Python3,你可以通过安装包来实现这一步:
$ Python3 setup.py install
或者在 bs4 目录中(Python\Python36\Lib\site-packages\bs4)执行 Python 代码版本转换代码 2to3:
$ 2to3-3.2 -w bs4
3.2 安装解析器
BeautifulSoup 支持 Python 标准库中的 HTML 解析器,还支持一些第三方的解析器,lxml 就是其中比较火的一个。
下面提供了各种不同操作系统安装 lxml 的方法:
$ apt-get install python-lxml
$ easy_install lxml
$ pip install lxml
另一个可供选择的解析器是纯 Python 实现的 html5lib,html5lib 的解析 HTML 的方式与浏览器相同,可以选择下列方法来安装 html5lib:
$ apt-get install python-html5lib
$ easy_install html5lib
$ pip install html5lib
下表总结了各个主流解析器的优缺点:
解析器 |
典型用法 |
优点 |
缺点 |
Python 的 html.parser | BeautifulSoup(markup, "html.parser") |
|
|
lxml 的 HTML parser | BeautifulSoup(markup, "lxml") |
|
|
lxml 的 XML parser | BeautifulSoup(markup, ["lxml", "xml"]) BeautifulSoup(markup, "xml") |
|
|
html5lib | BeautifulSoup(markup, "html5lib") |
|
|
从效率层面考虑,推荐安装和使用 lxml 解析器。如果是在 Python2.7.3 或 Python3.2.2 之前的版本,必须安装 lxml 或 html5lib,因为那些 Python 版本的标准库中内置的 HTML 解析方法还不够稳定。
注意:如果一个文档的格式不合法的话,那么在不同的解析器中返回的结果可能是不一样的,具体请查看 解析器之间的区别 章节内容。
四、如何使用
解析一个文档,只需要将其传递给 BeautifulSoup 构造方法。你可以传递一个字符串或者是一个文件句柄。
from bs4 import BeautifulSoup
with open("index.html") as fp:
soup = BeautifulSoup(fp)
soup = BeautifulSoup("<html>data</html>")
首先,文档将被转换成 Unicode 编码,并且 HTML 实体也都被转换成 Unicode 字符:
>>> BeautifulSoup("Sacré bleu!")
Sacré bleu!
然后,BeautifulSoup 选择最合适的解析器来解析这段文档,如果要解析一个 XML 文档,那么需要手动指定 XML 解析器(soup = BeautifulSoup(markup, "xml")),否则它仍然会尝试使用 HTML 解析器。
五、对象的种类
BeautifulSoup 将复杂 HTML 文档转换成一个同样复杂的树形结构,每个节点都是 Python 对象,所有对象可以归纳为 4 种:Tag,NavigableString,BeautifulSoup 和 Comment。
5.1 Tag(标签)
注:tag 在本文中即 “标签”,两者同义,在下文中将交替使用。
一个 Tag 对象对应一个 XML 或 HTML 原生文档中的标签:
>>> soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
>>> tag = soup.b
>>> type(tag)
<class 'bs4.element.Tag'>
标签有很多属性和方法,在 遍历文档树 和 搜索文档树 章节中均有详细解释。
现在,让我们先介绍一下 tag 中最重要的特征:name(标签名)和 attributes(属性)。
5.1.1 Name(标签名)
每个标签都有一个名字,通过 .name 来获取:
>>> tag.name
'b'
如果改变了标签的名称,那么将影响所有通过当前 BeautifulSoup 对象生成的 HTML 文档:
>>> tag.name = "blockquote"
>>> tag
<blockquote class="boldest">Extremely bold</blockquote>
5.1.2 Attributes(属性)
一个标签可以有很多个属性。
比如标签 <b id="boldest"> 有一个叫“id”的属性,它的值为“boldest”。
标签访问属性方法与字典相同:
>>>tag['id']
['boldest']
也可以使用 .attrs 直接访问字典:
>>> tag.attrs
{'id': ['boldest']}
标签的属性可以被添加、删除或修改。再强调一次,标签的属性操作方法与 Python 字典是一样的!
>>> tag['class'] = 'verybold'
>>> tag['id'] = 1
>>> tag
<blockquote class="verybold" id="1">Extremely bold</blockquote>
>>> del tag['class']
>>> del tag['id']
>>> tag['class']
Traceback (most recent call last):
File "<pyshell#40>", line 1, in <module>
tag['class']
File "C:\Users\goodb\AppData\Local\Programs\Python\Python36\lib\site-packages\bs4\element.py", line 1011, in __getitem__
return self.attrs[key]
KeyError: 'class'
>>> print(tag.get('class'))
None
Multi-valued attributes(多值属性)
HTML4 定义了一些可以包含多个值的属性,在 HTML5 中略有增删。其中最常见的多值属性是 class(一个 tag 可以有多个 CSS 的 class)。还有一些属性像 rel,rev,accept-charset,headers 和 accesskey。在 BeautifulSoup 中,是以列表的形式来处理多值属性的:
>>> css_soup = BeautifulSoup('<p class="body"></p>')
>>> css_soup.p['class']
['body']
>>> css_soup = BeautifulSoup('<p class="body strikeout"></p>')
>>> css_soup.p['class']
['body', 'strikeout']
如果某个属性看起来存在多个值,但在 HTML 的定义中却不是一个多值属性,那么 BeautifulSoup 会将其作为字符串返回:
>>> id_soup = BeautifulSoup('<p id="My id"></p>')
>>> id_soup.p['id']
'My id'
将标签转换成字符串时,多值属性会合并为一个值:
>>> rel_soup = BeautifulSoup('<p>Back to the <a rel="index">homepage</a></p>')
>>> rel_soup.a['rel']
['index']
>>> rel_soup.a['rel'] = ['index', 'contents']
>>> print(rel_soup.p)
<p>Back to the <a rel="index contents">homepage</a></p>
你可以使用 get_attribute_list() 方法以列表形式获取一个属性值:如果它是多值属性,那么列表中存在多个字符串;否则列表中就只有一个字符串。
>>> id_soup.p.get_attribute_list('id')
['my id']
如果解析的文档是 XML 格式,那么 tag 中不包含多值属性:
>>> xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml')
>>> xml_soup.p['class']
'body strikeout'
5.2 NavigableString(可以遍历的字符串)
字符串对应的是标签内部包含的文本。BeautifulSoup 使用 NavigableString 类来包装这些文本:
>>> 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)
<class 'str'>
标签中包含的字符串不能被编辑,但是可以被替换成其它字符串,用 replace_with() 方法:
>>> tag.string.replace_with("No longer bold")
'Extremely bold'
>>> tag
<blockquote>No longer bold</blockquote>
NavigableString 对象支持 遍历文档树 和 搜索文档树 中定义的大部分属性,但不是全部哦~尤其是,一个字符串不能包含其它内容(标签能够包含字符串或是其它标签),字符串不支持 .contents 或 .string 属性或 find() 方法。
如果想在 BeautifulSoup 之外使用 NavigableString 对象,需要调用 str() 方法,将该对象转换成普通的 Unicode 字符串。否则,就算 BeautifulSoup 已经执行结束,该对象也会带有整个 BeautifulSoup 解析树的引用地址,这样会造成内存的巨大浪费。
5.3 BeautifulSoup
BeautifulSoup 对象表示的是一个文档的全部内容。大部分时候,可以把它当作 Tag 对象,它支持 遍历文档树 和 搜索文档树 中描述的大部分的方法。
因为 BeautifulSoup 对象并不是真正的 HTML 或 XML 标签,所以它没有 name 和 attributes。但有时查看它的 .name 属性是很方便的,所以 BeautifulSoup 对象包含了一个值为 "[document]" 的特殊属性 .name:
>>> soup.name
'[document]'
5.4 Comments and other special strings(注释及特殊字符串)
Tag,NavigableString 和 BeautifulSoup 几乎涵盖了 HTML 或 XML 文档中的所有内容,但不包括注释。
>>> markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
>>> soup = BeautifulSoup(markup)
>>> comment = soup.b.string
>>> type(comment)
<class 'bs4.element.Comment'>
Comment 对象是一个特殊类型的 NavigableString 对象:
>>> comment
'Hey, buddy. Want to buy a used parser?'
但是当它出现在 HTML 文档中时,Comment 对象会使用特殊的格式输出:
>>> print(soup.b.prettify())
<b>
<!--Hey, buddy. Want to buy a used parser?-->
</b>
BeautifulSoup 中定义的其它类都可能会出现在 XML 的文档中:CData,ProcessingInstruction,Declaration 和 Doctype。与 Comment 对象类似,这些类都是 NavigableString 的子类,只是添加了一些额外方法的字符串。下面是用 CDATA 来替代注释的例子:
>>> from bs4 import CData
>>> cdata = CData("A CDATA block")
>>> comment.replace_with(cdata)
'Hey, buddy. Want to buy a used parser?'
>>> print(soup.b.prettify())
<b>
<![CDATA[A CDATA block]]>
</b>
六、遍历文档树
我们还是拿《爱丽丝梦游仙境》的文档来做演示:
>>> html_doc = """<html><head><title>睡鼠的故事</title></head>
<body>
<p class="title"><b>睡鼠的故事</b></p>
<p class="story">从前有三位小姐姐,她们的名字是:
<a href="http://example.com/elsie" class="sister" id="link1">埃尔西</a>,
<a href="http://example.com/lacie" class="sister" id="link2">莱斯</a>和
<a href="http://example.com/tillie" class="sister" id="link3">蒂尔莉</a>;
她们住在一个井底下面。</p>
<p class="story">...</p>
"""
>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup(html_doc, 'html.parser')
下面我将给大家演示如何从文档的一个位置移动到另一个位置!
6.1 子节点(向下遍历)
标签可能包含字符串或其它标签,这些都是这个标签的子节点。BeautifulSoup 提供了许多不同的属性,用于遍历和迭代一个标签的子节点。
注意:BeautifulSoup 中的字符串节点是不支持这些属性的,因为字符串本身没有子节点。
6.1.1 使用标签名进行遍历
遍历解析树最简单的方法就是告诉它你想要获取的标签的名称。比如你想获取 <head> 标签,只要用 soup.head 即可:
>>> soup.head
<head><title>睡鼠的故事</title></head>
>>> soup.title
<title>睡鼠的故事</title>
你可以重复多次使用这个小技巧来深入解析树的某一个部分。下面代码获取 <body> 标签中的第一个 <b> 标签:
>>> soup.body.b
<b>睡鼠的故事</b>
使用标签名作为属性的方法只能获得当前名字后的第一个标签:
>>> soup.a
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>
如果想要获取所有的 <a> 标签,或者获取一些更复杂的东西时,就要用到在 查找文档树 章节中讲解的一个方法 —— find_all():
>>> soup.find_all('a')
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
6.1.2 .contents 和 .children
一个标签的子节点可以从一个叫 .contents 的列表中获得:
>>> head_tag = soup.head
>>> head_tag
<head><title>睡鼠的故事</title></head>
>>> head_tag.contents
[<title>睡鼠的故事</title>]
>>> title_tag = head_tag.contents[0]
>>> title_tag.contents
['睡鼠的故事']
BeautifulSoup 对象本身拥有子节点,也就是说 <html> 标签也是 BeautifulSoup 对象的子节点:
>>> len(soup.contents)
1
>>> soup.contents[0].name
'html'
字符串没有 .contents 属性,因此字符串没有子节点:
>>> text = title_tag.contents[0]
>>> text.contents
Traceback (most recent call last):
File "<pyshell#17>", line 1, in <module>
text.contents
File "C:\Users\goodb\AppData\Local\Programs\Python\Python36\lib\site-packages\bs4\element.py", line 737, in __getattr__
self.__class__.__name__, attr))
AttributeError: 'NavigableString' object has no attribute 'contents'
如果你不想通过 .contents 获取一个列表,还可以通过标签的 .children 属性得到一个生成器:
>>> for child in title_tag.children:
print(child)
睡鼠的故事
6.1.3 .descendants
.contents 和 .children 属性仅包含标签的直接子节点。例如 <head> 标签只有一个直接子节点 <title> 标签:
>>> head_tag.contents
[<title>睡鼠的故事</title>]
但是,<title> 标签自身也有一个子节点:字符串 "睡鼠的故事",这种情况下字符串 "睡鼠的故事" 也属于 <head> 标签的子孙节点。如果要对多层子孙节点进行递归迭代,可以使用 .descendants 属性完成任务:
>>> for child in head_tag.descendants:
print(child)
<title>睡鼠的故事</title>
睡鼠的故事
上面的例子中,<head> 标签只有一个子节点,但是有 2 个子孙节点:<head> 标签和 <head> 标签的子节点。BeautifulSoup 有一个直接子节点(<html> 标签),却有很多子孙节点:
>>> len(list(soup.children))
1
>>> len(list(soup.descendants))
26
6.1.4 .string
如果标签只有一个子节点,并且这个子节点是一个 NavigableString 对象,那么可以用 .string 将其获取:
>>> title_tag.string
'睡鼠的故事'
如果标签的唯一子节点是另一个标签,并且那个标签拥有 .string,那么父节点可以直接通过 .string 来访问其子孙节点的字符串:
>>> head_tag.contents
[<title>睡鼠的故事</title>]
>>> head_tag.string
'睡鼠的故事'
如果一个标签包含不止一个子节点,那么就不清楚 .string 应该指向谁了,所以此时 .string 的值是 None:
>>> print(soup.html.string)
None
6.1.5 .strings 和 stripped_strings
如果一个标签中不止一个子节点,你也是可以获取里面包含的字符串的(不止一个),需要使用 .strings 生成器:
>>> for string in soup.strings:
print(repr(string))
'睡鼠的故事'
'\n'
'\n'
'睡鼠的故事'
'\n'
'从前有三位小姐姐,她们的名字是:\n'
'埃尔西'
',\n'
'莱斯'
'和\n'
'蒂尔莉'
';\n她们住在一个井底下面。'
'\n'
'...'
'\n'
输出的这些字符串中可能包含了很多空格或空行,对我们来说一点用都没有……使用 .stripped_strings 可以去除多余空白:
>>> for string in soup.stripped_strings:
print(repr(string))
'睡鼠的故事'
'睡鼠的故事'
'从前有三位小姐姐,她们的名字是:'
'埃尔西'
','
'莱斯'
'和'
'蒂尔莉'
';\n她们住在一个井底下面。'
'...'
6.2 父节点(向上遍历)
我们继续以“家族树”作类比,每一个标签和字符串都有一个父节点:它们总是被包含在另外一个标签中。
6.2.1 .parent
你可以使用 .parent 属性访问一个元素的父节点。举个栗子,《爱丽丝梦游仙境》这个文档中,<head> 标签就是 <title> 标签的父节点:
>>> title_tag = soup.title
>>> title_tag
<title>睡鼠的故事</title>
>>> title_tag.parent
<head><title>睡鼠的故事</title></head>
字符串本身有一个父节点,就是包含它的 <title> 标签:
>>> title_tag.string.parent
<title>睡鼠的故事</title>
顶层节点比如 <html> 的父节点是 BeautifulSoup 对象本身:
>>> html_tag = soup.html
>>> type(html_tag.parent)
<class 'bs4.BeautifulSoup'>
BeautifulSoup 对象的 .parent 是 None:
>>> print(soup.parent)
None
6.2.2 .parents
你可以使用 .parents 迭代一个元素的所有父节点。下面例子使用了 .parents 属性遍历了 <a> 标签的所有父节点:
>>> link = soup.a
>>> link
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>
>>> for parent in link.parents:
if parent is None:
print(parent)
else:
print(parent.name)
p
body
html
[document]
6.3 兄弟节点(左右遍历)
大家请看一段简单的例子:
>>> sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>", "html.parser")
>>> print(sibling_soup.prettify())
<a>
<b>
text1
</b>
<c>
text2
</c>
</a>
<b> 标签和 <c> 标签在同一层:它们都是 <a> 标签的直接子节点,我们将它们成为兄弟节点。当一段文档以标准格式输出时,兄弟节点有相同的缩进级别。
6.3.1 .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
'text1'
>>> print(sibling_soup.b.string.next_sibling)
None
在现实情况中,一个标签的 .next_sibling 或 .previous_sibling 通常是一个包含空格的字符串。让我回到《爱丽丝梦游仙境》中:
<a href="http://example.com/elsie" class="sister" id="link1">埃尔西</a>,
<a href="http://example.com/lacie" class="sister" id="link2">莱斯</a>和
<a href="http://example.com/tillie" class="sister" id="link3">蒂尔莉</a>;
如果你觉得第一个 <a> 标签的 .next_sibling 是第二个 <a> 标签,那你就错了!事实上,它的结果是一个字符串 —— 由逗号和换行符构成,用于隔开第二个 <a> 标签:
>>> link = soup.a
>>> link
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>
>>> link.next_sibling
',\n'
第二个 <a> 标签是逗号的 .next_sibling 属性:
>>> link.next_sibling.next_sibling
<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>
6.3.2 .next_siblings 和 .previous_siblings
你可以通过 .next_siblings 和 .previous_siblings 属性对当前节点的所有兄弟节点迭代输出:
>>> link.next_sibling.next_sibling
<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>
>>> for sibling in soup.a.next_siblings:
print(repr(sibling))
',\n'
<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>
'和\n'
<a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>
';\n她们住在一个井底下面。'
>>> for sibling in soup.find(id="link3").previous_siblings:
print(repr(sibling))
'和\n'
<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>
',\n'
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>
'从前有三位小姐姐,她们的名字是:\n'
6.4 回退和前进
看一下《爱丽丝梦游仙境》的开头部分:
<html><head><title>睡鼠的故事</title></head>
<p class="title"><b>睡鼠的故事</b></p>
HTML 解析器把这段字符串转换成一连串的事件:打开一个 <html> 标签 -> 打开一个 <head> 标签 -> 打开一个 <title> 标签 -> 添加一段字符串 ->关闭一个 <title> 标签 -> 打开 <p> 标签等等。BeautifulSoup 提供了重现文档初始解析的工具。
6.4.1 .next_element 和 .previous_element
字符串或对象的 .next_element 属性指向下一个被解析的对象,结果可能与 .next_sibling 相同,但通常是不一样的。
这是《爱丽丝梦游仙境》文档中最后一个 <a> 标签,它的 .next_sibling 属性是指当前标签后紧接着的字符串:
>>> last_a_tag = soup.find("a", id="link3")
>>> last_a_tag
<a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>
>>> last_a_tag.next_sibling
';\n她们住在一个井底下面。'
但是这个 <a> 标签的 .next_element 属性则是指在 <a> 标签之后被解析内容,所以应该是字符串 "蒂尔莉":
>>> last_a_tag.next_element
'蒂尔莉'
这是因为在原始文档中,字符串 "蒂尔莉" 在分号前出现,解析器先进入 <a> 标签,然后是字符串 "蒂尔莉",接着关闭 </a> 标签,最后是分号和剩余部分。分号与 <a> 标签在同一层级,但是字符串 "蒂尔莉" 会被先解析。
.previous_element 属性刚好与 .next_element 相反,它指向当前被解析的对象的前一个解析对象:
>>> last_a_tag.previous_element
'和\n'
>>> last_a_tag.previous_element.next_element
<a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>
6.4.2 .next_elements 和 .previous_elements
通过 .next_elements 和 .previous_elements 的迭代器就可以向前或向后访问文档的解析内容,就好像文档正在被解析一样:
>>> for element in last_a_tag.next_elements:
print(repr(element))
'蒂尔莉'
';\n她们住在一个井底下面。'
'\n'
<p class="story">...</p>
'...'
'\n'
七、搜索文档树
BeautifulSoup 定义了很多搜索方法,但它们的用法都非常相似。这里我们用大篇幅着重介绍两个最常用的方法:find() 和 find_all()。其它方法的参数和用法类似,就制作简要说明,大家举一反三即可。
我们再一次以《爱丽丝梦游仙境》作为例子:
>>> html_doc = """<html><head><title>睡鼠的故事</title></head>
<body>
<p class="title"><b>睡鼠的故事</b></p>
<p class="story">从前有三位小姐姐,她们的名字是:
<a href="http://example.com/elsie" class="sister" id="link1">埃尔西</a>,
<a href="http://example.com/lacie" class="sister" id="link2">莱斯</a>和
<a href="http://example.com/tillie" class="sister" id="link3">蒂尔莉</a>;
她们住在一个井底下面。</p>
<p class="story">...</p>
"""
>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup(html_doc, 'html.parser')
通过将一个过滤器参数传递到类似 find_all() 的方法,可以搜索到感兴趣的内容。
7.1 几种过滤器
在讲解 find_all() 和其他类似方法之前,我想通过一些例子来向你展示都有哪些过滤器可以使用。
这些过滤器贯穿了所有的搜索 API 函数,它们可以被用在标签的名称、属性、文本这些上面。
7.1.1 字符串
最简单的过滤器是字符串,在搜索方法中传入一个字符串参数,BeautifulSoup 会查找与字符串完整匹配的内容,下面的例子用于查找文档中所有的 <b> 标签:
>>> soup.find('b')
<b>睡鼠的故事</b>
如果传入的是字节码参数,BeautifulSoup 将假设它是 UTF-8 编码。为了避免解码出错,可以直接传入一段 Unicode 编码。
7.1.2 正则表达式
如果传入正则表达式作为参数,BeautifulSoup 会通过正则表达式的 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
7.1.3 列表
如果传入列表参数,BeautifulSoup 会将与列表中任一元素匹配的内容返回。下面代码找到文档中所有 <a> 标签和 <b> 标签:
>>> soup.find_all(["a", "b"])
[<b>睡鼠的故事</b>, <a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
7.1.4 True
True 值可以匹配任意值,下面代码查找到所有的标签,但是不会返回字符串节点:
>>> for tag in soup.find_all(True):
print(tag.name)
html
head
title
body
p
b
p
a
a
a
p
7.1.5 函数
如果没有合适过滤器,那么还可以自己定义一个函数,该函数只接受一个元素作为参数。如果这个方法返回 True 表示当前元素匹配并且被找到,否则返回 False。
下面这个函数用于匹配那些包含 "class" 属性但不包含 "id" 属性的标签:
>>> 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>睡鼠的故事</b></p>, <p class="story">从前有三位小姐姐,她们的名字是:
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>,
<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>和
<a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>;
她们住在一个井底下面。</p>, <p class="story">...</p>]
返回结果中只有 <p> 标签没有 <a> 标签(上面出现的 <a> 是包含在 <p> 中的),因为 <a> 标签里面还定义了 "id",没有返回 <html> 和 <head>,因为 <html> 和 <head> 中没有定义 "class" 属性。
如果你传入一个函数来过滤一个像 href 这样的特定属性,传入函数的参数将是属性值,而不是整个标签。
下面这个函数可以找到所有拥有 href 属性,但不包含 "lacie" 字符串的标签:
>>> 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">埃尔西</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
只要你需要,函数还可以更复杂。
下面这个函数在一个标签包含字符串对象的时候返回 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
现在,我们来了解一下搜索方法的细节。
7.2 find_all()
find_all(name, attrs, recursive, string, limit, **kwargs)
find_all() 方法搜索当前 tag 下的所有子节点,并判断是否符合过滤器的条件。
这里有几个过滤器的例子:
>>> soup.find_all("title")
[<title>睡鼠的故事</title>]
>>>
>>> soup.find_all("p", "title")
[<p class="title"><b>睡鼠的故事</b></p>]
>>>
>>> soup.find_all("a")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
>>>
>>> soup.find_all(id="link2")
[<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>]
>>>
>>> import re
>>> soup.find(string=re.compile("小姐姐"))
'从前有三位小姐姐,她们的名字是:\n'
>>>
上面的 string 和 id 关键字参数代表什么呢?为什么 find_all("p", "title") 返回的是 Class 为 ”title” 的 <p> 标签呢?请看下面的参数讲解。
7.2.1 name 参数
通过 name 参数,你可以根据指定名字来查找标签。
简单的用法如下:
>>> soup.find_all("title")
[<title>睡鼠的故事</title>]
上一节提到的几种过滤器均可以作为 name 参数的值:字符串,正则表达式,列表,函数,或者直接一个布尔类型值 True。
7.2.2 keyword 参数
如果一个指定名字的参数不是搜索内置的(name, attrs, recursive, string, limit)参数名,搜索时会把该参数当作指定 tag 的属性来搜索。
比如你传递一个名为 id 的参数,BeautifulSoup 将会搜索每个 tag 的 ”id” 属性:
>>> soup.find_all(id="link2")
[<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>]
如果你传递一个名为 href 的参数,BeautifulSoup 将会搜索每个 tag 的 ”href” 属性:
>>> soup.find_all(href=re.compile("elsie"))
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>]
搜索指定名字的属性时可以使用的参数值包括:字符串、正则表达式、列表、函数和 True 值。
下面的例子在文档树中查找所有包含 id 属性的 tag,无论 id 的值是什么都将匹配:
>>> soup.find_all(id=True)
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
你还可以同时过滤多个属性:
>>> soup.find_all(href=re.compile("elsie"), id="link1")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>]
注意:有些 tag 属性在搜索不能使用,比如 HTML5 中的 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 关键字参数来实现 “曲线救国”:
>>> data_soup.find_all(attrs={"data-foo": "value"})
[<div data-foo="value">foo!</div>]
你不能使用关键字参数来搜索 HTML 的 "name" 元素,因为 BeautifulSoup 使用 name 参数来表示标签自身的名字。
取而代之,你可以将 "name" 添加到 attrs 参数的值中:
>>> name_soup = BeautifulSoup('<input name="email"/>')
>>> name_soup.find_all(name="email")
[]
>>> name_soup.find_all(attrs={"name": "email"})
[<input name="email"/>]
7.2.3 根据 CSS 进行搜索
按照 CSS 类名搜索标签的功能非常实用,但由于表示 CSS 类名的关键字 “class” 在 Python 中是保留字,所以使用 class 做参数会导致语法错误。从 BeautifulSoup 的 4.1.1 版本开始,可以通过 class_ 参数搜索有指定 CSS 类名的标签:
>>> soup.find_all("a", class_="sister")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
跟关键字参数一样,class_ 参数也支持不同类型的过滤器:字符串、正则表达式、函数或 True:
>>> soup.find_all(class_=re.compile("itl"))
[<p class="title"><b>睡鼠的故事</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">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</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>]
搜索 class 属性时也可以指定完全匹配的 CSS 值:
>>> css_soup.find_all("p", class_="body strikeout")
[<p class="body strikeout"></p>]
但如果 CSS 值的顺序与文档不一致,将导致结果搜索不到(尽管其字符串是一样的):
>>> css_soup.find_all("p", class_="strikeout body")
[]
如果你希望搜索结果同时匹配两个以上的 CSS 类名,你应该使用 CSS 选择器:
>>> css_soup.select("p.strikeout.body")
[<p class="body strikeout"></p>]
在那些没有 class_ 关键字的 BeautifulSoup 版本中,你可以使用 attrs 技巧(上面咱举过一个例子):
>>> soup.find_all("a", attrs={"class": "sister"})
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
7.2.4 string 参数
通过 string 参数可以搜索标签中的文本内容。与 name 参数一样,string 参数接受字符串,正则表达式,列表,函数,或者直接一个布尔类型值 True。
请看下面例子:
>>> soup.find_all(string="埃尔西")
['埃尔西']
>>>
>>> soup.find_all(string=["蒂尔莉", "埃尔西", "莱斯"])
['埃尔西', '莱斯', '蒂尔莉']
>>>
>>> soup.find_all(string=re.compile("睡鼠"))
['睡鼠的故事', '睡鼠的故事']
>>>
>>> def is_the_only_string_within_a_tag(s):
"""如果字符串是其父标签的唯一子节点,则返回 True。"""
return (s == s.parent.string)
>>> soup.find_all(string=is_the_only_string_within_a_tag)
['睡鼠的故事', '睡鼠的故事', '埃尔西', '莱斯', '蒂尔莉', '...']
尽管 string 参数是用于搜索字符串的,但你可以与其它参数混合起来使用:下面代码中,BeautifulSoup 会找到所有与 string 参数值相匹配的 <a> 标签:
>>> soup.find_all("a", string="埃尔西")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>]
string 参数是 BeautifulSoup 4.4.0 新增加的特性,在早期的版本中,它叫 text 参数:
>>> soup.find_all("a", text="埃尔西")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>]
7.2.5 limit 参数
find_all() 方法返回匹配过滤器的所有标签和文本。如果文档树很大,那么搜索就会变得很慢。如果你不需要全部的结果,可以使用 limit 参数限制返回结果的数量。效果与 SQL 中的 LIMIT 关键字类似 —— 当搜索到的结果数量达到 limit 的限制时,就停止搜索并返回结果。
文档树中有 3 个标签符合搜索条件,但结果只返回了 2 个,因为我们限制了返回数量:
>>> soup.find_all("a", limit=2)
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>]
7.2.6 recursive 参数
如果你调用 mytag.find_all() 方法,BeautifulSoup 将会获取 mytag 的所有子孙节点。如果只想搜索 mytag 的直接子节点,可以使用参数 recursive=False。
对比一下:
>>> soup.html.find_all("title")
[<title>睡鼠的故事</title>]
>>>
>>> soup.html.find_all("title", recursive=False)
[]
文档的原型是这样的:
<html>
<head>
<title>
The Dormouse's story
</title>
</head>
...
<title> 标签在 <html> 标签下,但并不是直接子节点,<head> 标签才是直接子节点。在允许查询所有后代节点的时候,BeautifulSoup 能够查找到 <title> 标签。但是使用了 recursive=False 参数之后,只能查找直接子节点,因此就查不到 <title> 标签了。
BeautifulSoup 提供了多种 DOM 树搜索方法(下面将展示给大家)。这些方法都使用了与 find_all() 类似的参数:name、attrs、stirng、limit 和关键字参数。但是只有 find_all() 和 find() 支持 recursive 参数。给 find_parents() 方法传递 recursive=False 参数并没有什么作用。
7.3 像调用 find_all() 一样调用一个标签
由于 find_all() 几乎是 Beautiful Soup 中最常用的搜索方法,所以我们为它定义了一种简写的形式:如果你将 BeautifulSoup 对象或 Tag 对象当作一个方法来使用,那么这个方法的执行结果与调用这个对象的 find_all() 方法是相同的。
因此,下面两行代码是等价的:
soup.find_all("a")
soup("a")
还有下面两行代码也是等价的:
soup.title.find_all(text=True)
soup.title(text=True)
7.4 find() 方法
find_all(name, attrs, recursive, string, **kwargs)
find_all() 方法将返回文档中符合条件的所有tag,尽管有时候我们只想得到一个结果。比如文档中只有一个<body>标签,那么使用 find_all() 方法来查找<body>标签就不太合适,使用 find_all 方法并设置 limit=1 参数不如直接使用 find()方法。下面两行代码是等价的:
>>> soup.find_all('title', limit=1)
[<title>睡鼠的故事</title>]
>>> soup.find('title')
<title>睡鼠的故事</title>
唯一的区别是 find_all() 方法的返回结果是值包含一个元素的列表,而 find() 方法直接返回结果。find_all() 方法没有找到目标是返回空列表, find() 方法找不到目标时,返回 None 。
>>> print(soup.find("nosuchtag"))
None
>>> print(soup.find_all("nosuchtag"))
[]
soup.head.title 是 6.1 子节点(向下遍历)-使用标签名进行遍历 方法的简写。这个简写的原理就是多次调用当前tag的 find() 方法:
>>> soup.head.title
<title>睡鼠的故事</title>
>>> soup.find("head").find("title")
<title>睡鼠的故事</title>
7.5 find_parents() 和 find_parent()
find_parents(name, attrs, string, limit, **kwargs)
find_parent(name, attrs, string, **kwargs)
我们已经用了很大篇幅来介绍 find_all() 和 find() 方法,Beautiful Soup中还有10个用于搜索的API。它们中的五个用的是与 find_all() 相同的搜索参数。另外5个与 find() 方法的搜索参数类似。区别仅是它们搜索文档的不同部分。
记住:find_all() 和 find() 只搜索当前节点的所有子节点,孙子节点等。 find_parents() 和 find_parent() 用来搜索当前节点的父辈节点,搜索方法与普通tag的搜索方法相同,搜索文档包含的内容。我们从一个文档中的一个叶子节点开始:
>>> a_string = soup.find(text="莱斯")
>>> a_string
'莱斯'
>>> a_string.find_parents("a")
[<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>]
>>> a_string.find_parents("p")
[<p class="story">从前有三位小姐姐,她们的名字是:
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>,
<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>和
<a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>;
她们住在一个井底下面。</p>]
>>> a_string.find_parents("p", class_="sister")
[]
文档中的一个<a>标签是是当前叶子节点的直接父节点,所以可以被找到.还有一个<p>标签,是目标叶子节点的间接父辈节点,所以也可以被找到。包含class值为”title”的<p>标签不是不是目标叶子节点的父辈节点,所以通过 find_parents() 方法搜索不到。
find_parent() 和 find_parents() 方法会让人联想到 6.2 父节点(向上遍历)中 .parent 和 .parents 属性。它们之间的联系非常紧密。搜索父辈节点的方法实际上就是对 .parents 属性的迭代搜索.
7.6 find_next_siblings() 和 find_next_sibling()
find_next_siblings(name, attrs, string, limit, **kwargs)
find_next_sibling(name, attrs, string, **kwargs)
这2个方法通过 6.3 兄弟节点(左右遍历)中 .next_siblings 属性对当tag的所有后面解析的兄弟tag节点进行迭代,find_next_siblings() 方法返回所有符合条件的后面的兄弟节点,find_next_sibling() 只返回符合条件的后面的第一个tag节点。
>>> first_link = soup.a
>>> first_link
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>
>>> first_link.find_next_siblings("a")
[<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
>>> first_story_paragraph = soup.find("p", "story")
>>> first_story_paragraph.find_next_sibling("p")
<p class="story">...</p>
7.7 find_previous_siblings() 和 find_previous_sibling()
find_previous_siblings() (name, attrs, string, limit, **kwargs)
find_previous_sibling()(name, attrs, string, **kwargs)
这2个方法通过 6.3 兄弟节点(左右遍历)中 .previous_siblings 属性对当前tag的前面解析的兄弟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">蒂尔莉</a>
>>> last_link.find_previous_siblings("a")
[<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>]
>>> first_story_paragraph = soup.find("p", "story")
>>> first_story_paragraph.find_previous_sibling("p")
<p class="title"><b>睡鼠的故事</b></p>
7.8 find_all_next() 和 find_next()
find_all_next(name, attrs, string, limit, **kwargs)
find_next(name, attrs, string, **kwargs)
这2个方法通过 6.4 回退和前进 中 .next_elements 属性对当前tag的之后的 tag和字符串进行迭代, find_all_next() 方法返回所有符合条件的节点, find_next() 方法返回第一个符合条件的节点:
>>> first_link = soup.a
>>> first_link
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>
>>> first_link.find_all_next(string=True)
['埃尔西', ',\n', '莱斯', '和\n', '蒂尔莉', ';\n她们住在一个井底下面。', '\n', '...', '\n']
>>> first_link.find_next("p")
<p class="story">...</p>
第一个例子中,字符串 “埃尔西”也被显示出来,尽管它被包含在我们开始查找的<a>标签的里面。第二个例子中,最后一个<p>标签也被显示出来,尽管它与我们开始查找位置的<a>标签不属于同一部分。例子中,搜索的重点是要匹配过滤器的条件,并且在文档中出现的顺序而不是开始查找的元素的位置。
7.9 find_all_previous() 和 find_previous()
find_all_previous(name, attrs, string, limit, **kwargs)
find_previous(name, attrs, string, **kwargs)
这2个方法通过 6.4 回退和前进 中 .previous_elements 属性对当前节点前面 的tag和字符串进行迭代, find_all_previous() 方法返回所有符合条件的节点, find_previous() 方法返回第一个符合条件的节点。
>>> first_link = soup.a
>>> first_link
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>
>>> first_link.find_all_previous("p")
[<p class="story">从前有三位小姐姐,她们的名字是:
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>,
<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>和
<a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>;
她们住在一个井底下面。</p>, <p class="title"><b>睡鼠的故事</b></p>]
>>> first_link.find_previous("title")
<title>睡鼠的故事</title>
find_all_previous("p") 返回了文档中的第一段(class=”title”的那段),但还返回了第二段,<p>标签包含了我们开始查找的<a>标签。不要惊讶,这段代码的功能是查找所有出现在指定<a>标签之前的<p>标签,因为这个<p>标签包含了开始的<a>标签,所以<p>标签一定是在<a>之前出现的。
7.10 CSS选择器
从版本4.7.0开始,Beautiful Soup通过 SoupSieve 项目支持大多数CSS4选择器。 如果您通过pip安装了Beautiful Soup,则同时安装了SoupSieve,因此您无需执行任何额外操作。
BeautifulSoup有一个.select()方法,该方法使用SoupSieve对解析的文档运行CSS选择器并返回所有匹配的元素。 Tag有一个类似的方法,它针对单个标记的内容运行CSS选择器。
(早期版本的Beautiful Soup也有.select()方法,但只支持最常用的CSS选择器。)
SoupSieve文档 列出了所有当前支持的CSS选择器,但以下是一些基础知识:
可以使用CSS选择器的语法找到tag:
>>> soup.select("title")
[<title>睡鼠的故事</title>]
>>> soup.select("p:nth-of-type(3)")
[<p class="story">...</p>]
通过tag标签逐层查找:
>>> soup.select("body a")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
>>> soup.select("html head title")
[<title>睡鼠的故事</title>]
找到某个tag标签下的直接子标签:
>>> soup.select("head > title")
[<title>睡鼠的故事</title>]
>>> soup.select("p > a")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
>>> soup.select("p > a:nth-of-type(2)")
[<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>]
>>> soup.select("p > #link1")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>]
>>> soup.select("body > a")
[]
找到兄弟节点标签:
>>> soup.select("#link1 ~ .sister")
[<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
>>> soup.select("#link1 + .sister")
[<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>]
通过CSS的类名查找:
>>> soup.select(".sister")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
>>> soup.select("[class~=sister]")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
通过tag的ID查找:
>>> soup.select("#link1")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>]
>>> soup.select("a#link2")
[<a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>]
查找与选择器列表中的任何选择器匹配的tag:
>>> soup.select("#link1,#link2")
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>]
通过是否存在某个属性来查找:
>>> soup.select('a[href]')
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
通过属性的值来查找:
>>> soup.select('a[href="http://example.com/elsie"]')
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>]
>>> soup.select('a[href^="http://example.com/"]')
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>, <a class="sister" href="http://example.com/lacie" id="link2">莱斯</a>, <a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
>>> soup.select('a[href$="tillie"]')
[<a class="sister" href="http://example.com/tillie" id="link3">蒂尔莉</a>]
>>> soup.select('a[href*=".com/el"]')
[<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>]
还有一个名为 select_one()的方法,它只查找与选择器匹配的第一个标记:
>>> soup.select_one(".sister")
<a class="sister" href="http://example.com/elsie" id="link1">埃尔西</a>
如果您已经解析了定义名称空间的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> """
>>>soup = BeautifulSoup(xml, "xml")
>>>soup.select("child")
[<ns1:child>I'm in namespace 1</ns1:child>, <ns2:child>I'm in namespace 2</ns2:child>]
>>>soup.select("ns1|child", namespaces=namespaces)
[<ns1:child>I'm in namespace 1</ns1:child>]
注意:这里需要安装 xml 解析库,如果出现以下报错:
bs4.FeatureNotFound: Couldn't find a tree builder with the features you requested: xml. Do you need to install a parser library?
需要 通过 pip install lxml 安装 lxml:
处理使用命名空间的CSS选择器时,Beautiful Soup使用在解析文档时找到的命名空间缩写。 您可以通过传入自己的缩写词典来覆盖它:
>>>namespaces = dict(first="http://namespace1/", second="http://namespace2/")
>>>soup.select("second|child", namespaces=namespaces)
[<ns1:child>I'm in namespace 2</ns1:child>]
所有这些CSS选择器的东西对于已经知道CSS选择器语法的人来说都很方便。 您可以使用Beautiful Soup API完成所有这些工作。 如果你只需要CSS选择器,你应该使用lxml解析文档:它的速度要快得多。 但是这可以让你将CSS选择器与Beautiful Soup API结合起来。
8 修改文档树
Beautiful Soup的主要优势在于搜索解析树,但您也可以修改树并将更改写为新的HTML或XML文档。
8.1 修改tag的名称和属性
在 5.1 Tag(标签)- Attributes(属性) 章节中已经介绍过这个功能,但是再看一遍也无妨. 重命名一个tag,改变属性的值,添加或删除属性:
>>> soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
>>> tag = soup.b
>>> tag.name = "blockquote"
>>> tag['class'] = 'verybold'
>>> tag['id'] = 1
>>> tag
<blockquote class="verybold" id="1">Extremely bold</blockquote>
>>> del tag['class']
>>> del tag['id']
>>> tag
<blockquote>Extremely bold</blockquote>
8.2 修改 .string
给tag的 .string 属性赋值,就相当于用当前的内容替代了原来的内容:
>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.string = "New link text."
>>> tag
<a href="http://example.com/">New link text.</a>
注意:如果当前的tag包含了其它tag,那么给它的 .string 属性赋值会覆盖掉原有的所有内容包括子tag。
8.3 append()
Tag.append() 方法想tag中添加内容,就好像Python的列表的 .append() 方法:
>>> soup = BeautifulSoup("<a>Foo</a>")
>>> soup.a.append("Bar")
>>> soup
<a>FooBar</a>
>>> soup.a.contents
['Foo', 'Bar']
8.4 extend()
从Beautiful Soup 4.7.0开始,Tag还支持一个名为.extend()的方法,它就像在Python列表上调用.extend()一样:
>>> soup = BeautifulSoup("<a>Soup</a>")
>>> soup.a.extend(["'s", " ", "on"])
>>> soup
<a>Soup's on</a>
>>> soup.a.contents
['Soup', "'s", ' ', 'on']
8.5 NevigableString() 和 .new_tag()
如果想添加一段文本内容到文档中也没问题,可以调用Python的 append() 方法或调用 NavigableString() 构造函数: :
>>> soup = BeautifulSoup("<b></b>")
>>> tag = soup.b
>>> tag.append("Hello")
>>> new_string = NavigableString(" there")
>>> tag.append(new_string)
>>> tag
<b>Hello there.</b>
>>> tag.contents
['Hello', ' there']
如果要创建注释或NavigableString的其他子类,只需调用构造函数:
>>> from bs4 import Comment
>>> new_comment = Comment("Nice to see you.")
>>> tag.append(new_comment)
>>> tag
<b>Hello there<!--Nice to see you.--></b>
>>> tag.contents
['Hello', ' there', 'Nice to see you.']
(这是Beautiful Soup 4.4.0的新功能。)
创建一个tag最好的方法是调用工厂方法 BeautifulSoup.new_tag() :
>>> soup = BeautifulSoup("<b></b>")
>>> original_tag = soup.b
>>> new_tag = soup.new_tag("a", href="http://www.example.com")
>>> original_tag.append(new_tag)
>>> original_tag
<b><a href="http://www.example.com"></a></b>
>>> new_tag.string = "Link text."
>>> original_tag
<b><a href="http://www.example.com">Link text.</a></b>
第一个参数作为tag的name,是必填,其它参数选填。
8.6 insert()
Tag.insert() 方法与 Tag.append() 方法类似,区别是不会把新元素添加到父节点 .contents 属性的最后,而是把元素插入到指定的位置。与Python列表总的 .insert() 方法的用法相同:
>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.insert(1, "but did not endorse ")
>>> tag
<a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
>>> tag.contents
['I linked to ', 'but did not endorse ', <i>example.com</i>]
8.7 insert_before() 和 insert_after()
insert_before() 方法在当前tag或文本节点前插入tag 或者 字符串:
>>> soup = BeautifulSoup("<b>stop</b>")
>>> tag = soup.new_tag("i")
>>> tag.string = "Don't"
>>> soup.b.string.insert_before(tag)
>>> soup.b
<b><i>Don't</i>stop</b>
insert_after() 方法在当前tag或文本节点前插入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>stop</b>
>>> soup.b.contents
[<i>Don't</i>, ' you ', <div>ever</div>, 'stop']
8.8 clear()
Tag.clear() 方法移除当前tag的内容:
>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.clear()
>>> tag
<a href="http://example.com/"></a>
8.9 extract()
PageElement.extract() 方法将当前tag移除文档树,并作为方法结果返回:
>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> i_tag = soup.i.extract()
>>> a_tag
<a href="http://example.com/">I linked to </a>
>>> i_tag
<i>example.com</i>
>>> print(i_tag.parent)
None
这个方法实际上产生了2个文档树: 一个是用来解析原始文档的 BeautifulSoup 对象,另一个是被移除并且返回的tag。被移除并返回的tag可以继续调用 extract 方法:
>>> my_string = i_tag.string.extract()
>>> my_string
'example.com'
>>> print(my_string.parent)
None
>>> i_tag
<i></i>
8.10 decompose()
Tag.decompose() 方法将当前节点移除文档树并完全销毁:
>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> soup.i.decompose()
>>> a_tag
<a href="http://example.com/">I linked to </a>
8.11 replace_with()
PageElement.replace_with() 方法移除文档树中的某段内容,并用新tag或文本节点替代它:
>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> new_tag = soup.new_tag("b")
>>> new_tag.string = "example.net"
>>> a_tag.i.replace_with(new_tag)
>>> a_tag
<a href="http://example.com/">I linked to <b>example.net</b></a>
replace_with() 方法返回被替代的tag或文本节点,可以用来浏览或添加到文档树其它地方
8.12 wrap()
PageElement.wrap() 方法可以对指定的tag元素进行包装,并返回包装后的结果:
>>> soup = BeautifulSoup("<p>I wish I was bold.</p>")
>>> soup.p.string.wrap(soup.new_tag("b"))
<b>I wish I was bold.</b>
>>> soup.p.wrap(soup.new_tag("div")
<div><p><b>I wish I was bold.</b></p></div>
该方法在 Beautiful Soup 4.0.5 中添加
8.13 unwrap()
Tag.unwrap() 方法与 wrap() 方法相反。将移除tag内的所有tag标签,该方法常被用来进行标记的解包:
>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> a_tag.i.unwrap()
>>> a_tag
<a href="http://example.com/">I linked to example.com</a>
与 replace_with() 方法相同, unwrap() 方法返回被移除的tag。
九、输出
9.1 格式化输出
prettify() 方法将Beautiful Soup的文档树格式化后以Unicode编码输出,每个XML/HTML标签都独占一行
>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> soup.prettify()
'<html>\n <body>\n <a href="http://example.com/">\n I linked to\n <i>\n example.com\n </i>\n </a>\n </body>\n</html>'
>>> print(soup.prettify())
<html>
<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>
9.2 压缩输出
如果只想得到结果字符串,不重视格式,那么可以对一个 BeautifulSoup 对象或 Tag 对象使用Python的 unicode() 或 str() 方法:
>>> str(soup)
'<html><head></head><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'
>>> unicode(soup.a)
u'<a href="http://example.com/">I linked to <i>example.com</i></a>'
str() 方法返回UTF-8编码的字符串,可以指定 编码 的设置。
还可以调用 encode() 方法获得字节码或调用 decode() 方法获得Unicode.
9.3 输出格式
Beautiful Soup输出是会将HTML中的特殊字符转换成Unicode,比如“&lquot;”:
>>> soup = BeautifulSoup("“Dammit!” he said.")
>>> unicode(soup)
u'<html><head></head><body>\u201cDammit!\u201d he said.</body></html>'
如果将文档转换成字符串,Unicode编码会被编码成UTF-8.这样就无法正确显示HTML特殊字符了:
>>> str(soup)
'<html><head></head><body>\xe2\x80\x9cDammit!\xe2\x80\x9d he said.</body></html>'
默认情况下,输出时转义的唯一字符是裸字符和尖括号。 这些变成了“&amp;”,“&lt;”和“&gt;”,因此Beautiful Soup不会无意中生成无效的HTML或XML:
>>> soup = BeautifulSoup("<p>The law firm of Dewey, Cheatem, & Howe</p>")
>>> soup.p
<p>The law firm of Dewey, Cheatem, & Howe</p>
>>> soup = BeautifulSoup('<a href="http://example.com/?foo=val1&bar=val2">A link</a>')
>>> soup.a
<a href="http://example.com/?foo=val1&bar=val2">A link</a>
您可以通过为prettify(),encode()或decode()提供formatter参数的值来更改此行为。 Beautiful Soup识别格式化程序的六个可能值。
默认值为formatter =“minimal”。 字符串只会被处理得足以确保Beautiful Soup生成有效的HTML / XML:
>>> french = "<p>Il a dit <<Sacré bleu!>></p>"
>>> soup = BeautifulSoup(french)
>>> print(soup.prettify(formatter="minimal"))
<html>
<body>
<p>
Il a dit <<Sacré bleu!>>
</p>
</body>
</html>
如果您传入formatter =“html”,Beautiful Soup会尽可能将Unicode字符转换为HTML实体:
>>> print(soup.prettify(formatter="html"))
<html>
<body>
<p>
Il a dit <<Sacré bleu!>>
</p>
</body>
</html>
#如果您传入formatter =“html5”,是一样的
>>> print(soup.prettify(formatter="html5"))
<html>
<body>
<p>
Il a dit <<Sacré bleu!>>
</p>
</body>
</html>
但是如果您传入formatter =“html5”,Beautiful Soup会省略HTML空标签中的结束斜杠,比如“br”:
>>> soup = BeautifulSoup("<br>")
>>> print(soup.encode(formatter="html"))
<html><body><br/></body></html>
>>> print(soup.encode(formatter="html5"))
<html><body><br></body></html>
如果传入formatter = None,Beautiful Soup将不会在输出时修改字符串。 这是最快的选项,但它可能导致Beautiful Soup生成无效的HTML / XML,如下例所示:
>>> print(soup.prettify(formatter=None))
<html>
<body>
<p>
Il a dit <<Sacré bleu!>>
</p>
</body>
</html>
>>> link_soup = BeautifulSoup('<a href="http://example.com/?foo=val1&bar=val2">A link</a>')
>>> print(link_soup.a.encode(formatter=None))
b'<a href="http://example.com/?foo=val1&bar=val2">A link</a>'
最后,如果传入格式化程序的函数,Beautiful Soup将为文档中的每个字符串和属性值调用该函数一次。 你可以在这个功能中做任何你想做的事情。 这是一个将字符串转换为大写的格式化程序,并且完全没有其他内容:
>>> def uppercase(str):
return str.upper()
>>> print(soup.prettify(formatter=uppercase))
<html>
<body>
<p>
IL A DIT <<SACRÉ BLEU!>>
</p>
</body>
</html>
>>> print(link_soup.a.prettify(formatter=uppercase))
<a href="HTTP://EXAMPLE.COM/?FOO=VAL1&BAR=VAL2">
A LINK
</a>
如果您正在编写自己的函数,则应该了解bs4.dammit模块中的EntitySubstitution类。 此类将Beautiful Soup的标准格式化程序实现为类方法:“html”格式化程序是EntitySubstitution.substitute_html,“minimal”格式化程序是EntitySubstitution.substitute_xml。 您可以使用这些函数来模拟formatter = html或formatter == minimal,但然后再做一些额外的事情。
这是一个尽可能用HTML实体替换Unicode字符的示例,但也将所有字符串转换为大写:
>>> from bs4.dammit import EntitySubstitution
>>> def uppercase_and_substitute_html_entities(str):
return EntitySubstitution.substitute_html(str.upper())
>>> print(soup.prettify(formatter=uppercase_and_substitute_html_entities))
<html>
<body>
<p>
IL A DIT <<SACRÉ BLEU!>>
</p>
</body>
</html>
最后一点需要注意的是:如果您创建了一个CData对象,该对象内的文本将始终与其显示完全一致,没有格式化。 Beautiful Soup会调用formatter方法,以防万一你编写了一个自定义方法来计算文档中的所有字符串,但它会忽略返回值:
>>> from bs4.element import CData
>>> soup = BeautifulSoup("<a></a>")
>>> soup.a.string = CData("one < three")
>>> print(soup.a.prettify(formatter="xml"))
<a>
<![CDATA[one < three]]>
</a>
9.4 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)
>>> 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'
或者使用 6.1 子节点(向下遍历)中 .stripped_strings 生成器,获得文本列表后手动处理列表:
>>> [text for text in soup.stripped_strings]
['I linked to', 'example.com']
十、指定文档解析器
如果仅是想要解析HTML文档,只要用文档创建 BeautifulSoup 对象就可以了.Beautiful Soup会自动选择一个解析器来解析文档.但是还可以通过参数指定使用那种解析器来解析当前文档。
BeautifulSoup 第一个参数应该是要被解析的文档字符串或是文件句柄,第二个参数用来标识怎样解析文档.如果第二个参数为空,那么Beautiful Soup根据当前系统安装的库自动选择解析器,解析器的优先数序: lxml, html5lib, Python标准库.在下面两种条件下解析器优先顺序会变化:
- 要解析的文档是什么类型: 目前支持, “html”, “xml”, 和 “html5”
- 指定使用哪种解析器: 目前支持, “lxml”, “html5lib”, 和 “html.parser”
3.2 安装解析器 章节介绍了可以使用哪种解析器,以及如何安装。
如果指定的解析器没有安装,Beautiful Soup会自动选择其它方案.目前只有 lxml 解析器支持XML文档的解析,在没有安装lxml库的情况下,创建 beautifulsoup 对象时无论是否指定使用lxml,都无法得到解析后的对象
10.1 解析器之间的区别
Beautiful Soup为不同的解析器提供了相同的接口,但解析器本身时有区别的.同一篇文档被不同的解析器解析后可能会生成不同结构的树型文档.区别最大的是HTML解析器和XML解析器,看下面片段被解析成HTML结构:
>>> BeautifulSoup("<a><b /></a>")
<html><body><a><b></b></a></body></html>
因为空标签<b />不符合HTML标准,所以解析器把它解析成<b></b>
同样的文档使用XML解析如下(解析XML需要安装lxml库).注意,空标签<b />依然被保留,并且文档前添加了XML头,而不是被包含在<html>标签内:
>>> BeautifulSoup("<a><b /></a>", "xml")
<?xml version="1.0" encoding="utf-8"?>
<a><b/></a>
HTML解析器之间也有区别,如果被解析的HTML文档是标准格式,那么解析器之间没有任何差别,只是解析速度不同,结果都会返回正确的文档树.
但是如果被解析文档不是标准格式,那么不同的解析器返回结果可能不同.下面例子中,使用lxml解析错误格式的文档,结果</p>标签被直接忽略掉了:
>>> BeautifulSoup("<a></p>", "lxml")
<html><body><a></a></body></html>
使用html5lib库解析相同文档会得到不同的结果:
>>> BeautifulSoup("<a></p>", "html5lib")
<html><head></head><body><a><p></p></a></body></html>
html5lib库没有忽略掉</p>标签,而是自动补全了标签,还给文档树添加了<head>标签.
使用pyhton内置库解析结果如下:
>>> BeautifulSoup("<a></p>", "html.parser")
<a></a>
与lxml 库类似的,Python内置库忽略掉了</p>标签,与html5lib库不同的是标准库没有尝试创建符合标准的文档格式或将文档片段包含在<body>标签内,与lxml不同的是标准库甚至连<html>标签都没有尝试去添加.
因为文档片段“<a></p>”是错误格式,所以以上解析方式都能算作”正确”,html5lib库使用的是HTML5的部分标准,所以最接近”正确”.不过所有解析器的结构都能够被认为是”正常”的.
不同的解析器可能影响代码执行结果,如果在分发给别人的代码中使用了 BeautifulSoup ,那么最好注明使用了哪种解析器,以减少不必要的麻烦.
十一、编码
任何HTML或XML文档都有自己的编码方式,比如ASCII 或 UTF-8,但是使用Beautiful Soup解析后,文档都被转换成了Unicode:
>>> markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
>>> soup = BeautifulSoup(markup)
>>> soup.h1
<h1>Sacré bleu!</h1>
>>> soup.h1.string
'Sacré bleu!'
这不是魔术(但很神奇),Beautiful Soup用了 编码自动检测 子库来识别当前文档编码并转换成Unicode编码. BeautifulSoup 对象的 .original_encoding 属性记录了自动识别编码的结果:
>>> soup.original_encoding
'utf-8'
编码自动检测 功能大部分时候都能猜对编码格式,但有时候也会出错.有时候即使猜测正确,也是在逐个字节的遍历整个文档后才猜对的,这样很慢.如果预先知道文档编码,可以设置编码参数来减少自动检查编码出错的概率并且提高文档解析速度.在创建 BeautifulSoup 对象的时候设置 from_encoding 参数.
下面一段文档用了ISO-8859-8编码方式,这段文档太短,结果Beautiful Soup以为文档是用windows-1252编码:
>>> markup = b"<h1>\xed\xe5\xec\xf9</h1>"
>>> soup = BeautifulSoup(markup)
>>> soup.h1
<h1>íåìù</h1>
>>> soup.original_encoding
'windows-1252'
通过传入 from_encoding 参数来指定编码方式:
>>> soup = BeautifulSoup(markup, from_encoding="iso-8859-8")
>>> soup.h1
<h1>םולש</h1>
>>> soup.original_encoding
'iso-8859-8'
少数情况下(通常是UTF-8编码的文档中包含了其它编码格式的文件),想获得正确的Unicode编码就不得不将文档中少数特殊编码字符替换成特殊Unicode编码,“REPLACEMENT CHARACTER” (U+FFFD, �) . 如果Beautifu Soup猜测文档编码时作了特殊字符的替换,那么Beautiful Soup会把 UnicodeDammit 或 BeautifulSoup 对象的 .contains_replacement_characters 属性标记为 True .这样就可以知道当前文档进行Unicode编码后丢失了一部分特殊内容字符.如果文档中包含�而 .contains_replacement_characters 属性是 False ,则表示�就是文档中原来的字符,不是转码失败.
11.1 输出编码
通过Beautiful Soup输出文档时,不管输入文档是什么编码方式,输出编码均为UTF-8编码,下面例子输入文档是Latin-1编码:
>>> markup = b'''
<html>
<head>
<meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type" />
</head>
<body>
<p>Sacr\xe9 bleu!</p>
</body>
</html>
'''
>>> soup = BeautifulSoup(markup)
>>> print(soup.prettify())
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-type"/>
</head>
<body>
<p>
Sacré bleu!
</p>
</body>
</html>
注意,输出文档中的<meta>标签的编码设置已经修改成了与输出编码一致的UTF-8.
如果不想用UTF-8编码输出,可以将编码方式传入 prettify() 方法:
>>> print(soup.prettify("latin-1"))
b'<html>\n <head>\n <meta content="text/html; charset=latin-1" http-equiv="Content-type"/>\n </head>\n <body>\n <p>\n Sacr\xe9 bleu!\n </p>\n </body>\n</html>\n'
还可以调用 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)
>>> tag = snowman_soup.b
SNOWMAN字符在UTF-8编码中可以正常显示(看上去像是☃),但有些编码不支持SNOWMAN字符,比如ISO-Latin-1或ASCII,那么在这些编码中SNOWMAN字符会被转换成“☃”:
>>> print(tag.encode("utf-8"))
b'<b>\xe2\x98\x83</b>'
>>> print(tag.encode("latin-1"))
b'<b>☃</b>'
>>> print(tag.encode("ascii"))
b'<b>☃</b>'
11.2 Unicode, dammit! (靠!)
编码自动检测 功能可以在Beautiful Soup以外使用,检测某段未知编码时,可以使用这个方法:
>>> from bs4 import UnicodeDammit
>>> dammit = UnicodeDammit("Sacr\xc3\xa9 bleu!")
>>> print(dammit.unicode_markup)
Sacré bleu!
>>> dammit.original_encoding
'utf-8'
如果Python中安装了 chardet 或 cchardet 那么编码检测功能的准确率将大大提高.输入的字符越多,检测结果越精确,如果事先猜测到一些可能编码,那么可以将猜测的编码作为参数,这样将优先检测这些编码:
>>> dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
>>> print(dammit.unicode_markup)
Sacré bleu!
>>> dammit.original_encoding
'latin-1'
编码自动检测 功能中有2项功能是Beautiful Soup库中用不到的
11.2.1 智能引号
使用Unicode时,Beautiful Soup还会智能的把引号 转换成HTML或XML中的特殊字符:
>>> markup = b"<p>I just \x93love\x94 Microsoft Word\x92s smart quotes</p>"
>>> UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="html").unicode_markup
u'<p>I just “love” Microsoft Word’s smart quotes</p>'
>>> UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
u'<p>I just “love” Microsoft Word’s smart quotes</p>'
也可以把引号转换为ASCII码:
>>> UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
u'<p>I just "love" Microsoft Word\'s smart quotes</p>'
很有用的功能,但是Beautiful Soup没有使用这种方式.默认情况下,Beautiful Soup把引号转换成Unicode:
>>> UnicodeDammit(markup, ["windows-1252"]).unicode_markup
u'<p>I just \u201clove\u201d Microsoft Word\u2019s smart quotes</p>'
矛盾的编码
有时文档的大部分都是用UTF-8,但同时还包含了Windows-1252编码的字符,就像微软的智能引号 一样.一些包含多个信息的来源网站容易出现这种情况. 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编码内容,但这解决了最常见的一类问题.
在创建 BeautifulSoup 或 UnicodeDammit 对象前一定要先对文档调用 UnicodeDammit.detwingle() 确保文档的编码方式正确.如果尝试去解析一段包含Windows-1252编码的UTF-8文档,就会得到一堆乱码,比如: ☃☃☃“I like snowmen!”.
UnicodeDammit.detwingle() 方法在Beautiful Soup 4.1.0版本中新增
十二、比较对象是否相等
Beautiful Soup表示当两个NavigableString或Tag对象表示相同的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
如果要查看两个变量是否指向完全相同的对象,请使用 is :
>>> print(first_b is second_b)
False
十三、复制 Beautiful Soup 对象
您可以使用copy.copy()创建任何Tag或NavigableString的副本:
>>> 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()一样:
>>> print(p_copy.parent)
None