HTML 解析实战:BeautifulSoup 与 lxml
前置知识
本文基于模型知识整理,建议核对官方文档(见 参考资料)。
一句话定义
BeautifulSoup 提供「对人类友好」的树查询 API,lxml 提供底层的高速解析引擎;两者组合(BS4 用 lxml 作 parser)是静态页字段抽取的常规配置。
为什么重要
解析是从「一坨 HTML」到「结构化字段」的关键一跳。解析代码的写法直接决定站点改版后的维护成本:锚点选得好,改版只改一行;锚点选得差,全部重写。
前置知识
kp-003(DOM 树与选择器)、kp-006(拿到 HTML 文本)。
核心概念
- parser 选择:
html.parser(内置、慢)、lxml(快、需安装)、html5lib(最宽容、最慢)。 - find / find_all:单数/复数查询,参数可为标签名、attrs、CSS 选择器(
select)。 - 节点属性:
tag.name、tag.get("href")、tag.get_text(strip=True)。 - 相对定位:先锁定列表容器,再在容器内找子节点,避免全局误命中。
- None 防御:
find找不到返回None,链式调用前必须判空。
原理与机制
标准抽取模式:容器定位 → 单元遍历 → 单元内相对取字段。
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "lxml")
items = []
for pod in soup.select("article.product_pod"): # ① 容器/单元定位
a = pod.select_one("h3 a") # ② 单元内相对定位
price = pod.select_one("p.price_color")
rating = pod.select_one("p.star-rating")
items.append({
"title": a["title"] if a else None, # ③ 判空 + 优先读属性
"price": price.get_text(strip=True) if price else None,
"rating": rating["class"][1] if rating and len(rating["class"]) > 1 else None,
})
选锚点的优先级(抗改版能力从高到低):data-* 属性 > 语义 class(如 product_pod)> 结构位置(nth-child)。标题类文本优先读 title/alt 属性而非内嵌文本——站点常把长文本截断显示在标签里,完整值在属性上。
实例或案例
books.toscrape.com 完整落地:每页 20 本,抽出 title/price/rating/链接四字段,翻页逻辑交给 kp-010,落库交给 kp-011。练习目标:10 分钟内从「右键检查」到「跑出 20 行字典列表」。
常见误区
- 误区一:
get_text()结果带满屏空白。 列表结构中标签间有换行缩进,必须get_text(strip=True)或后续 strip。 - 误区二:不判空直接
a["title"]。 改版或个别缺字段时直接TypeError崩掉整个任务;统一用if x else None。 - 误区三:解析慢就抱怨 BS4。 换
lxmlparser 通常有数倍提升;仍慢则该考虑用 kp-009 的接口路线而非死磕 HTML。
自测题
find与find_all返回类型差异及代码含义?
答:find 返回单个节点或 None;find_all 返回列表(可能为空)——前者要判空,后者要判长度。
- 为什么优先读
a["title"]而不是a.get_text()?
答:显示文本可能被截断(如加省略号),属性里通常是完整原文。
- 解析器性能排序?
答:lxml > html.parser > html5lib;lxml 同时是速度与容错的常见折中首选。
与其他知识点的关系
kp-008 给出 XPath 这一更强的定位语言;kp-010 把「一页的解析」扩成「整站的采集」;kp-011 消费本篇产出的字典列表。
延伸阅读
BeautifulSoup 官方文档「Quick Start」与「Searching the tree」两节。