应使用 lxml.html.fromstring 而非 etree.fromstring 解析 HTML,因其调用 libxml2 的 HTML 解析器,自动补全标签、修复未闭合结构、容忍语法错误;etree.fromstring 严格按 XML 规则解析,遇不规范标签即报错。

lxml 不是纯 Python 实现的解析器,它的核心能力来自 libxml2 的 C 层引擎 —— 这直接决定了它快、容错强、但对 malformed HTML 的“修复行为”不是凭空猜测,而是有明确规则。
etree.HTML() 为什么能修歪 HTML?
当你调用 etree.HTML(html_string),lxml 实际把字符串交给 libxml2 的 htmlParseDoc 函数处理。这个函数内置了一套 HTML 恢复算法(HTML recovery algorithm),会自动:
- 补全缺失的
<html>、<body>等外围标签 - 闭合未闭合的标签(比如
<p>hello<div>world→ 自动插入</p>) - 将自闭合标签(如
<br>)标准化为<br/>形式(仅在 XML 模式下严格;HTML 模式下按语义处理) - 忽略非法嵌套(如
<p>内直接放<div>)并尝试重排树结构
这不是“智能猜测”,而是遵循 WHATWG HTML 规范中定义的 parse tree construction 步骤。所以结果可预期、可复现,但和浏览器 DOM 树仍可能有细微差异(比如某些注释位置、空白文本节点处理)。
etree.fromstring() 和 etree.HTML() 的关键区别
二者底层调用不同解析器,行为差异直接影响结果:
立即学习“前端免费学习笔记(深入)”;
-
etree.fromstring()走的是 XML 解析路径(libxml2 的xmlParseDoc),遇到任何语法错误(如未闭合标签、属性无引号)直接抛XMLSyntaxError -
etree.HTML()走的是 HTML 解析路径(htmlParseDoc),默认宽容模式,几乎不报错 —— 即使传入纯文本或乱码,也会返回一个根为<html>的 Element 对象(内容可能为空或退化) - 若你明确知道输入是格式良好的 XML,必须用
fromstring();若处理真实网页源码(含各种手写错误),必须用HTML()
混淆两者最常见后果:用 fromstring() 解析网页 → 程序崩溃;用 HTML() 解析严格 XML(如 RSS、SOAP 响应)→ 标签被重写、命名空间丢失、@xml:lang 等属性失效。
XPath 查询在 lxml 中如何真正执行
lxml 的 XPath 并非 Python 层遍历模拟,而是把表达式编译成 libxml2 内部的 xmlXPathCompExpr 结构,再由 C 引擎原生执行。这意味着:
-
//div[@class="item"]不是“先找所有 div,再过滤 class”——而是 libxml2 在构建文档树时就建立索引式匹配路径 - 使用
text()或@href等轴(axis)操作时,结果是list,但每个元素是 libxml2 的xmlNodePtr封装,不是 Python 字符串,直到你取值才触发转换 - 重复使用同一 XPath 表达式时,建议先用
etree.XPath()编译一次:find_items = etree.XPath('//div[@class="item"]'),避免每次解析开销
注意:xpath() 返回空列表不等于没匹配到,可能是匹配到但内容为空(如 <div class="item"></div>);需额外检查 .text 或 .getchildren()。
内存与编码问题最容易被忽略的点
lxml 默认以 UTF-8 解析输入,但真实网页常带 <meta charset="gb2312"> 或 HTTP Content-Type: text/html; charset=iso-8859-1。如果忽略编码声明:
- 用
etree.HTML(html_bytes)直接传 bytes 时,lxml 会按自身逻辑检测编码(优先看 meta、再看 HTTP 头、最后 fallback 到 UTF-8),但检测失败就会乱码 - 更稳妥做法是先解码为 str 再传入:
etree.HTML(html_bytes.decode('gbk', errors='replace')),显式控制容错 - 大量解析时,
etree.HTML()创建的 Element 对象不会自动释放底层 C 结构体,尤其在循环中反复调用易导致内存缓慢增长 —— 建议配合del html_tree或用etree.clear_error_log()清理日志缓存
真正影响性能的从来不是 XPath 写法,而是没意识到:libxml2 的解析器状态、编码探测逻辑、以及 Python 对象与 C 节点之间的引用计数绑定关系。


















