前言
这篇文章是排查问题向的。编码知识本身在另一处讲,这里只干一件事:把你实际会遇到的编码报错,按「报错信息 → 成因 → 解法」一条条列清楚。
Python 3 里和字符串相关的报错,翻来覆去就三类:
UnicodeDecodeError:读进来的字节流,按你指定的编码解不开。UnicodeEncodeError:要输出的字符串,按目标编码编不出。SyntaxError: Non-UTF-8 code ...:你的源码文件本身不是合法 UTF-8。
很多人卡住不是因为不会写encode/decode,而是因为不会读报错。这三类报错的信息其实非常有用——它告诉你「哪个字节、在哪个位置、用了哪个编码」。学会读它,问题基本就定位了一半。
前置说明:本机无 Python 解释器,代码与报错文本均为逐行推演与官方文档对照,未经运行。文中引用的行为以 Python 官方文档和 PEP 为准。
一、一条主线:报错总是发生在「转换的那一步」
Python 3 严格区分str(文本,码位序列)和bytes(字节序列)。报错只会发生在两者互转的那一刻,不会平白无故出现:
| 转换方向 | 方法 | 出错时抛 |
|---|
bytes→str | decode(编码, errors) | UnicodeDecodeError |
str→bytes | encode(编码, errors) | UnicodeEncodeError |
所以排查的第一步永远是问:是哪一步转换?用的哪个编码?
UnicodeDecodeError/UnicodeEncodeError都是UnicodeError的子类。报错信息里通常包含三段关键信息:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte'utf-8':你指定的编码(也就是「猜错的那个」)。0x80:解不开的那个字节。position 0:它出现的位置。
UnicodeEncodeError的格式类似,但指出的是「哪个字符」而不是「哪个字节」:
UnicodeEncodeError: 'ascii' codec can't encode character 'ꀀ' in position 0: ordinal not in range(128)这段来自官方 Unicode HOWTO 的示例:用ascii去编码一个码位为ꀀ的字符,失败,因为 ASCII 只能表示 0~127。
二、UnicodeDecodeError:解不开
成因:字节流的真实编码和你指定的解码编码不一致。最常见的组合是——文件其实是 GBK,你按 UTF-8 解。
# 适用于 Python 3.8+
with open("data.txt", encoding="utf-8") as f:
text = f.read() # 若文件其实是 GBK,这里抛 UnicodeDecodeError解法分三层,从「治本」到「治标」:
- 确认真实编码再解。可以试
open时换编码,或用内容探测辅助判断。 - 换正确的编码:
open("data.txt", encoding="gbk")。 - 确实不知道编码,且只要大致内容:用
errors参数让解码不中断。
errors参数决定「解不开时怎么办」,几个取值在官方文档里的行为如下(以b'\x80abc'.decode("utf-8", ...)为例):
errors取值 | 结果 | 行为 |
|---|
"strict" | 抛UnicodeDecodeError | 默认值,最严格 |
"ignore" | 'abc' | 直接丢掉解不开的字节 |
"replace" | '�abc' | 换成替换字符 U+FFFD(「�」) |
"backslashreplace" | '\\x80abc' | 换成\xNN形式 |
"surrogateescape" | 保留为代理码位 | 可无损还原(见下) |
surrogateescape值得单独说:它把解不开的字节映射成 U+DC80~U+DCFF 的代理码位,之后用同样的surrogateescape编码回去时,能原样变回那个字节。它适合「我只是路过这份数据、不能破坏它」的场景(PEP 383)。但它还原出来的字符串不是正常文本,拿去显示或比较会出问题。
# 适用于 Python 3.8+
raw = b"\x80abc"
print(raw.decode("utf-8", "replace")) # '�abc'
print(raw.decode("utf-8", "ignore")) # 'abc'
# surrogateescape 可往返:
roundtrip = raw.decode("utf-8", "surrogateescape")
print(roundtrip.encode("utf-8", "surrogateescape") == raw) # True三、UnicodeEncodeError:编不出
成因:你手里的字符串包含目标编码表示不了的字符。最常见的是用ascii或gbk去编码一个含生僻字/emoji 的字符串。
官方文档给的编码侧errors取值比解码侧多两个:
errors取值 | 对ꀀabcd编码为ascii的结果 |
|---|
"strict" | 抛UnicodeEncodeError |
"ignore" | b'abcd' |
"replace" | b'?abcd?'(编码侧用问号,不是 U+FFFD) |
"xmlcharrefreplace" | b'ꀀabcd޴' |
"backslashreplace" | b'\\ua000abcd\\u07b4' |
"namereplace" | b'\\N{YI SYLLABLE IT}abcd\\u07b4' |
注意两个只在编码时可用的处理器:xmlcharrefreplace(转成 XML 数字字符引用)和namereplace(转成\N{...}名字转义)。还有一个surrogatepass,只对utf-8、utf-16、utf-32系列编码有效,允许把代理码位当普通码位编解码。这些处理器在解码时不能随便用,官方文档明确区分了「所有编码器通用」和「仅编码时可用」两组。
四、SyntaxError: Non-UTF-8 code:源码自身的问题
这类错误和运行时无关,发生在 Python 解析你的源文件时。
Python 2 的默认源码编码是 ASCII(PEP 263),所以含中文的源码必须在第一行或第二行声明:
# -*- coding: utf-8 -*-Python 3 把默认源码编码改成了 UTF-8(PEP 3120),不再需要那行声明。于是问题反过来了:如果你的.py文件是用 GBK 保存的,里面有非 ASCII 字节,Python 3 会按 UTF-8 去解析,直接报:
SyntaxError: Non-UTF-8 code starting with '\xd0' in file demo.py on line 1, but no encoding declared; see PEP 263 for details(不同小版本措辞可能略有差异。)
解法二选一:
- 推荐:把源文件重新保存成 UTF-8 编码。
- 兜底:在文件头加一行声明,告诉解释器真实编码,例如
# -*- coding: gbk -*-。但这属于迁就历史文件,新代码不该这么写。
五、乱码的三种典型场景
5.1 读文件
症状:有UnicodeDecodeError,或者读出来是「锟斤拷」「ä¸Â」这类乱码。
根因:open()不写encoding时,用的是平台默认编码——Windows 上可能是 GBK,Linux 上通常是 UTF-8。同一份文件在不同系统上结果不同。
# 适用于 Python 3.8+
# ❌ 不写 encoding,跨平台行为不一致
with open("a.txt") as f:
data = f.read()
# ✅ 明确编码
with open("a.txt", encoding="utf-8") as f:
data = f.read()5.2 网络响应
症状:抓回来的中文是乱码,或resp.text里全是替换字符。
根因:HTTP 响应头里的 charset 缺失或不准确,客户端用错误的编码解码了解码。requests的做法是优先信任响应头;头缺失时可能退化成 ISO-8859-1。
对策:先确认响应头,再决定encoding;实在拿不准时,可以借助库提供的内容探测能力(以该库官方文档为准)。更稳妥的是自己按优先级判断:响应头 > 文档内 meta 声明 > 内容探测。
5.3 Windows 控制台
症状:print一个含特殊字符的字符串时报UnicodeEncodeError: 'gbk' codec can't encode character ...。
根因:控制台(或其被重定向后的流)使用的编码,和你字符串里的字符对不上。
背景事实:从Python 3.6 起,Windows 控制台改用 UTF-8 作为sys.stdout的编码(PEP 528),底层通过WriteConsoleW直接走宽字符 API。所以在真实控制台里打印 Unicode 字符,现在的 Python 基本没问题。问题主要出在重定向到文件或管道时——那时的编码会退回系统区域设置(ANSI 代码页),而非交互式控制台。
对策:
- 运行时指定环境变量
PYTHONIOENCODING=utf-8。 - 或不依赖控制台,把结果直接写进以 UTF-8 打开的文件。
- 未来版本方向已明确:PEP 686 计划让 UTF-8 模式成为默认(目标 Python 3.15),届时默认编码的困扰会进一步减少。
常见坑点
- 混用两个不同编码的字节流拼接
❌ 把 GBK 的字节和 UTF-8 的字节+到一起再解码,必然失败。
✅ 先各自解码成str,再拼接;字节层面不要跨编码拼接。
open()不写 encoding
❌open("f.txt"),换台机器就乱码,还以为是文件坏了。
✅ 一律显式open("f.txt", encoding="utf-8")。
- 用
ignore掩盖真实问题
❌decode("utf-8", "ignore")让程序不报错,但数据被悄悄丢字。
✅ 先找真实编码;确需容错时优先replace,让丢失可见。
- 指望
surrogateescape产出可读文本
❌ 用surrogateescape解出来直接拿去比较或显示。
✅ 它只保证「往返不丢字节」,正常文本处理还是要用正确编码。
- Python 3 里还写
# -*- coding: utf-8 -*-却没存成 UTF-8
❌ 文件是 GBK,却声明 utf-8,报错信息与实际不符,越查越乱。
✅ 让文件实际编码与声明一致;新代码直接存 UTF-8,无需声明。
- 在控制台报
UnicodeEncodeError就以为代码错了
❌ 满头找 bug,其实字符串完全正确,只是输不出。
✅ 分清「数据对不对」和「输出端能不能表示它」两件事。
- 裸
except吞掉编码错误
❌except:把UnicodeDecodeError一起吞掉,返回半个空字符串,问题被藏起来。
✅ 精确捕获UnicodeDecodeError/UnicodeEncodeError,至少记一条日志。
- 用
ascii当默认编码
❌"中文".encode("ascii")抛UnicodeEncodeError。
✅ 需要非 ASCII 时就用utf-8,别用ascii。
总结
| 报错 | 发生环节 | 第一反应 |
|---|
UnicodeDecodeError | bytes.decode/ 读文件 | 编码猜错了,换正确编码 |
UnicodeEncodeError | str.encode/ 输出 | 目标编码表示不了,换 utf-8 |
SyntaxError: Non-UTF-8 code | 解析源码 | 源文件存成 UTF-8 |
编码排查的核心,是先读报错再动手:它已经告诉你字节、位置和用错的编码名。顺着这三条往下查,比反复换编码试运气高效得多。另外记住一个方向:新代码一律显式指定encoding="utf-8",能挡掉绝大多数跨平台问题。