登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python HTMLParser 怎样保留原始字符引用回调

来源:17golang原创

时间:2026-11-10 11:28:16 217浏览 收藏

如果你的任务是记录 HTML 源文本里到底写了 >、> 还是 >,直接使用 HTMLParser() 会遇到一个关键默认值:convert_charrefs=True。它会把字符引用转换成对应的 Unicode 字符,因此后续通常只能收到普通文本。

官方地址:https://docs.python.org/3/library/html.parser.html

要保留原始字符引用的回调事件,创建解析器时传入 convert_charrefs=False,然后分别覆盖 handle_entityref(name) 和 handle_charref(name)。实际接收数据时继续使用 feed(),输入结束时显式调用 close(),这样分块数据和最后的缓冲区才有明确的收束边界。

先把默认行为看清楚

HTMLParser 接收的是字符串,并在遇到开始标签、结束标签、文本和其它标记时调用对应的处理方法。默认构造方式适合“我只想拿到可读文本”的场景:普通文本中的命名字符引用和数字字符引用会先被转换。

这意味着下面三种写法在普通文本处理路径上都可能汇聚成同一个字符:

  • >:命名字符引用;
  • >:十进制数字字符引用;
  • >:十六进制数字字符引用。

如果业务只关心页面显示结果,默认值很省事;如果要做源文本保留、格式迁移、审计记录或按引用类型统计,就应该把这个默认转换关闭。这里的“保留”指让解析器触发引用回调并交给你的代码记录原始形式,不是让 HTMLParser 变成完整的 HTML 源码编辑器。

关闭转换后,两个回调分别负责什么

创建解析器时传入 convert_charrefs=False。命名引用进入 handle_entityref(name),数字引用进入 handle_charref(name)。数字回调收到的 name 不带开头的 :十进制例子是 62,十六进制例子是 x3E。

HTMLParser 关闭自动转换后将命名和数字字符引用分流到两个回调的静态结构图
图1:字符引用回调分流说明图,展示命名实体与数字引用的处理边界。

可以把回调设计成一个小型事件流水线:回调只负责保存“类型 + 原始参数”,后续阶段再决定是否解码、重写或告警。这样不会把解析动作和业务输出绑死在一起。

from html.parser import HTMLParser


class ReferenceRecorder(HTMLParser):
    def __init__(self):
        # 关闭自动转换,才能收到命名和数字引用的独立回调。
        super().__init__(convert_charrefs=False)
        self.events = []

    def handle_entityref(self, name):
        # name 只包含实体名,例如 gt;保留前后的标记以便后续重建。
        self.events.append({"kind": "named", "raw": f"&{name};"})

    def handle_charref(self, name):
        # name 可能是十进制数字,也可能以 x 开头表示十六进制。
        self.events.append({"kind": "numeric", "raw": f"{name};"})

    def handle_data(self, data):
        # 普通文本单独记录,避免把引用事件和文字拼接时混淆。
        if data:
            self.events.append({"kind": "data", "raw": data})


parser = ReferenceRecorder()
parser.feed("标题 > 数字 > 十六进制 >")
parser.close()

示例里没有在回调中直接调用 html.unescape()。这是有意的:如果当前流水线要知道原始写法,应该先记录原始事件,再在下游按业务需要生成展示文本。另一个细节是命名引用回调的参数是实体名,数字引用回调的参数是数字部分;两者不要用同一套数字转换逻辑处理。

把 feed 和 close 放进流水线

feed(data) 可以多次调用,解析器会处理已经完整的部分,并把不完整的数据暂存在内部缓冲区。网络读取、文件分片或消息队列消费时,常见做法是每次拿到一个字符串块就调用一次 feed(),不要假设一个块恰好对应一个 HTML 元素或一个字符引用。

HTMLParser 多次 feed 输入、内部缓冲和 close 收尾关系的静态说明图
图2:分块输入与缓冲收尾说明图,强调 close 是输入流水线的收束动作。
from html.parser import HTMLParser


