目录
前言
在之前的内容中,我们深入探讨了正则表达式的多种用法。然而,正则表达式并非十全十美,一旦出现编写错误或逻辑漏洞,最终得到的结果往往与我们的预期大相径庭。
网页作为一种具有特定结构和层级关系的信息载体,许多节点都通过 id 或 class 进行了明确区分。那么,能否利用这些网页的结构和属性特点,找到一种更为高效、准确的提取方法呢?
在本节中,我们将为大家介绍一个强大的网页解析工具 ——Beautiful Soup。它能够依据网页的结构和属性等特性,轻松地对网页进行解析。使用 Beautiful Soup,我们无需再编写复杂冗长的正则表达式,只需几条简洁明了的语句,就能完成网页中特定元素的提取工作。
接下来,让我们一起深入了解 Beautiful Soup 的强大功能,开启高效解析网页的新篇章!
一、Beautiful Soup 简介
1.1 Beautiful Soup概述
简单来说,BeautifulSoup 是 Python 语言中一个专门用于解析 HTML 或 XML 的强大库。它提供了极为便捷的方式,能够轻松地从网页中抽取我们所需的数据。其官方对 BeautifulSoup 的解释为:
BeautifulSoup 提供了一系列简单易用、充满 Python 风格的函数,这些函数可以实现导航、搜索、修改分析树等多种功能。它就像一个功能齐全的工具箱,通过对文档进行解析,为用户精准地提供所需抓取的数据。而且,BeautifulSoup 的使用非常简便,只需少量的代码,就能构建出一个功能完整的应用程序。
此外,BeautifulSoup 还具备自动转换编码的功能。它会自动将输入的文档转换为 Unicode 编码,输出的文档则转换为 utf - 8 编码。当然,如果文档本身没有指定编码方式,用户只需简单说明原始编码方式即可。
如今,BeautifulSoup 已经与 lxml、html5lib 等一样,成为了出色的 Python 解释器。它能够为用户灵活地提供不同的解析策略,并且在解析速度方面表现强劲。
由此可见,借助 Beautiful Soup,我们可以大大简化繁琐的网页数据提取工作,显著提升解析效率。
1.2 准备工作
在开始使用 Beautiful Soup 进行网页解析之前,请务必确保已经正确安装了 Beautiful Soup 和 lxml 库。如果还没有安装,请自行安装。
1.3 解析器
Beautiful Soup 在进行网页解析时,实际上是依赖于解析器来完成工作的。它不仅支持 Python 标准库中自带的 HTML 解析器,还兼容一些功能强大的第三方解析器,比如 lxml。下面的表详细列出了 Beautiful Soup 所支持的各种解析器。
Beautiful Soup支持的解析器
通过上述对比可以清晰地看出,lxml 解析器不仅具备解析 HTML 和 XML 的双重功能,而且在速度方面表现出色,同时还具有很强的容错能力。因此,我们强烈推荐使用 lxml 解析器。
如果选择使用 lxml 解析器,在初始化 Beautiful Soup 对象时,只需将第二个参数设置为 lxml 即可。示例代码如下:
from bs4 import BeautifulSoup
soup = BeautifulSoup('<p>Hello</p>', 'lxml')
print(soup.p.string)
在后续关于 Beautiful Soup 的用法实例演示中,我们将统一采用 lxml 解析器进行讲解。
二、基本使用
下面,我们通过一个具体的实例来深入了解 Beautiful Soup 的基本使用方法:
html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><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, 'lxml')
print(soup.prettify())
print(soup.title.string)
运行结果:
在这个例子中,我们首先定义了一个名为 html 的变量,它存储了一段 HTML 字符串。需要注意的是,这段字符串并不是一个完整的 HTML 字符串,其中的 body 和 html 节点都没有闭合。
接下来,我们将 html 字符串作为第一个参数传递给 BeautifulSoup 对象,同时将第二个参数设置为解析器类型 “lxml”,这样就完成了 BeautifulSoup 对象的初始化,并将其赋值给了 soup 变量。
初始化完成后,我们就可以调用 soup 对象的各种方法和属性,来对这段 HTML 代码进行解析了。
首先,我们调用了 prettify () 方法。这个方法的作用是将待解析的字符串以标准缩进格式输出,使代码结构更加清晰易读。需要特别注意的是,在输出结果中,我们可以看到原本未闭合的 body 和 html 节点都已经自动闭合了。这并不是 prettify () 方法的作用,而是在初始化 BeautifulSoup 对象时,它就已经自动对不规范的 HTML 字符串进行了格式校正。
然后,我们调用了 soup.title.string。这里的 soup.title 可以选中 HTML 中的 title 节点,而 string 属性则用于获取该节点中的文本内容。通过这两个简单的属性调用,我们就轻松完成了文本的提取工作,充分体现了 Beautiful Soup 的便捷性。
三、节点选择器的使用
在 Beautiful Soup 中,直接调用节点的名称就可以选择对应的节点元素,再调用 string
属性就能够获取节点内的文本内容。这种选择方式的优点是速度非常快,特别适用于单个节点结构层次清晰的情况。
3.1 选择元素
下面,我们通过一个具体的例子来详细说明如何选择元素:
html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><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, 'lxml')
print(soup.title)
print(type(soup.title))
print(soup.title.string)
print(soup.head)
print(soup.p)
运行结果:
在这个例子中,我们仍然使用了之前的 HTML 代码。首先,我们打印输出了 title 节点的选择结果,可以看到输出的正是 title 节点及其内部的文字内容。
接着,我们输出了 title 节点的类型,结果是 bs4.element.Tag
类型。这是 Beautiful Soup 中一个非常重要的数据结构,经过选择器选择后的结果,通常都是这种 Tag 类型。
Tag 类型具有一些实用的属性,比如 string 属性。当我们调用 string 属性时,就可以获取到节点的文本内容,这也正是接下来的输出结果。
然后,我们尝试选择了 head 节点,输出结果显示的是 head 节点及其内部的所有内容。
最后,我们选择了 p 节点。但这里需要注意的是,输出结果仅仅是第一个 p 节点的内容,后面的几个 p 节点并没有被选中。这表明,当存在多个相同节点时,这种直接调用节点名称的选择方式只会选择到第一个匹配的节点,而忽略后面的其他节点。
3.2 提取信息
在前面的演示中,我们展示了如何通过调用 string
属性来获取节点文本的值。那么,如何获取节点属性的值呢?又如何获取节点的名称呢?下面,我们将对这些信息的提取方式进行统一梳理。
3.2.1 获取名称
在 Beautiful Soup 中,可以利用 name
属性来获取节点的名称。我们还是以上面的文本为例,选取 title 节点,然后调用 name
属性,就可以得到节点的名称:
print(soup.title.name)
运行结果:
title
3.2.2 获取属性
每个节点都可能拥有多个属性,比如常见的 id 和 class 等。在选择了一个节点元素后,可以通过调用 attrs
方法来获取该节点的所有属性:
print(soup.p.attrs)
print(soup.p.attrs['name'])
运行结果:
从运行结果可以看出,attrs
的返回结果是一个字典形式,它将选择的节点的所有属性和属性值组合成了一个字典。
如果我们想要获取某个特定的属性值,比如 name 属性,就相当于从字典中获取某个键值,只需要使用中括号加上属性名即可。例如,要获取 name 属性的值,可以通过 attrs['name']
来实现。
其实,还有一种更简便的获取属性值的方式,我们可以不用写 attrs,直接在节点元素后面加上中括号,传入属性名就可以获取属性值了。示例代码如下:
print(soup.p['name'])
print(soup.p['class'])
运行结果如下:
dromouse
['title']
在这里需要注意的是,不同属性的返回结果类型可能不同。有的返回结果是字符串,比如 name 属性的值是唯一的,所以返回的是单个字符串;而对于 class 属性,一个节点元素可能有多个 class,因此返回的是一个由字符串组成的列表。在实际处理过程中,我们需要根据具体情况判断属性值的类型。
3.2.3 获取内容
我们可以利用 string
属性来获取节点元素所包含的文本内容。例如,要获取第一个 p 节点的文本内容,可以使用以下代码:
print(soup.p.string)
运行结果如下:
The Dormouse's story
再次强调,这里选择到的 p 节点是第一个 p 节点,获取的文本内容也是第一个 p 节点内部的文本。
3.3 嵌套选择
在前面的例子中,我们知道每一个通过选择器得到的返回结果都是 bs4.element.Tag
类型。这种类型的对象同样可以继续调用节点进行下一步的选择,也就是所谓的嵌套选择。
例如,我们先获取了 head 节点元素,然后可以继续调用 head 节点内部的 title 节点元素:
html = """
<html><head><title>The Dormouse's story</title></head>
<body>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.head.title)
print(type(soup.head.title))
print(soup.head.title.string)
运行结果如下:
第一行输出结果是我们调用 head 之后再次调用 title 而选择的 title 节点元素。然后,我们打印输出了它的类型,可以看到它仍然是 bs4.element.Tag
类型。
这说明,我们在 Tag 类型的基础上再次进行选择得到的结果依然是 Tag 类型。每次返回的结果类型都相同,这就为我们进行嵌套选择提供了可能。
最后,我们输出了它的 string
属性,也就是该节点里的文本内容。
3.4 关联选择
在实际进行节点选择时,有时候我们无法一步就选到想要的节点元素,而是需要先选中某一个节点元素,然后以它为基准,再去选择它的子节点、父节点、兄弟节点等。下面,我们就来详细介绍如何选择这些关联的节点元素。
3.4.1 子节点和子孙节点
在选取了一个节点元素之后,如果我们想要获取它的直接子节点,可以调用 contents
属性。示例代码如下:
html = """
<html>
<head>
<title>The Dormouse's story</title>
</head>
<body>
<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">
<span>Elsie</span>
</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, 'lxml')
print(soup.p.contents)
运行结果如下:
从运行结果可以看出,返回的结果是一个列表形式。p 节点中既包含了文本内容,又包含了其他节点元素,最终这些内容都会以列表的形式统一返回。
需要注意的是,列表中的每个元素都是 p 节点的直接子节点。例如,第一个 a 节点里面包含了一层 span 节点,这相当于 p 节点的孙子节点,但在返回结果中,并没有单独把 span 节点选出来。也就是说,contents
属性得到的结果是直接子节点的列表。
同样,我们也可以调用 children
属性来得到相应的结果:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.p.children)
for i, child in enumerate(soup.p.children):
print(i, child)
运行结果如下:
这里我们使用的是同样的 HTML 文本,调用 children 属性后,返回的结果是一个生成器类型。接下来,我们通过 for 循环遍历输出了相应的内容。
如果我们想要得到所有的子孙节点,可以调用 descendants
属性:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.p.descendants)
for i, child in enumerate(soup.p.descendants):
print(i, child)
运行结果如下:
此时返回的结果依然是一个生成器。通过遍历输出可以看到,这次的输出结果包含了 span 节点。这是因为 descendants 属性会递归查询所有子节点,从而得到所有的子孙节点。
3.4.2 父节点和祖先节点
如果我们要获取某个节点元素的父节点,可以调用 parent
属性。示例代码如下:
html = """
<html>
<head>
<title>The Dormouse's story</title>
</head>
<body>
<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">
<span>Elsie</span>
</a>
</p>
<p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.a.parent)
运行结果如下:
在这个例子中,我们选择的是第一个 a 节点的父节点元素。很明显,a 节点的父节点是 p 节点,所以输出结果就是 p 节点及其内部的内容。
需要注意的是,这里输出的仅仅是 a 节点的直接父节点,并没有再向外寻找父节点的祖先节点。如果我们想要获取所有的祖先节点,可以调用 parents
属性:
html = """
<html>
<body>
<p class="story">
<a href="http://example.com/elsie" class="sister" id="link1">
<span>Elsie</span>
</a>
</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(type(soup.a.parents))
print(list(enumerate(soup.a.parents)))
运行结果如下:
从运行结果可以发现,返回的结果是生成器类型。我们通过将其转换为列表,并输出了它的索引和内容,列表中的元素就是 a 节点的祖先节点。
3.4.3 兄弟节点
前面我们介绍了子节点和父节点的获取方式,那么如果要获取同级的节点,也就是兄弟节点,应该怎么做呢?下面是一个示例:
html = """
<html>
<body>
<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">
<span>Elsie</span>
</a>
Hello
<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>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print('Next Sibling', soup.a.next_sibling)
print('Prev Sibling', soup.a.previous_sibling)
print('Next Siblings', list(enumerate(soup.a.next_siblings)))
print('Prev Siblings', list(enumerate(soup.a.previous_siblings)))
运行结果如下:
这里调用了 4 个属性,next_sibling
和previous_sibling
分别用于获取节点的下一个和上一个兄弟元素。如果目标节点没有对应的兄弟节点,previous_sibling
可能返回None
,就像这里第一个<a>
标签的previous_sibling
。next_siblings
和previous_siblings
则分别返回后面和前面的兄弟节点集合,返回结果是生成器类型,通过list()
函数将其转换为列表以便查看所有兄弟节点。
3.4.4 提取信息
刚才我们讲了怎么选择那些有关联的元素节点。要是现在想得到这些节点的文本内容、属性之类的信息,方法和前面介绍的差不多。下面通过例子来说明:
html = """
<html>
<body>
<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">Bob</a><a href="http://example.com/lacie" class="sister" id="link2">Lacie</a>
</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print('Next Sibling:')
print(type(soup.a.next_sibling))
print(soup.a.next_sibling)
print(soup.a.next_sibling.string)
print('Parent:')
print(type(soup.a.parents))
print(list(soup.a.parents)[0])
print(list(soup.a.parents)[0].attrs['class'])
运行结果:
要是选择节点的返回结果只有一个节点,那直接用string、attrs这些属性,就能拿到它的文本内容和属性信息。但要是返回的是一个能生成多个节点的生成器,就得先把它变成列表,从里面挑出想要的元素,接着再用string、attrs属性,这样才能得到对应节点的文本和属性。