DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Python 读取 GBK 文件报 UnicodeDecodeError

Python 读取已确认的 GBK 文本报 UnicodeDecodeError,先核对输入编码,再显式读取或保留原件转存 UTF-8;另查 Python 3.15 默认编码变化。

读取旧文本文件时,open(...).read() 或 Path.read_text() 报 UnicodeDecodeError,错误中出现 'utf-8' codec can't decode。如果文件确实由上游按 GBK 导出,用 UTF-8 读取就可能在中文内容处失败;应在读取时指定与文件一致的编码。

先保留原文件和完整异常,核对读到了哪个路径、调用是否传入 encoding,以及上游实际写出的编码。报错里的 utf-8 是这次使用的解码器名称,不能据此认定输入一定是 GBK。

先确认文件编码,不按出错字节猜

优先查看导出程序的设置、文件格式约定或写出代码。编辑器识别结果和编码检测工具只能提供线索;HTML 中的 charset 声明也要与实际保存方式核对。文件有中文、来自 Windows,或者错误中出现某个十六进制字节,都不足以证明它是 GBK。

若异常写的是 'gbk' codec can't decode,方向已经不同:当前程序正在使用 GBK,输入可能是 UTF-8 或其他格式。不要继续把解码器改成 GBK。

无法确认编码时,先联系文件生成方或检查导出流程。压缩包、二进制文件、截断内容,以及拼接了不同编码的文本,也可能触发解码异常;轮流尝试编码直到不报错,不能保证读出的字符正确。

已确认 GBK,在读取边界显式指定

下面的示例适用于已确认采用 GBK 的文本文件。将 legacy.txt 换成待检查文件的路径,先在保留原件的副本上验证:

from pathlib import Path

path = Path("legacy.txt")
with path.open("r", encoding="gbk", errors="strict") as source:
    text = source.read()

读取到的 text 已经是 str,后续直接按文本处理,不再调用 .decode()。这个写法只读文件,不会把原文件转换成另一种编码。文本读取默认会将常见换行转换为 \n;若业务需要保留原有换行,可在 open() 中增加 newline=""。

严格解码通过后,还要对照已知字段、中文字符和记录结构。某种编码能够接受这些字节,不代表它就是文件的真实编码。若按确认的 GBK 读取仍失败,应回到失败片段及生成流程,检查文件是否损坏、编码约定是否执行一致。

不要用 errors="ignore" 把异常字节丢掉。errors="replace" 会用替换字符改变内容,对姓名、标识符、路径或需要解析的记录可能造成数据错误。保留严格报错,才能知道哪份输入尚未处理正确。

Python 3.15 升级后,检查原调用是否依赖默认编码

Python 3.15 默认启用 UTF-8 模式。未传 encoding 的文本文件读取,默认使用 UTF-8;显式关闭该模式时,默认编码仍依赖运行环境。

如果同一份 GBK 文件此前在使用 GBK 默认编码的环境中可以读取,升级后又没有指定编码,就可能出现这类失败。这个条件需要核对实际进程:sys.flags.utf8_mode 可用于查看 UTF-8 模式状态,启动参数和 PYTHONUTF8 环境变量也会影响它。若旧环境本来就在使用 UTF-8,不能把升级认定为这次异常的原因。

默认编码变化不会改写已有文件的字节。对编码已经确定的 GBK 输入,显式设置 encoding="gbk" 可以让读取规则不再依赖这项默认值。调整终端代码页,或设置针对标准输入输出的 PYTHONIOENCODING,也不能把现有 GBK 文件转成 UTF-8。

需要统一 UTF-8 时,写到新文件

确认源文件是 GBK、内容校验无误后,才考虑转存。下面的示例面向大小可控的普通文本文件;两个路径分别是原文件和尚不存在的新文件,执行前确认所在目录及权限:

from pathlib import Path

source = Path("legacy.txt")
target = Path("legacy.utf8.txt")

raw = source.read_bytes()
text = raw.decode("gbk", errors="strict")

with target.open("x", encoding="utf-8", newline="") as output:
    output.write(text)

这里先完整解码,再创建目标文件。解码失败时不会创建目标;"x" 模式拒绝覆盖已有目标,原文件保持不变。newline="" 避免写出时再次转换换行。示例同时把原始字节和文本放入内存,不适合直接处理超大文件。

写入过程失败仍可能留下不完整的新文件,这段代码不提供原子写入。切换读取路径前,按 UTF-8 严格读取新文件,对照源内容、记录数量和业务字段;核验完成前保留原件。若转换的是 HTML、XML 等带编码声明的格式,还需要让声明与新字节编码一致,不能仅转存正文。

如果实际错误是 JSONL 首行的 Unexpected UTF-8 BOM,可继续检查 UTF-8 BOM 与文本读取。如果异常发生在 Windows subprocess 的 _readerthread,同时捕获结果为 None,应按 子进程后台解码异常 检查管道输出;那时需要核对的是子程序的输出约定。