bigywater.tech
爬虫工程 · 46

解析器对比:lxml、html.parser、html5lib 如何处理残缺 HTML

爬虫解析的是真实网页,真实网页里的 HTML 经常是残缺的。解析器怎么“修”这些残缺,决定了你的选择器选出什么。这篇用真实的 Chrome 当标准,逐个看 lxml、html5lib、标准库 html.parser、selectolax 和 BeautifulSoup 的几种后端各自对不对。

22 对 6
selectolax(Lexbor)与 bs4 + html.parser 在 22 个残缺片段上与 Chrome 一致的片段数
12 / 22
lxml.html 与 Chrome 一致的片段数——不补 tbody、不重建错位的格式标签、吞掉
x.html 对 y.html
重复属性 href 时,Chrome 保留第一个、html.parser 保留最后一个

谁和浏览器一致

实测 · 本机执行输出
  解析器                            与 Chrome 一致的片段数
  lxml.html 6.1.1                     12 / 22
  html5lib 1.1                        21 / 22
  bs4 + html.parser                    6 / 22
  bs4 + lxml                          12 / 22
  bs4 + html5lib                      22 / 22
  selectolax 1.0.0                    22 / 22

Chrome 154 按 HTML 规范解析。selectolax 的 Lexbor 后端和 bs4 + html5lib 22 个片段全部一致;html5lib 自身 21 个(不同的一个是序列化时没转义属性值里的 >);libxml2 系的 lxml.html 只有 12 个;标准库 html.parser 经 BeautifulSoup 只有 6 个。这里比较的是 body.innerHTML,比较前统一了空白、大小写、<br/> 的写法和属性顺序。

差在哪里

