Python surrogateescape 往返:字节不是合法 UTF-8,怎样保留它而不替换成问号

前天 3阅读

有些工具需要把带有少量非法字节的旧日志读进来,检查能识别的部分,再原样写回。errors="replace"虽然能让读取继续,却已经把出错字节换掉;等到保存时,原来究竟是FF还是另一个字节就无法从替代符号还原了。surrogateescape可以为这种受控往返保留一条通道。

下面是完整内存实验,适用于Python 3.8及以上,已在CPython 3.12.14执行。保存为demo.py后运行python3 demo.py。我们明确约定UTF-8,并在合法的A、B、é之间放入非法字节FF;这个例子不尝试猜测未知文件的真实编码。

Python surrogateescape 往返:字节不是合法 UTF-8,怎样保留它而不替换成问号

AI模型生成概念图:特殊色块被暂存到透明套中,取出后仍回到原位置,用于比喻非法字节的可逆保留;不是实际字节布局。

为不能解码的部分留下可逆标记

第一段先用默认strict确认样本确实非法。随后只改变错误处理策略,检查FF对应的代理码点和结尾正常解出的é,再用同一UTF-8编码与surrogateescape写回。程序用字节相等验证往返,不能只凭终端看起来相似判断。

完整可运行程序

raw = b"A\xffB\xc3\xa9"
try:
    raw.decode("utf-8")
except UnicodeDecodeError:
    print("strict decode: UnicodeDecodeError")
else:
    raise AssertionError("invalid UTF-8 accepted")

text = raw.decode("utf-8", errors="surrogateescape")
assert text[1] == "\udcff"
assert text[-1] == "\u00e9"
restored = text.encode("utf-8", errors="surrogateescape")
assert restored == raw
print("escaped representation:", ascii(text))
print("restored bytes:", restored.hex(" "))

try:
    text.encode("utf-8")
except UnicodeEncodeError:
    print("strict re-encode: UnicodeEncodeError")
else:
    raise AssertionError("surrogate unexpectedly encoded")

replaced = raw.decode("utf-8", errors="replace")
lost = replaced.encode("utf-8")
assert lost != raw
print("replacement bytes:", lost.hex(" "))
changed = text.replace("\udcff", "?")
assert changed.encode("utf-8", errors="surrogateescape") != raw
print("modified marker round trip:",
      changed.encode("utf-8", errors="surrogateescape") == raw)

本次实际输出(展示内容,不是待执行程序)

strict decode: UnicodeDecodeError
escaped representation: 'A\udcffB\xe9'
restored bytes: 41 ff 42 c3 a9
strict re-encode: UnicodeEncodeError
replacement bytes: 41 ef bf bd 42 c3 a9
modified marker round trip: False

为什么字符串里出现udcff

escaped representation打印的是ascii(text)生成的可见表示。里面的反斜杠转义用于让读者观察码点,不是原字符串中真的保存了六个普通字符。FF在本例中由U+DCFF代理码点携带,合法的C3 A9仍正常解成é。

restored bytes回到41 ff 42 c3 a9,说明原字节未丢失。接着故意省略回写的错误策略,strict编码抛出UnicodeEncodeError:这个临时字符串含有普通UTF-8编码不能直接接受的代理码点,不能把它当作已经清洗好的通用文本。

替代字符和可逆标记承担不同任务

replace路径的输出中,原来的ff变成ef bf bd,即替代字符U+FFFD的UTF-8编码。它适合明确允许受损展示的场景,却不是无损保存方式。最后把U+DCFF主动换成问号,往返结果也变为False,说明可逆性依赖标记仍被完整保留。

如果中途换编码、删字符、重新解释转义或把文本交给不接受代理码点的系统,都需要重新评估。最稳妥的原始证据仍是原bytes;用于展示的文本可以单独生成,不要让显示层修改唯一的原始副本。

使用范围要跟着输入协议走

surrogateescape不会判断一段字节原本想表达哪种语言,也不会自动修好损坏的UTF-8。它解决的是特定编码下遇到不可解码字节时如何保留,而不是把未知编码猜成正确文字。若接口规定所有输入必须是合法UTF-8,仍应按协议拒绝或隔离非法记录。

示例使用ascii输出,是为了不让终端自己的编码策略再次处理代理码点。真实排错日志可记录原字节的十六进制表示和采用的编码名称;若需要按字节位置编辑,直接保留bytes通常比在字符串索引和字节偏移之间来回推算更清楚。

参考资料

资料核验日期:2026年10月2日。输出来自本文固定输入的本地实跑。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。