爬虫解析的是真实网页,真实网页里的 HTML 经常是残缺的。解析器怎么“修”这些残缺,决定了你的选择器选出什么。这篇用真实的 Chrome 当标准,逐个看 lxml、html5lib、标准库 html.parser、selectolax 和 BeautifulSoup 的几种后端各自对不对。
解析器 与 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>text</a><a href=" y"="">z</a>'
lxml.html 6.1.1 → '<a href="x>text</a><a%20href=" y">z</a>'
html5lib 1.1 → '<a href="x>text</a><a href=" y"="">z</a>'
没有分号的实体:输入 '© 2026 & ¬it; <'
Chrome → '© 2026 & ¬it; <'
bs4 + html.parser → '© 2026 & &notit <'
<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><b>bold</b></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><b>t</b></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 几乎都不做),于是树的形状不同,选择器的结果也不同。