实测 · 本机执行输出
  未闭合的 <p>:输入 '<p>one<p>two'
      Chrome → '<p>one</p><p>two</p>'
      bs4 + html.parser → '<p>one<p>two</p></p>'
  错位嵌套的格式标签:输入 '<b><i>x</b>y</i>'
      Chrome → '<b><i>x</i></b><i>y</i>'
      lxml.html 6.1.1 → '<b><i>x</i></b>y'
      bs4 + html.parser → '<b><i>x</i></b>y'
      bs4 + lxml → '<b><i>x</i></b>y'
  表格没有 tbody:输入 '<table><tr><td>a</td></tr></table>'
      Chrome → '<table><tbody><tr><td>a</td></tr></tbody></table>'
      lxml.html 6.1.1 → '<table><tr><td>a</td></tr></table>'
      bs4 + html.parser → '<table><tr><td>a</td></tr></table>'
      bs4 + lxml → '<table><tr><td>a</td></tr></table>'
  表格里的游离文字:输入 '<table>text<tr><td>a</td></tr></table>'
      Chrome → 'text<table><tbody><tr><td>a</td></tr></tbody></table>'
      lxml.html 6.1.1 → '<table>text<tr><td>a</td></tr></table>'
      bs4 + html.parser → '<table>text<tr><td>a</td></tr></table>'
      bs4 + lxml → '<table>text<tr><td>a</td></tr></table>'
  嵌套的 <a>:输入 '<a href=1>x<a href=2>y</a></a>'
      Chrome → '<a href="1">x</a><a href="2">y</a>'
      bs4 + html.parser → '<a href="1">x<a href="2">y</a></a>'
  <div> 里的 <p> 被 </div> 截断:输入 '<div><p>x</div>y</p>'
      Chrome → '<div><p>x</p></div>y<p></p>'
      lxml.html 6.1.1 → '<div><p>x</p></div>y'
      bs4 + html.parser → '<div><p>x</p></div>y'
      bs4 + lxml → '<div><p>x</p></div>y'
  重复的属性:输入 '<a href=x.html href=y.html>t</a>'
      Chrome → '<a href="x.html">t</a>'
      bs4 + html.parser → '<a href="y.html">t</a>'
  </br> 结束标签:输入 'a</br>b'
      Chrome → 'a<br>b'
      lxml.html 6.1.1 → 'ab'
      bs4 + html.parser → 'ab'
      bs4 + lxml → 'ab'
  属性值缺少结尾引号:输入 '<a href="x>text</a><a href="y">z</a>'
      Chrome → '<a href="x&gt;text&lt;/a&gt;&lt;a href=" y"="">z</a>'
      lxml.html 6.1.1 → '<a href="x&gt;text&lt;/a&gt;&lt;a%20href=" y">z</a>'
      html5lib 1.1 → '<a href="x>text&lt;/a>&lt;a href=" y"="">z</a>'
  没有分号的实体:输入 '&copy 2026 &amp &notit; &lt'
      Chrome → '© 2026 &amp; ¬it; &lt;'
      bs4 + html.parser → '© 2026 &amp; &amp;notit &lt;'
  <script> 里有 </div>:输入 '<script>var s = "</div>";</script><p>after</p>'
      Chrome → '<script>var s = "</div>";</script><p>after</p>'
      其它解析器全部一致
  <li> 没有 <ul>:输入 '<li>a<li>b'
      Chrome → '<li>a</li><li>b</li>'
      bs4 + html.parser → '<li>a<li>b</li></li>'
  <select> 里的未闭合 <option>:输入 '<select><option>a<option>b</select>'
      Chrome → '<select><option>a</option><option>b</option></select>'
      bs4 + html.parser → '<select><option>a<option>b</option></option></select>'
  <p> 里放 <table>:输入 '<p>t<table><tr><td>x</td></tr></table>'
      Chrome → '<p>t</p><table><tbody><tr><td>x</td></tr></tbody></table>'
      lxml.html 6.1.1 → '<p>t</p><table><tr><td>x</td></tr></table>'
      bs4 + html.parser → '<p>t<table><tr><td>x</td></tr></table></p>'
      bs4 + lxml → '<p>t</p><table><tr><td>x</td></tr></table>'
  自闭合的非空元素 <div/>:输入 '<div/>x<span/>y'
      Chrome → '<div>x<span>y</span></div>'
      lxml.html 6.1.1 → '<div></div>x<span></span>y'
      bs4 + html.parser → '<div></div>x<span></span>y'
      bs4 + lxml → '<div></div>x<span></span>y'
  嵌套的 <form>:输入 '<form id=a><form id=b><input></form></form>'
      Chrome → '<form id="a"><input></form>'
      lxml.html 6.1.1 → '<form id="a"></form><form id="b"><input></form>'
      bs4 + html.parser → '<form id="a"><form id="b"><input/></form></form>'
      bs4 + lxml → '<form id="a"></form><form id="b"><input/></form>'
  <textarea> 里的标签:输入 '<textarea><b>bold</b></textarea>'
      Chrome → '<textarea>&lt;b&gt;bold&lt;/b&gt;</textarea>'
      其它解析器全部一致
  大小写混用与奇怪属性:输入 '<DIV CLASS=A Id="b" data-X=1><P>x</P></DIV>'
      Chrome → '<div class="A" id="b" data-x="1"><p>x</p></div>'
      其它解析器全部一致
  <td> 在 <table> 之外:输入 '<td>a</td><td>b</td>'
      Chrome → 'ab'
      lxml.html 6.1.1 → '<td>a</td><td>b</td>'
      bs4 + html.parser → '<td>a</td><td>b</td>'
      bs4 + lxml → '<td>a</td><td>b</td>'
  <title> 里的标签:输入 '<title><b>t</b></title><p>x</p>'
      Chrome → '<title>&lt;b&gt;t&lt;/b&gt;</title><p>x</p>'
      其它解析器全部一致
  注释 <!-- a -- b -->:输入 'x<!-- a -- b -->y'
      Chrome → 'x<!-- a -- b -->y'
      其它解析器全部一致
  <!-->:输入 'x<!-->y'
      Chrome → 'x<!---->y'
      bs4 + html.parser → 'x<!-- -->y'
      bs4 + lxml → 'x<!-- -->y'

规范规定的“修复”行为包括:遇到新的 <p> 就隐式关闭上一个、表格自动补 <tbody>、格式标签错位嵌套时重新打开(<b><i>x</b>y</i> 里 y 仍是斜体)、嵌套的 <a> 拆开、重复属性保留第一个、</br> 当作 <br>、表格外的 <td> 被忽略。libxml2 和 html.parser 有不少不做(libxml2 会隐式关闭

、会拆开嵌套的 ,但不补 tbody、不重建格式标签;html.parser 几乎都不做),于是树的形状不同,选择器的结果也不同。

分步演示

解析器对比
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。标准答案:Chrome 154.0.8037.98(headless,通过 CDP 导航到本机服务器的页面,读 document.body.innerHTML);对照:lxml 6.1.1(lxml.html.document_fromstring)、html5lib 1.1(用它自己的序列化器)、beautifulsoup4 4.15.0 搭配 html.parser / lxml / html5lib、selectolax 1.0.0(Lexbor);后三个第三方库用 pip 装进实验专用目录,实验后删除;比较时统一空白、标签名大小写、
      、属性顺序;片段是 22 个手写的残缺 HTML
    • 没有验证:真实网页(大量脚本、内联样式、嵌套更深)上的一致率;lxml.html 以外的 libxml2 配置(如 recover 选项);Firefox、Safari 是否与 Chrome 一致;解析速度(下一批会单独看);片段放在 以外的上下文(如 、、)时的行为;selectolax 的旧 Modest 后端(已弃用,没有测)
    京ICP备2026067502号-1京公网安备11010502063487号