class ChunkParser(HTMLParser):
    def __init__(self):
        # 保留字符引用事件;事件列表也方便失败时统一落盘。
        super().__init__(convert_charrefs=False)
        self.events = []

    def handle_entityref(self, name):
        # 命名引用单独落一条事件,保留实体名而不是只保存解码字符。
        self.events.append(("entity", name))

    def handle_charref(self, name):
        # 数字引用保留原始进制标记,x 前缀不能在记录时丢掉。
        self.events.append(("char", name))

    def handle_data(self, data):
        # 空文本没有业务价值,避免让事件记录被无意义片段填满。
        if data:
            self.events.append(("data", data))


def parse_chunks(chunks):
    parser = ChunkParser()
    try:
        for chunk in chunks:
            # 每个 chunk 必须是 str;不完整的标记由 HTMLParser 暂存。
            parser.feed(chunk)
        # 输入源结束后显式收尾,要求解析器处理剩余缓冲数据。
        parser.close()
        return parser.events
    except Exception as exc:
        # 失败时带上异常类型,便于上层决定重试、隔离或人工复盘。
        raise RuntimeError("HTML 分块解析失败") from exc


events = parse_chunks(["标题 &g", "t; 数字 ", "62; 结束"])

这里的关键不是把字符串拆成三段,而是说明分块边界可以落在字符引用中间。解析器会把尚未完成的部分留在缓冲区,后续 feed() 继续补齐。close() 不是“关闭文件”的替代品,而是通知解析器输入已经结束,要求它处理尚未完成的尾部。

给回调流水线加三道门禁

如果这段代码用于批处理或定时任务,可以把门禁放在回调之后,而不是试图通过修改回调参数解决所有问题。

门禁检查内容处理动作
输入门禁每个 feed() 参数是否为字符串拒绝字节对象,统一在读取层完成解码
事件门禁命名引用与数字引用是否按类型记录保留 name 的原始形式,避免过早解码
收尾门禁成功路径是否调用 close()把未处理尾部纳入结果或失败记录

注意,HTMLParser 能解析不规范的标记,但这不等于它会替你做 HTML 文档树校验。它不会检查结束标签是否与开始标签匹配,也不会为被外层元素隐式关闭的元素调用对应的结束标签处理器。若业务要求严格结构校验,应在解析器之外增加专门的结构策略。

失败时怎样留下可复盘的信息

自动化任务最难排查的情况通常不是“某个回调没写”,而是输入被切块、异常被吞掉,最后只剩一条“解析失败”。建议至少记录四个字段:输入块序号、回调类型、原始参数、异常类型。不要在失败处理里再次盲目调用同一个消费动作;先把原始块和事件列表落到可追踪的任务记录中。

def summarize_events(events):
    # 只汇总类型和数量,不改变已经保存的原始引用。
    counts = {"data": 0, "entity": 0, "char": 0}
    for kind, _value in events:
        if kind in counts:
            counts[kind] += 1
    return counts

如果必须把引用转换成显示文本,可以把转换放在单独的后处理函数,并保留原始事件作为审计字段。这样同一份输入既能生成用户看到的文本,也能回答“源文件到底使用了哪一种引用写法”。

最后的速查结论

  • 默认 convert_charrefs=True,普通文本中的字符引用会自动转成 Unicode 字符。
  • 想接收命名引用回调,覆盖 handle_entityref(name);想接收数字引用回调,覆盖 handle_charref(name)。
  • 数字回调的参数区分十进制和以 x 开头的十六进制形式。
  • 分块输入使用多次 feed(),输入结束显式调用 close()。
  • 回调先记录原始事件,解码、展示和告警放到后处理阶段,失败时保留块序号和异常类型。

相关问题

为什么我覆盖了 handle_charref() 却没有收到事件? 最常见的原因是仍使用默认的 convert_charrefs=True,需要在构造解析器时显式传入 False。

feed() 后一定要调用 close() 吗? 对有可能留下不完整尾部的分块输入,应该调用;它让解析器按输入结束处理内部缓冲区。

这个类能保证 HTML 标签结构正确吗? 不能。它适合按事件消费 HTML/XHTML 文本,不负责完整文档的严格配对校验。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>