Python HTMLParser 字符引用:关闭自动转换后,为什么属性里的 amp 仍然被解开
写一个HTML检查工具时,设置convert_charrefs=False,通常是希望看到原来的字符引用。不过这个开关不能理解为“所有位置都停止解码”。普通正文、开始标签中的属性值,以及script内容,经过的解析路径并不相同。
下面用同一段固定HTML运行两次解析,只记录回调,不执行其中的脚本。保存为demo.py,运行python3 demo.py;代码适用于Python 3.5及以上,已在CPython 3.12.14执行。样本没有浏览器、网络请求或第三方依赖。
AI模型生成概念图:正文页、标签和脚本容器展示不同的处理位置,用于说明字符引用转换受上下文影响;不是浏览器或解析器截图。
同一份输入经过两套回调记录
Probe记录开始标签、正文片段、命名引用、数字引用和结束标签。普通段落中同时有amp和数字33,属性值也有amp,script字符串则把同样的拼写放进另一种上下文。程序每次都调用close,确保解析器按输入结束处理剩余内容。
完整可运行程序
from html.parser import HTMLParser
source = (
'<p title="A&B">x&y!</p>'
'<script>const mark = "&";</script>'
)
class Probe(HTMLParser):
def __init__(self, convert):
super().__init__(convert_charrefs=convert)
self.events = []
def handle_starttag(self, tag, attrs):
self.events.append(("start", tag, attrs))
def handle_data(self, data):
self.events.append(("data", data))
def handle_entityref(self, name):
self.events.append(("entity", name))
def handle_charref(self, name):
self.events.append(("char", name))
def handle_endtag(self, tag):
self.events.append(("end", tag))
for convert in (True, False):
parser = Probe(convert)
parser.feed(source)
parser.close()
assert parser.events[0] == ("start", "p", [("title", "A&B")])
assert ("data", 'const mark = "&";') in parser.events
text_events = [
event for event in parser.events
if event[0] in {"data", "entity", "char"}
]
if convert:
assert text_events[0] == ("data", "x&y!")
assert all(event[0] == "data" for event in text_events)
else:
assert ("entity", "amp") in text_events
assert ("char", "33") in text_events
print("convert:", convert)
print("attribute:", parser.events[0][2])
print("text events:", text_events)本次实际输出(展示内容,不是待执行程序)
convert: True
attribute: [('title', 'A&B')]
text events: [('data', 'x&y!'), ('data', 'const mark = "&";')]
convert: False
attribute: [('title', 'A&B')]
text events: [('data', 'x'), ('entity', 'amp'), ('data', 'y'), ('char', '33'), ('data', 'const mark = "&";')]普通正文的开关确实起了作用
convert为True时,普通正文直接形成x&y!的数据事件;没有单独的entity或char事件。改为False后,记录分别包含x、entity amp、y和char 33。这里的33是回调收到的引用文字,还没有被我们的代码再转换成感叹号。
若工具只重写handle_data,却关闭自动转换,可能把这些引用部分漏掉:它们被交给了另外两个回调,并不是字符凭空消失。需要保留或分析引用时,应把对应回调纳入实现与测试。
属性值仍然交付解析后的内容
两次attribute输出都为A&B。handle_starttag提供的attrs已经处理了属性中的字符引用,因此convert_charrefs=False并不保证属性值还保持A&B那种源码拼写。依赖原始引号、空白或引用形式的工具,不能只用attrs重新拼接并宣称逐字保留。
如果只需要最近一个开始标签的原文,可以考察get_starttag_text;若需要整个文件的精确编辑,还应保存原输入与修改区间。解析后的语义值和源码文本属于两种交付物,先明确要保留哪一种,再选记录方式。
script中的引用不会变成普通正文
两套输出都把script内容作为包含&的完整数据片段保留。本例并没有调用或执行那段JavaScript,HTMLParser只把对应内容交给回调。不能因为普通段落里的引用被解开,就继续对脚本字符串无条件做同样的二次解码。
HTMLParser也不等于浏览器DOM构建器、有效性校验器或HTML消毒器。把data事件拼起来不会自动得到安全HTML;删除几个标签也不是完整的安全清洗方案。本文的验收目标仅是弄清各位置交给哪个回调、交来的是原始拼写还是转换后的值。
参考资料
资料核验日期:2026年10月2日。输出来自本文固定输入的本地实跑。


