☰
Python之string编码问题
2026/10/10 4:42:39 网站建设 项目流程

前言


这篇文章是排查问题向的。编码知识本身在另一处讲,这里只干一件事:把你实际会遇到的编码报错,按「报错信息 → 成因 → 解法」一条条列清楚。


Python 3 里和字符串相关的报错,翻来覆去就三类:



  • UnicodeDecodeError:读进来的字节流,按你指定的编码解不开。

  • UnicodeEncodeError:要输出的字符串,按目标编码编不出。

  • SyntaxError: Non-UTF-8 code ...:你的源码文件本身不是合法 UTF-8。


很多人卡住不是因为不会写encode/decode,而是因为不会读报错。这三类报错的信息其实非常有用——它告诉你「哪个字节、在哪个位置、用了哪个编码」。学会读它,问题基本就定位了一半。


前置说明:本机无 Python 解释器,代码与报错文本均为逐行推演与官方文档对照,未经运行。文中引用的行为以 Python 官方文档和 PEP 为准。


一、一条主线:报错总是发生在「转换的那一步」


Python 3 严格区分str(文本,码位序列)和bytes(字节序列)。报错只会发生在两者互转的那一刻,不会平白无故出现:




转换方向方法出错时抛



bytes→strdecode(编码, errors)UnicodeDecodeError

str→bytesencode(编码, 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

解法分三层,从「治本」到「治标」:



  1. 确认真实编码再解。可以试open时换编码,或用内容探测辅助判断。

  2. 换正确的编码:open("data.txt", encoding="gbk")。

  3. 确实不知道编码,且只要大致内容:用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),届时默认编码的困扰会进一步减少。


常见坑点



  1. 混用两个不同编码的字节流拼接


❌ 把 GBK 的字节和 UTF-8 的字节+到一起再解码,必然失败。


✅ 先各自解码成str,再拼接;字节层面不要跨编码拼接。



  1. open()不写 encoding


❌open("f.txt"),换台机器就乱码,还以为是文件坏了。


✅ 一律显式open("f.txt", encoding="utf-8")。



  1. 用ignore掩盖真实问题


❌decode("utf-8", "ignore")让程序不报错,但数据被悄悄丢字。


✅ 先找真实编码;确需容错时优先replace,让丢失可见。



  1. 指望surrogateescape产出可读文本


❌ 用surrogateescape解出来直接拿去比较或显示。


✅ 它只保证「往返不丢字节」,正常文本处理还是要用正确编码。



  1. Python 3 里还写# -*- coding: utf-8 -*-却没存成 UTF-8


❌ 文件是 GBK,却声明 utf-8,报错信息与实际不符,越查越乱。


✅ 让文件实际编码与声明一致;新代码直接存 UTF-8,无需声明。



  1. 在控制台报UnicodeEncodeError就以为代码错了


❌ 满头找 bug,其实字符串完全正确,只是输不出。


✅ 分清「数据对不对」和「输出端能不能表示它」两件事。



  1. 裸except吞掉编码错误


❌except:把UnicodeDecodeError一起吞掉,返回半个空字符串,问题被藏起来。


✅ 精确捕获UnicodeDecodeError/UnicodeEncodeError,至少记一条日志。



  1. 用ascii当默认编码


❌"中文".encode("ascii")抛UnicodeEncodeError。


✅ 需要非 ASCII 时就用utf-8,别用ascii。


总结




报错发生环节第一反应



UnicodeDecodeErrorbytes.decode/ 读文件编码猜错了,换正确编码

UnicodeEncodeErrorstr.encode/ 输出目标编码表示不了,换 utf-8

SyntaxError: Non-UTF-8 code解析源码源文件存成 UTF-8



编码排查的核心,是先读报错再动手:它已经告诉你字节、位置和用错的编码名。顺着这三条往下查,比反复换编码试运气高效得多。另外记住一个方向:新代码一律显式指定encoding="utf-8",能挡掉绝大多数跨平台问题。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询