我们解析网页最大的难点就在于其 HTML 源码是一长串复杂的字符串,而 BeautifulSoup 恰好提供了将其解析为特定的数据结构的能力,这样我们就可以通过 “访问属性” 和 “调用方法” 的方式快速获取网页中的指定内容。
解析器
BeautifulSoup 支持 Python 标准库中的 HTML 解析器,还支持一些第三方的解析器,比如:lxml 或 html5lib,这些解析器都是需要额外 pip 安装的。
解析器 | 使用方法 | 优势 | 劣势 |
---|---|---|---|
Python标准库 | BeautifulSoup(markup, “html.parser”) | Python的内置标准库,执行速度适中,文档容错能力强 | 文档容错能力差 |
lxml HTML 解析器 | BeautifulSoup(markup, “lxml”) | 速度快,文档容错能力强 | 需要安装C语言库 pip install lxml |
lxml XML 解析器 | BeautifulSoup(markup, “xml”) | 速度快,唯一支持XML的解析器 | 需要安装C语言库 pip install lxml |
html5lib 解析器 | BeautifulSoup(markup, “html5lib”) | 最好的容错性,以浏览器的方式解析文档,生成HTML5格式的文档 | 速度慢,不依赖外部扩展 pip install html5lib |
数据结构
我们将一段关于“爱丽丝梦游仙境”的文档 HTML 源码传入 BeautifulSoup 的构造函数,就能得到一个文档的对象:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from bs4 import BeautifulSoup
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
soup = BeautifulSoup(html_doc, 'html.parser')
BeautifulSoup 将复杂 HTML 文档转换成一个复杂的树形结构,每个节点都是一个 Python 对象,所有对象可以归纳为 4 种: BeautifulSoup(对应 HTML 文档), Tag(对应 HTML 标签), NavigableString(对应被 HTML 标签所修饰的字符串), Comment(对应注释及特殊字符串)。为了避免概念的混淆,我们把 Python 对象里的成员称为成员,把 HTML 原生文档中的标签称为标签,标签可以附加属性:
BeautifulSoup对象
BeautifulSoup 对象表示的是一个文档的全部内容,其成员大部分都是 Tag 对象:1
print(type(soup)) # <class 'bs4.BeautifulSoup'>
作为成员的 Tag 对象,其成员变量名与 XML 或 HTML 原生文档中的标签相同,可以通过 “.标签名” 的访问形式直接访问文档中第一个对应的标签:1
tag = soup.title
Tag对象
Tag 对象是 BeautifulSoup 对象的成员:1
2
3soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b
print(type(tag)) # <class 'bs4.element.Tag'>
Tag 对象中的 name 成员对应了原生文档的标签项,可以读取甚至修改:1
2
3print(tag.name) # b
tag.name = "blockquote"
print(tag) # <blockquote class="boldest">Extremely bold</blockquote>
在原生文档中的标签可以附带属性,如 <b class=”boldest”> 标签有一个 “class” 的属性,值为 “boldest”。转换成 Tag 对象后,可以通过字典的方式获取之,当然也可以修改:1
2print(tag['class']) # boldest
print(tag.attrs) # {'class': 'boldest'}
在 HTML4/5 的语法中,有的标签其属性可以取多个值,这个时候通过 Tag 对象的字典方式访问,将得到一个列表(是否返回一个列表取决于当前标签是否有取多个值的语法规范):1
2css_soup = BeautifulSoup('<p class="body strikeout"></p>')
css_soup.p['class'] # ["body", "strikeout"]
NavigableString对象
原生文档中被 HTML 标签修饰的字符串,BeautifulSoup 为它们专门定义了一个类,通过访问 Tag 对象的 string 成员就可以得到该标签修饰的字符串内容:1
2print(tag.string) # Extremely bold
print(type(tag.string)) # <class 'bs4.element.NavigableString'>
被 Tag对象修饰的字符串不能编辑,但是可以被替换成其它的字符串,用 replace_with() 方法:1
2tag.string.replace_with("No longer bold")
print(tag) # <blockquote>No longer bold</blockquote>
Comment对象
Comment 对象其实就是一种特殊的 NavigableString 对象:1
2
3
4
5markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup)
comment = soup.b.string
print(comment) # Hey, buddy. Want to buy a used parser?
print(type(comment)) # <class 'bs4.element.Comment'>
可以看到,通过 Tag 对象的 string 成员获取的字符串也包含了注释部分,但其类型却是 Comment,这样也就为我们区分内容提供了可能。
文档树
我们已经搞清楚了一个 HTML 原生文档中的所有要素是如何与 BeautifulSoup 相关对象对应的了,但是,原生文档中各标签的依赖关系也需要被还原,这也正是我们前面提到的 BeautifulSoup 将复杂 HTML 文档转换成一个复杂的树形结构,我们称其为文档树。通过对文档树进行 遍历 与 搜索,我们就能方便快捷地实现对原始网页的抽取与分析。
遍历文档树
我们还是以最开始的“爱丽丝梦游仙境”的文档来做实例:soup = BeautifulSoup(html_doc)
子节点
一个 Tag 对象可能包含多个字符串或其它的 Tag 对象,这些都是这个 Tag 对象的子节点。前面介绍了 BeautifulSoup 对象可以通过 “.标签名” 的方式直接访问文档中第一个对应的标签,这种模式同样适配于具有子节点的 Tag 对象:1
print(soup.body.b) # <b>The Dormouse's story</b>
Tag 对象的 .contents 成员可以将子节点以列表的方式输出:1
2
3
4
5
6
7head_tag = soup.head
print(head_tag) # <head><title>The Dormouse's story</title></head>
print(head_tag.contents) # [<title>The Dormouse's story</title>]
title_tag = head_tag.contents[0]
print(title_tag) # <title>The Dormouse's story</title>
print(title_tag.contents) # ['The Dormouse's story']
另外,通过 Tag 对象的 .children 生成器可以对其子节点进行循环:1
2for child in title_tag.children:
print(child) # The Dormouse's story
可以看到,上面的介绍的 Tag 对象的成员只能访问到直接子节点(即广度遍历),如果想要访问子孙节点(即深度遍历)则需要通过 .descendants 成员进行递归循环:1
2
3
4
5
6
7
8
9print(len(list(soup.children))) # 1
print(len(list(soup.descendants))) # 25
for child in head_tag.descendants:
print(child)
'''
<title>The Dormouse's story</title>
The Dormouse's story
'''
当一个 Tag 对象只有一个 NavigableString 对象的子节点(其包含的字符串),那么就可以使用 .string 成员获得该字符串:1
print(title_tag.string) # 'The Dormouse's story'
值得注意的是,当 Tag 对象有且仅有一个子节点,那么即便该子节点是一个 Tag 对象,也可以使用 .string 成员获得其包含的字符串:1
2
3
4print(head_tag.contents) # [<title>The Dormouse's story</title>]
print(head_tag.string) # 'The Dormouse's story'
print(soup.html.string) # None
相反,当 Tag 对象有多个子节点时,直接访问 .string 将得到一个 None 值,因为此时 Tag 对象不知道应该返回哪个字符串。但是,通过 .strings 成员可以一股脑地将当前 Tag 对象所有子节点所包含的字符串一并返回,包括原生文档中的空格或换行都会被返回,另一个成员 .stripped_strings 则可以返回不带空格或换行的结果。
父节点
一个 Tag 对象有“子节点”,自然也可能存在“父节点”,通过 .parent 成员就可以获取,相关的概念和原理与子节点是一致的:1
2
3
4print(title_tag.string.parent) # <title>The Dormouse's story</title>
html_tag = soup.html
print(type(html_tag.parent)) # <class 'bs4.BeautifulSoup'>
可以看到,作为文档树最顶层的 Tag 对象(<html>),其父节点是一个 BeautifulSoup 对象,可以想象该 BeautifulSoup 对象的父节点一定是 None。同样的,通过 .parents 成员可以迭代访问所有的父节点。
兄弟节点
在文档树中,使用 .next_sibling 和 .previous_sibling 属性来查询兄弟节点:1
2
3sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>", 'html.parser')
print(sibling_soup.b.next_sibling) # <c>text2</c>
print(sibling_soup.c.previous_sibling) # <b>text1</b>
上面的实例是符合我们预期的,因为传入 BeautifulSoup 的页面是没有换行的,而真实的网页是不可能没有换行的:1
2
3<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>;
上面是开篇所示的样例文档的源码片段,第一个 <a> 标签的 .next_sibling 并不是第二个 <a> 标签,而是逗号和换行符(即与第二个 <a> 标签中间插入的所有内容),它的再下一个兄弟节点才是我们预期的第二个 <a> 标签(这是一个巨坑)。
另外,通过 .next_siblings 和 .previous_siblings 属性可以对当前节点的兄弟节点迭代输出:1
2
3
4
5
6
7
8
9
10for 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>
';\nand they lived at the bottom of a well.'
'''
回退与前进
.next_element 属性指向解析过程中下一个被解析的对象(字符串或tag),而 .previous_element 属性指向当前被解析的对象的前一个解析对象。如何理解“解析”呢?我们来看一段 html 的源码:1
2<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>标签”等事件。那么:1
2
3
4last_a_tag = soup.find("a", id="link3")
print(last_a_tag) # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
print(last_a_tag.next_element) # Tillie
由于找到最后一个 <a> 标签的下一个事件是“添加 Tillie 这段文字”,所以返回是 Tillie。可以想象,当某些事件缺失的时候 .next_element 的结果可能会与 .next_sibling 相同。
另外, 通过 .next_elements 和 .previous_elements 可以迭代地向前或向后访问文档的解析内容。1
2
3
4
5
6
7
8
9
10
11for element in last_a_tag.next_elements:
print(repr(element))
'''
'Tillie'
';\nand they lived at the bottom of a well.'
'\n'
<p class="story">...</p>
'...'
'\n'
'''
搜索文档树
搜索文档树的方法都是以 find 开头的,且它们都具有相同的参数格式,单独 find 的函数返回的是第一个满足条件的 Tag对象,带有 find_all 的函数则返回所有满足条件的 Tag对象组成的列表。
find ( name , attrs , recursive , text , **kwargs )
find 相关的方法其参数都是如上的格式:
- name参数:指明查找的 Tag对象,其取值可以是如下几种过滤器:
- 字符串:BeautifulSoup 会查找与字符串完整匹配的 Tag对象
- 正则表达式:BeautifulSoup 会通过正则表达式的 match() 来匹配内容
- 列表:BeautifulSoup 会将与列表中任一元素匹配的 Tag对象返回
- 方法:只能是一个返回 True/False 的方法,且只接收一个参数,BeautifulSoup 会返回所有满足该函数为 True 的 Tag对象
- True:可以匹配任何 Tag对象
- attr参数:指明查找的 Tag对象中的属性,可以按照关键词模式进行赋值,也可以采用 key-value 的形式传入。值得注意的是,可以在第一个参数位置上传入 attr 参数,因为该参数的固定格式可以被 BeautifulSoup 识别出来
- recursive参数:是否搜索当前 Tag对象的所有子孙节点,默认是 True;如果设置为 False,则只搜索其直接子节点
- text参数:指定查找的字符串内容,其取值同样支持字符串、正则表达式、列表、方法、True等过滤器
- limit参数:指定返回查找到的 Tag对象的数量
上面梳理清楚了搜索文档树相关接口的参数含义,下面我们还是以本文中使用的“爱丽丝梦游仙境”的文档作为实例,总结一下具体的应用方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33# name参数使用字符串作为过滤器
print(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>]
'''
print(soup.html.find_all("title")) # [<title>The Dormouse's story</title>]
print(soup.html.find_all("title", recursive=False)) # []
print(soup.find_all("a", text="Elsie")) # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
# name参数使用正则表达式作为过滤器
import re
for tag in soup.find_all(re.compile("^h")):
print(tag.name)
'''
html
head
'''
# name参数使用列表作为过滤器
print(soup.find_all(["a", "b"], limit=1))
'''
[<b>The Dormouse's story</b>]
'''
# name参数使用方法作为过滤器
def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')
print(soup.find(has_class_but_no_id))
'''
<p class="title"><b>The Dormouse's story</b></p>
'''
可以看到,find_all 返回的都是符合查找条件的 Tag对象的列表,哪怕限制了 limit=1,也是返回一个只有一个元素的列表;而 find 返回的是单个 Tag对象。
下面我们来看一下 attr 参数的具体使用实例:1
2
3
4
5
6
7
8
9
10
11
12
13# 使用关键词模式
print(soup.find_all(id='link2'))
print(soup.find_all(href=re.compile("elsie"), id='link1'))
# 使用key-value模式
data_soup = BeautifulSoup('<div data-foo="value">foo!</div>')
print(data_soup.find_all(attrs={"data-foo": "value"}))
'''
[<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
[<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
[<div data-foo="value">foo!</div>]
'''
使用关键词模式,只要关键词与 Python 语法的保留字不冲突就可以直接使用;而上面举例的 data-* 属性是 HTML5 的语法,这种属性则无法通过关键词模式进行搜索,但是 key-value 模式是万能的。另外,一种很常用的搜索方式是按照 CSS 类名进行搜索,这里需要注意的是,CSS 类名的关键字 class 在 Python 中是保留字,我们需要转义为 class_ (或者用 key-value 模式):1
2
3
4
5
6
7print(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>]
'''
与前面遍历文档树介绍的关于父辈节点、兄弟节点一样,搜索也可以在当前节点的父兄节点中进行,对应的函数是 find_parents() 和 find_next_siblings()、find_previous_siblings() 等,它们的参数定义与用法也都是相同的。
其他
以上就是 BeautifulSoup 中我们最常用的功能了,可以完全满足我们解析与抽取网页内容的工程需求。当然还有一些功能,我们简单介绍一下,尽管它们并不常用:
- CSS选择器:BeautifulSoup 支持大部分的 CSS选择器,在 Tag 或 BeautifulSoup 对象的 .select() 方法中传入字符串参数,即可使用 CSS选择器的语法找到相应的 Tag
- 修改文档树:被 BeautifulSoup 解析出来的文档树,可以按照对象属性的方式进行赋值修改,当然也有一些增删的方法可供调用(如:tag.append()向当前节点内添加内容、tag.decompose()将当前节点移除文档树等)
- 输出:prettify() 方法将 BeautifulSoup 的文档树格式化后以 Unicode 编码输出,每个XML/HTML标签都独占一行
如果只想得到 Tag对象 中包含的文本内容,那么可以调用 get_text() 方法,这个方法获取到 Tag 中包含的所有文本内容,包括子孙 Tag 中的内容,并将结果作为 Unicode 字符串返回。