Beautiful Soup - HTML解析利器
# Beautiful Soup - HTML解析利器
我们已经用 requests
库成功获取了网页的 HTML 源代码,但它本质上是一长串混杂着各种标签的字符串。如何从这团“乱麻”中精准地提取出我们需要的数据,比如文章标题、商品价格、图片链接呢?Beautiful Soup
就是解决这个问题的最佳工具。
Beautiful Soup
(简称 BS4) 是一个可以从 HTML 或 XML 文件中提取数据的 Python 库。它将复杂的文档转换成一个树形结构,每个节点都是 Python 对象,极大地简化了文档的遍历和数据提取过程。
# 1. 安装 Beautiful Soup 及解析器
Beautiful Soup
自身只负责解析文档和遍历节点,但它需要一个底层的**解析器 (Parser)**来真正完成 HTML 的解析工作。你需要安装两个库:beautifulsoup4
和一个解析器。
推荐使用 lxml
解析器,因为它速度快、容错能力强。
# 同时安装 bs4 库和 lxml 解析器
pip install beautifulsoup4 lxml
2
Python 内置了 html.parser
,但 lxml
在各方面都更胜一筹,因此强烈推荐安装和使用 lxml
。
# 2. 创建 BeautifulSoup 对象
要使用 Beautiful Soup
,首先要将一段 HTML 文档字符串传入它的构造函数,创建一个 BeautifulSoup
对象。
import requests
from bs4 import BeautifulSoup
# 准备一段 HTML 示例代码
html_doc = """
<html><head><title>一个简单的示例页面</title></head>
<body>
<p class="story">
很久很久以前,有三个姐妹,她们的名字是:
<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>;
她们住在一个小房子的底部。
</p>
<p class="story">...</p>
<div id="footer">这是页脚</div>
</body></html>
"""
# 1. 创建 BeautifulSoup 对象
# 第一个参数是 HTML 字符串
# 第二个参数是所使用的解析器,'lxml' 是最佳选择
soup = BeautifulSoup(html_doc, 'lxml')
# 2. .prettify() 方法可以将解析后的 HTML 格式化输出,带缩进,方便查看
print(soup.prettify())
# 3. 我们可以轻松地获取一些基本信息
print(f"页面的标题是: {soup.title}") # <title>一个简单的示例页面</title>
print(f"标题的标签名: {soup.title.name}") # title
print(f"标题的文本内容: {soup.title.string}") # 一个简单的示例页面
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
在实际爬虫中,html_doc
通常来自 requests.get(url).text
。
# 3. Beautiful Soup 的四大对象
Beautiful Soup
将复杂的 HTML 文档转换成四种主要的对象类型,理解它们是高效使用的关键。
Tag
: HTML 中的标签,如<p>
,<a>
等。这是我们打交道最多的对象。NavigableString
: 标签内部的文本内容。BeautifulSoup
: 整个文档的根节点,可以看作是一个特殊的Tag
。Comment
: HTML 中的注释,一个特殊的NavigableString
。
# Tag 对象
tag_p = soup.p
print(f"第一个 p 标签是: {type(tag_p)}") # <class 'bs4.element.Tag'>
# NavigableString 对象
title_string = soup.title.string
print(f"标题内容是: {type(title_string)}") # <class 'bs4.element.NavigableString'>
# BeautifulSoup 对象
print(f"soup 对象本身是: {type(soup)}") # <class 'bs4.BeautifulSoup'>
2
3
4
5
6
7
8
9
10
# 4. 遍历文档树
Beautiful Soup
的核心功能之一就是让你能像操作 Python 对象一样轻松地在文档树的节点间穿梭。
# 4.1 访问子节点
.contents
: 获取一个Tag
的所有直接子节点,返回一个列表。.children
: 获取一个Tag
的所有直接子节点,返回一个可迭代的生成器。
# 获取 body 标签的所有直接子节点
body_contents = soup.body.contents
print(f"body 的子节点数量: {len(body_contents)}")
print(body_contents) # 注意,换行符也被解析为一个 NavigableString
# 使用 .children 生成器进行迭代,更节省内存
for child in soup.body.children:
if child.name: # 过滤掉非标签节点(如换行符)
print(f"找到一个子标签: <{child.name}>")
2
3
4
5
6
7
8
9
# 4.2 访问父节点
.parent
: 获取一个节点的直接父节点。
title_tag = soup.title
print(f"title 标签的父节点是: {title_tag.parent.name}") # head
2
# 4.3 访问兄弟节点
.next_sibling
: 获取当前节点的下一个直接兄弟节点。.previous_sibling
: 获取当前节点的上一个直接兄弟节点。
link1 = soup.find(id="link1") # 先找到第一个 a 标签
# 注意:兄弟节点可能是换行符或者普通文本,不一定是标签
next_sibling = link1.next_sibling.strip()
print(f"link1 的下一个兄弟节点内容: '{next_sibling}'")
next_tag_sibling = link1.find_next_sibling('a') # 寻找下一个 a 标签兄弟
print(f"link1 的下一个兄弟标签是: {next_tag_sibling}")
2
3
4
5
6
7
# 5. 搜索文档树 (核心功能)
遍历虽然直观,但效率低下。Beautiful Soup
提供了强大的搜索方法 find()
和 find_all()
,它们是数据提取的关键。
find_all(name, attrs, string, limit, ...)
: 查找所有匹配条件的Tag
,返回一个列表。find(name, attrs, string, ...)
: 只查找第一个匹配条件的Tag
,相当于find_all
设置limit=1
,返回一个Tag
对象或None
。
# 5.1 按标签名 (name
) 搜索
# 查找所有的 a 标签
all_a_tags = soup.find_all('a')
for tag in all_a_tags:
print(tag)
# 查找第一个 p 标签
first_p = soup.find('p')
print(f"\n第一个 p 标签: {first_p}")
2
3
4
5
6
7
8
# 5.2 按 CSS 类名 (class_
) 搜索
由于 class
是 Python 的关键字,所以在 Beautiful Soup
中,我们使用 class_
参数来按类名搜索。
# 查找所有 class="sister" 的标签
sister_tags = soup.find_all(class_="sister")
for tag in sister_tags:
print(tag)
2
3
4
# 5.3 按属性 (attrs
) 搜索
你可以传递一个字典给 attrs
参数来搜索具有特定属性的标签。
# 查找所有带有 href 属性的 a 标签
tags_with_href = soup.find_all('a', attrs={'href': True})
# 查找 id="link2" 的标签
link2 = soup.find(id="link2") # 这是一个简写形式
# 完整写法是: soup.find(attrs={'id': 'link2'})
print(f"\nID为link2的标签: {link2}")
# 查找 href="http://example.com/lacie" 的标签
link_lacie = soup.find(href="http://example.com/lacie") # 也是简写
print(f"href为.../lacie的标签: {link_lacie}")
2
3
4
5
6
7
8
9
10
11
# 5.4 按文本内容 (string
) 搜索
你可以直接搜索与字符串完全匹配的文本内容。
# 查找文本内容为 "Lacie" 的标签
lacie_text_tag = soup.find(string="Lacie")
print(f"\n文本为'Lacie'的标签: {lacie_text_tag}")
print(f"它的父标签是: {lacie_text_tag.parent}")
2
3
4
# 6. CSS 选择器 (更现代、更高效的搜索方式)
如果你熟悉 CSS 或者 jQuery,你会爱上 Beautiful Soup
的 .select()
方法。它允许你使用 CSS 选择器语法来查找标签,非常简洁、强大。
.select(selector)
: 根据 CSS 选择器返回一个包含所有匹配Tag
的列表。.select_one(selector)
: 只返回第一个匹配的Tag
。
选择器语法 | 含义 | 示例 |
---|---|---|
tag | 按标签名 | soup.select('title') |
.class | 按类名 | soup.select('.story') |
#id | 按 ID | soup.select('#link1') |
parent > child | 查找直接子元素 | soup.select('p > a') |
ancestor descendant | 查找后代元素(不一定是直接子元素) | soup.select('body a') |
[attribute=value] | 按属性值 | soup.select('a[href="..."]') |
# 使用 CSS 选择器查找所有 class="sister" 的 a 标签
sisters = soup.select('a.sister')
print("--- 使用 CSS 选择器 ---")
for s in sisters:
print(s)
# 查找 id="footer" 的 div 标签
footer = soup.select_one('#footer')
print(f"页脚标签: {footer}")
# 查找 p 标签下的所有直接子代 a 标签
links_in_p = soup.select('p > a')
print(f"p 标签下的链接数量: {len(links_in_p)}")
2
3
4
5
6
7
8
9
10
11
12
13
find_all
vs .select
:
- 两者功能相似,都能完成大部分任务。
.select
语法更简洁,特别是对于复杂的层级关系和属性组合。find_all
在某些高级场景下(如使用正则表达式、自定义函数搜索)更灵活。- 日常使用中,推荐优先考虑
.select()
和.select_one()
。
# 7. 从标签中提取信息
找到标签后,我们的最终目的是提取里面的数据。
# 7.1 提取文本内容 (.get_text()
或 .string
)
.string
: 如果一个标签只有一个NavigableString
子节点,你可以用.string
获取它。否则,返回None
。.get_text()
: 获取一个标签内所有的文本内容,包括其所有子孙节点的文本,并将它们拼接成一个字符串。
p_tag = soup.find('p', class_='story')
# .string 对这个复杂的 p 标签无效,因为它有多个子节点
print(f"p 标签的 .string: {p_tag.string}") # 输出: None
# .get_text() 可以提取所有文本
# separator 参数可以在不同文本块之间插入分隔符
# strip=True 可以去除开头和结尾多余的空白
p_text = p_tag.get_text(separator='|', strip=True)
print(f"\np 标签的 .get_text():\n{p_text}")
2
3
4
5
6
7
8
9
10
# 7.2 提取属性值
可以像操作 Python 字典一样获取标签的属性。
link1 = soup.find(id="link1")
# 获取 href 属性
href = link1['href']
print(f"\nlink1 的 href 属性是: {href}")
# 获取 class 属性(注意,class 可能有多个值,返回一个列表)
css_class = link1['class']
print(f"link1 的 class 是: {css_class}") # 输出: ['sister']
# 安全地获取属性,如果不存在也不会报错
non_existent_attr = link1.get('data-custom')
print(f"一个不存在的属性: {non_existent_attr}") # 输出: None
2
3
4
5
6
7
8
9
10
11
12
13
# 8. 综合案例:爬取并解析一个真实的页面
让我们结合 requests
和 Beautiful Soup
来爬取豆瓣电影 Top250 的第一页。
import requests
from bs4 import BeautifulSoup
# 1. 目标 URL 和请求头
url = "https://movie.douban.com/top250"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
# 2. 发送请求并获取 HTML
response = requests.get(url, headers=headers)
if response.status_code == 200:
# 3. 创建 BeautifulSoup 对象
soup = BeautifulSoup(response.text, 'lxml')
# 4. 使用 .select() 定位到所有电影的条目
# 通过浏览器开发者工具分析,我们发现每个电影信息都包含在一个 class="item" 的 div 中
all_movies = soup.select('div.item')
# 5. 遍历每个电影条目,提取信息
for movie in all_movies:
# 提取排名
rank = movie.select_one('em').get_text()
# 提取电影标题
# 标题在 class="title" 的 span 标签里
title = movie.select_one('span.title').get_text()
# 提取评分
# 评分在 class="rating_num" 的 span 标签里
rating = movie.select_one('span.rating_num').get_text()
# 提取引言 (可能没有)
quote_tag = movie.select_one('span.inq')
quote = quote_tag.get_text() if quote_tag else "无"
print(f"排名: {rank}\n电影: {title}\n评分: {rating}\n引言: {quote}\n" + "-"*20)
else:
print(f"请求失败,状态码: {response.status_code}")
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
34
35
36
37
38
39
40
掌握了 requests
和 Beautiful Soup
,你就拥有了爬取绝大多数静态网页数据的能力。