Beautiful Soup 是一种广泛使用的 Python 库,用于读取、搜索并提取 HTML 和 XML 中的信息,能将杂乱的标记转换为可遍历的树状结构,适用于网络数据抓取、内容分析、测试和数据清理等场景。
它的主要优势在于简单易上手的 API、灵活的搜索方法、对 CSS 选择器的支持,以及可与多种解析器后端配合使用的能力。如果您需要在不手动处理原始标签的情况下采集 HTML 数据,Beautiful Soup 是一个简单易用的入门选择。
安装 Beautiful Soup
在解析 HTML 之前,您需要先安装 Python 和 Beautiful Soup 软件包,打开终端或命令提示符并执行以下命令:
pip install beautifulsoup4
该软件包的名称为 beautifulsoup4,而通常导入的类名为 BeautifulSoup:
from bs4 import BeautifulSoup
本指南中会反复出现 from bs4 import BeautifulSoup 这行代码,因为它是大多数脚本的标准起点。
对于基础用法,Python 内置的 html.parser 已经足够:
from bs4 import BeautifulSoup
html = "<html><body><h1>Hello</h1></body></html>"
soup = BeautifulSoup(html, "html.parser")
print(soup.h1.get_text())
soup 变量即为您的 Beautiful Soup 对象。此后您无需再将页面视为一长串字符串,而是可以搜索其 HTML 结构、在父子节点间移动、读取属性并提取文本。
选择合适的解析器:html.parser 与 lxml、html5lib 的比较
Beautiful Soup 本身并非 HTML 解析器,而是在多种解析库之上提供了一套易用的 API。所选解析器会影响性能、依赖要求以及对标记错误的修正方式,且每种 HTML 解析器的容错规则也略有不同。
html.parser
Python 内置
无需额外依赖
小型脚本与简单页面
lxml
pip install lxml
速度快、效率高
大规模数据抓取任务
html5lib
pip install html5lib
类似浏览器的容错处理
严重损坏或不规范的标记
使用 lxml:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "lxml")
使用 html5lib:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html5lib")
如需处理 XML,请安装 lxml,并将解析器参数设为 "xml" 或 "lxml-xml"。这一区别十分重要,因为 XML 规则与 HTML 规则不同,尤其是在自闭合标签和大小写敏感性方面。
这几种解析器并无绝对优劣之分:html.parser 使用方便,lxml 在性能方面表现出色,而 html5lib 则擅长按照 HTML5 规范进行容错处理。
关键在于保持一致性:面对损坏的 HTML 标记,不同解析器可能会生成不同的树状结构。如果您的爬虫依赖页面中特定的路径,请显式指定解析器,而非使用默认设置。
解析 HTML 文档
环境配置完成后,整体流程十分简单:输入内容、创建 BeautifulSoup 对象、查找所需部分,并将其转换为可用数据。无论输入来自文件、字符串还是网络上的页面,这一流程都是通用的。
加载 HTML 文件或字符串
我们从一个简单的内存字符串示例开始:
from bs4 import BeautifulSoup
html = """
<html>
<body>
<h1>Proxy Guide</h1>
<p class="summary">A short introduction.</p>
</body>
</html>
"""
soup = BeautifulSoup(html, "html.parser")
print(soup.h1.get_text(strip=True))
Beautiful Soup 会读取 HTML 文档并构建树状结构,随后您可以像访问属性一样访问第一个匹配的 HTML 标签,如 soup.h1 所示。
对于本地文件,打开文件并将文件句柄传递给 Beautiful Soup:
from bs4 import BeautifulSoup
with open("page.html", "r", encoding="utf-8") as file:
soup = BeautifulSoup(file, "html.parser")
print(soup.title.get_text(strip=True))
当您已下载页面,并希望将采集过程与数据解析分开时,这种方式尤为适用。使用已保存的文件还有助于调试,因为每次运行时输入内容都保持不变。
如需解析 URL,可将 Beautiful Soup 与 Requests 库结合使用:
import requests
from bs4 import BeautifulSoup
url = "https://example.com/"
response = requests.get(url, timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.content, "html.parser")
print(soup.title.get_text(strip=True))
Requests 负责获取页面,待响应返回后再由 Beautiful Soup 负责解析 HTML。raise_for_status() 会在 HTTP 响应失败时终止脚本,而 timeout 则可防止请求无限等待网络响应。
需要注意的是,Requests 只会下载服务器返回的内容。如果所需内容是在加载 HTML 文档后由 JavaScript 动态生成的,该内容可能并不会出现在文档本身之中。
这种情况下,建议先分析响应内容,再对选择器是否有效下结论。
遍历解析树
解析 HTML 代码后,您可以通过多种方式遍历生成的树状结构,其中最快捷的是直接访问 HTML 标签:
title = soup.h1
first_paragraph = soup.p
该方法会返回找到的第一个 HTML 标签,适用于已知结构的网页,但大多数爬虫场景需要更精确的查找方式。
.find() 方法会返回第一个匹配结果:
article = soup.find("article")
您还可以按属性进行筛选:
price = soup.find("span", class_="price")
product = soup.find("div", id="product-42")
由于 class 是 Python 的关键字,Beautiful Soup 在方法参数中使用 class_ 来代替。
如需获取所有匹配的元素,可使用 .find_all():
links = soup.find_all("a")
for link in links:
print(link.get_text(strip=True))
find_all() 会返回一个类似列表的结果,您可以对其进行遍历,也可以传入数量限制和属性筛选条件:
cards = soup.find_all("div", class_="product-card", limit=10)
CSS 选择器是另一种实用的方式:使用 .select_one() 获取第一个匹配结果,使用 .select() 获取全部匹配结果:
main_price = soup.select_one("article.product .price")
items = soup.select("ul.results > li.item")
如果您已经通过浏览器开发者工具熟悉了页面的 HTML 结构,使用 CSS 选择器会格外方便:类名以 . 表示,ID 以 # 表示,后代关系的写法也与样式表中的用法类似。
您还可以借助 .parent、.children、.next_sibling、.previous_sibling 等 Beautiful Soup 属性来遍历树状结构,例如:
heading = soup.find("h2")
section = heading.parent
for child in section.children:
print(child)
当目标元素的位置取决于相邻的 HTML 元素,而非唯一的 class 或 ID 时,可以使用这些关系进行定位。
从 HTML 中提取数据
找到目标节点只完成了一半工作,接下来还需要将其转换为干净的 HTML 数据。
如需提取可见文本,可使用 .get_text():
headline = soup.find("h1")
if headline:
text = headline.get_text(" ", strip=True)
print(text)
分隔符 " " 可以防止嵌套标签中的文本连在一起,而 strip=True 则会去除多余的空白字符。
属性的使用方式类似字典:
link = soup.find("a", class_="download")
if link:
href = link.get("href")
print(href)
使用 .get() 比直接通过 link["href"] 索引更安全,因为属性缺失时它会返回 None,而不会抛出 KeyError。
您可以将多个字段提取为结构化的解析数据:
cards = soup.select(".product-card")
products = []
for card in cards:
name_tag = card.select_one(".name")
price_tag = card.select_one(".price")
link_tag = card.select_one("a")
products.append({
"name": name_tag.get_text(strip=True) if name_tag else None,
"price": price_tag.get_text(strip=True) if price_tag else None,
"url": link_tag.get("href") if link_tag else None,
})
print(products)
由于网站会不断变化,防御性编程十分重要:某个 HTML 标签缺失并不意味着整个程序都要崩溃,应先判断是否存在匹配结果,再调用相应方法。
畸形页面也会带来类似的挑战:Beautiful Soup 会尝试构建可用的树状结构,但最终结果取决于所选的解析器。
如果提取出的 HTML 数据突然出现异常,可以打印或保存树状结构中的相关部分,并与原始响应进行比对,排查此类错误。
实用示例
单独了解每项技巧固然有帮助,但只有将它们结合使用,整个流程才会更加清晰。下面列举几个常见任务:提取商品数据、修复不规范的标记,以及处理表格、图片和列表。
从网页中抓取数据
假设某个页面包含如下重复的商品卡片:
<article class="product">
<h2 class="name">Residential Proxy Plan</h2>
<span class="price">$10</span>
<a href="/plans/residential">View plan</a>
</article>
一个基础的爬虫可以获取页面、定位每张卡片,并收集所需字段:
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
url = "https://example.com/products"
response = requests.get(url, timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.content, "lxml")
products = []
for card in soup.select("article.product"):
name = card.select_one(".name")
price = card.select_one(".price")
link = card.select_one("a[href]")
products.append({
"name": name.get_text(strip=True) if name else None,
"price": price.get_text(strip=True) if price else None,
"url": urljoin(url, link["href"]) if link else None,
})
for product in products:
print(product)
这个示例体现了几个良好的习惯:首先,代码在每张商品卡片内部进行查找,而不是反复扫描整个 HTML 文档。
其次,它会妥善处理缺失的部分;第三,urljoin() 会将相对链接转换为绝对 URL。
接下来,您可以将解析得到的数据保存为 CSV、JSON、存入数据库,或传递给其他应用程序。应尽可能将提取逻辑与存储逻辑分开,这样代码会更易于测试和维护。
在实际网站中,选择器可能会发生变化。相比可能频繁改版的大量布局类名,更推荐使用具有明确含义且稳定的属性,例如商品 ID 或 data-* 属性。
处理畸形 HTML
实际页面并非总是规范的,来看下面这段有问题的代码片段:
<div class="card">
<h2>Example product
<p>Available now
</div>
其中 <h2> 和 <p> 标签没有正确闭合,但 Beautiful Soup 仍然可以构建出可用的树状结构:
from bs4 import BeautifulSoup
broken_html = """
<div class="card">
<h2>Example product
<p>Available now
</div>
"""
soup = BeautifulSoup(broken_html, "html5lib")
print(soup.prettify())
print(soup.find("h2").get_text(" ", strip=True))
在这里,html5lib 会按照 HTML5 规范的解析规则,根据需要插入或调整节点位置;而对于同样的畸形输入,lxml 和 html.parser 可能会生成不同的树状结构。
这并不意味着其中一种结果始终正确、其他都是错的——畸形标记本身就可能存在多种合理的解析方式。
在对准确性要求较高的场景下,建议用具有代表性的页面测试各个解析器,选择生成的 HTML 结构符合您提取逻辑预期的那一种。
提取文本前,您还可以先清除不需要的标签:
for tag in soup(["script", "style", "noscript"]):
tag.decompose()
clean_text = soup.get_text(" ", strip=True)
.decompose() 会将选中的节点从树状结构中移除,这在将页面转换为可读文本,或为后续数据解析准备内容时非常实用。
提取表格、图片和列表
表格数据通常按行列排列,假设您想提取某个价格表中的所有单元格:
table = soup.find("table", id="pricing")
rows = []
if table:
for tr in table.find_all("tr"):
cells = tr.find_all(["th", "td"])
row = [cell.get_text(" ", strip=True) for cell in cells]
if row:
rows.append(row)
print(rows)
这会生成一个嵌套列表。如果第一行是表头,您之后可以将其转换为字典或 pandas DataFrame。
图片信息主要依赖属性获取:
images = []
for img in soup.find_all("img"):
images.append({
"src": img.get("src"),
"alt": img.get("alt", ""),
})
需要注意的是,src 可能是相对路径,懒加载图片通常使用 data-src 等属性,而响应式网站则会使用 srcset,请检查 HTML 标签以确定图片 URL 实际所在的属性。
列表的处理则更加简单:
features = [
li.get_text(" ", strip=True)
for li in soup.select("ul.features > li")
]
print(features)
这里的核心思路是先在 HTML 代码中找到重复出现的模式,再从中提取字段,这比在整个页面上逐一搜索每个值要高效得多。
进阶解析技巧
熟悉基础用法后,您可以进一步缩小搜索范围,省去不必要的工作。
Beautiful Soup 的进阶用法通常涉及限定搜索范围、组合多个搜索条件,或将解析器妥善集成到请求层中。
处理嵌套结构
嵌套的页面元素可能会导致写出脆弱的 CSS 选择器,例如:
soup.select("body > main > div > div > section > div > article > h2")
这种选择器目前或许有效,但页面布局稍作调整就可能失效。更好的做法是先找到合适的容器元素,再在其内部进行查找:
catalog = soup.find("section", id="catalog")
if catalog:
for card in catalog.select(".product"):
name = card.select_one(".name")
if name:
print(name.get_text(strip=True))
您也可以使用 .find_all() 进行递归搜索,或者在只需要直接子元素时设置 recursive=False:
menu = soup.find("ul", class_="main-menu")
if menu:
top_level_items = menu.find_all("li", recursive=False)
当嵌套列表在多个层级中包含相同的 HTML 元素时,这种方式会很有用。
另一种方式是通过元素间的关系进行移动:
label = soup.find("span", string="Price")
value = label.find_next("span") if label else None
如果页面包含带标签的值,但该值本身没有唯一的类名,就可以采用这种方法。不过在使用关系推理时需要格外谨慎,因为多插入一项元素就可能导致结果完全不同。
在搜索中使用正则表达式
Beautiful Soup 可以将搜索与 Python 的 re 模块结合使用。当标签名、属性或文本遵循某种模式而非单一精确值时,这种方式很有用。
例如,查找从 h1 到 h3 的标题:
import re
headings = soup.find_all(re.compile(r"^h[1-3]$"))
查找 URL 以 /products/ 开头的链接:
product_links = soup.find_all(
"a",
href=re.compile(r"^/products/")
)
或者匹配文本:
price_text = soup.find_all(
string=re.compile(r"\$\d+(?:\.\d{2})?")
)
正则表达式最适合用作有针对性的过滤手段,不要试图把整个 HTML 文档都变成一个庞大的正则表达式问题。
Beautiful Soup 本身已经能够理解嵌套标记、属性和树状结构关系,因此仅在真正符合模式特征的部分使用正则表达式即可。
将 Beautiful Soup 与 Requests 结合使用
在实时网络爬取场景中,Requests 与 BeautifulSoup 是一组很好的组合:Requests 负责处理 HTTP 部分,BeautifulSoup 负责处理返回的标记内容。
以下是一种通用模式的示例:
import requests
from bs4 import BeautifulSoup
def fetch_soup(url):
headers = {
"User-Agent": "ExampleResearchBot/1.0"
}
response = requests.get(
url,
headers=headers,
timeout=(5, 20),
)
response.raise_for_status()
return BeautifulSoup(response.content, "lxml")
soup = fetch_soup("https://example.com/")
print(soup.title.get_text(strip=True))
通过元组形式的 timeout,可以分别设置连接超时和读取超时的数值。对于较大型的项目,还可以使用 requests.Session() 来复用连接并保留 Cookie。
如果需要在获得许可的情况下发送多次请求,可以在 Requests 模块中通过 proxies 参数使用代理服务器。
例如 MarsProxies 等供应商提供住宅代理及其他类型的代理服务器,可用于 Python 中发起请求。请注意控制请求频率,并遵守目标网站的相关政策。
一个简单的代理设置示例:
proxies = {
"http": "http://username:password@host:port",
"https": "http://username:password@host:port",
}
response = requests.get(
"https://example.com/",
proxies=proxies,
timeout=15,
)
请求层与解析层应当保持分离。如果服务器返回的是登录页面、CAPTCHA 验证提示、错误响应或 JavaScript 空壳页面,无论如何调整选择器,都无法恢复本就不存在于响应中的 HTML 数据。
最佳实践与优化建议
Beautiful Soup 本身使用简单,但随着脚本逐渐复杂,一些设计上的选择就会变得至关重要。
首先要有意识地选择解析器:对性能要求较高的项目,可以尝试 lxml;如果输入标记格式不规范,可以对比 html5lib 与其他解析器的表现。开发环境与生产环境应使用同一种解析器,以保证结果一致。
然后,缩小搜索范围。找到父容器后,再在其内部使用 .find() 或 .select() 进行进一步搜索。这样在有限的树结构中查找,比每次都遍历整个 HTML 文件更轻松,速度也可能更快。
选择能够经受住改版考验的选择器。相较于与版式设计相关、层层嵌套的复杂选择器,ID、语义化的类名、data-* 属性以及有意义的元素关系通常更加稳健可靠。
此外,除非 HTML 标签的位置本身就代表网页的某个语义部分,否则不应依赖标签的排列位置。
直接处理缺失值。这种做法:
price = card.select_one(".price")
value = price.get_text(strip=True) if price else None
比假设所有卡片信息都完整更为稳妥。您可以选择记录不完整的卡片、跳过它们,或将其设为 None。
排查此类错误时,应查看实际的响应内容。务必保存 response.text,打印一部分 soup.prettify() 的结果,并在修改选择器之前确认目标数据确实存在。问题可能源于类名变更、同意协议页面、HTTP 错误、地区差异,甚至是通过 JavaScript 加载的数据。
对于体积非常大的页面,可以考虑使用 SoupStrainer 只解析所需部分:
from bs4 import BeautifulSoup, SoupStrainer
only_articles = SoupStrainer("article")
soup = BeautifulSoup(
html,
"lxml",
parse_only=only_articles
)
如果所需信息只集中在网页的某一特定区域,这样做有助于避免对解析过程进行不必要的处理。
尽量让解析流程中的各个阶段保持独立,包括获取源数据、构建树结构、提取字段、规范化处理,以及最终存储解析结果。
同时要注意合规抓取。请务必了解目标网站 robots.txt 文件中关于抓取的说明,以及其服务条款和与您的使用场景相关的任何 API 或数据访问限制。robots.txt 文件只是说明了网站希望被抓取的方式,并不代表完整的法律许可。
如果网站提供合适的 API,通常比解析 HTML 页面更加稳健可靠,因为网页是面向用户设计的,随时可能发生变化,而 API 则是专为软件对接而构建的。
结语
Beautiful Soup 为 Python 开发者提供了一种便捷的方式,可将标记内容转换为可搜索的树状结构,并从中提取有用的片段。
整个流程包括获取或加载 HTML 文件、选择合适的解析器、生成 soup 对象、通过标签搜索或 CSS 选择器查找 HTML 元素,以及对获取的输出结果进行清洗,最终得到结构化数据。
要获得可靠的输出结果,需要关注稳定的选择器、对缺失字段的稳健处理、合适的解析器选型,以及网络请求与数据解析流程之间的合理分离。
尽可能使用正则表达式,将搜索范围限定在特定容器内,并在输出结果异常时检查源响应内容。
更好的做法是在获得许可的情况下,用一个示例项目进行实践:先选定一个页面,挑选若干字段,记录输出结果,然后再逐步处理分页、异常处理、会话以及(如有需要的)代理等更复杂的内容。
一旦掌握了这套工作流程,Beautiful Soup 就能帮助您解决各种 HTML 数据提取问题,而不会让代码变得复杂。