「训练师」三个字,UTF-8 存 9 字节、GBK 存 6 字节、UTF-16 也是 6 字节。
用 UTF-8 去读 GBK 文件会直接抛异常。终端里中文占 2 格,所以 len 是 3、显示宽度是 6。
上一节 Hello AI 的字符数和字节数刚好都是 8,这节换成中文,
这两个数立刻分家——所有「表格歪了」「文件乱码」都从这儿开始。
一、同一个词,三种字节账
「训练师」有 3 个字符,但存成文件是多少字节,取决于用什么编码:
- UTF-8:9 字节,每个汉字 3 字节。头几字节是 e8 ae ad e7 bb 83。
- GBK:6 字节,每个汉字 2 字节。
- UTF-16-LE:也是 6 字节,但字节顺序和 GBK 完全不同。
同样的三个字,三种长度。所以「这个文件多大」这个问题,必须先问「按什么编码算」。
二、本机的默认编码
Demo 读到本机 locale.getpreferredencoding(False) 是 utf-8,文件系统编码也是 utf-8,
标准输出编码还是 utf-8。
三个都统一,说明这台机器上不用管编码——但这是幸运,不是常态。
Windows 上默认编码可能是 cp936(也就是 GBK),同一个脚本换台机器就乱码。
所以写文件永远显式指定 encoding='utf-8'。少写这四个字,就是把命运交给运行环境。
三、编码错了会怎么样
两种错法,报错信息完全不同,得分开记:
- 编不出来:UnicodeEncodeError。
比如硬把中文按 ascii 编码,Demo 里报的是 'ascii' codec can't encode characters in position 0-2。
- 读不出来:UnicodeDecodeError。用 UTF-8 去读一个 GBK 文件,Demo 里直接判定「会炸」。
Demo 还量了文件体积:同样一行「训练师」,存 UTF-8 是 10 字节,存 GBK 是 7 字节,
差的就是结尾那个换行符之前的部分。
记住这条:写的时候用什么编码,读的时候必须用同一种。换一个,就是乱码或者异常,没有第三种结果。
四、显示宽度:中文占两格
这一个是排版问题,很容易被忽略:终端里一个汉字占两格,一个英文字母占一格。
Demo 的结果:AI 的 len 是 2、宽度 2;训练师 的 len 是 3、宽度 6;
AI训练师 的 len 是 5、宽度 8;全角的 AI 的 len 是 2、宽度 4。
所以按 len 排的表格一定歪。
要按「显示宽度」排——一个字符宽度是 2 还是 1,查它的 East Asian Width 属性。
这就是本书所有 Demo 输出表格都对齐的原因:它们不是用 len 排的,是用宽度排的。
五、动手 Demo:同一个「训练师」,三种字节账
把同一个词分别用 UTF-8/GBK/UTF-16-LE 编码,打印字节数与十六进制;
读本机的默认编码、文件系统编码、标准输出编码;试一次按 ascii 编码的报错;
再把同一行分别存成两种编码,量体积、看文件头,最后用 UTF-8 去读 GBK;末尾对比 len 与显示宽度。
s = '训练师'
print(len(s)) # 3 个字符
for enc in ('utf-8', 'gbk', 'utf-16-le'):
b = s.encode(enc)
print(enc, len(b), b.hex(' '))
print(locale.getpreferredencoding(False)) # 本机默认
with io.open(p, 'w', encoding='gbk', newline='\n') as f:
f.write(s + '\n')
io.open(p, encoding='utf-8').read() # 会抛 UnicodeDecodeError
实跑输出:
--------------------------------------------------------------------
中文编码:同一个「训练师」,三种字节账
--------------------------------------------------------------------
字符数 3
utf-8 字节数 9 e8 ae ad e7 bb 83 e5 b8 88
gbk 字节数 6 d1 b5 c1 b7 ca a6
utf-16-le 字节数 6 ad 8b c3 7e 08 5e
本机默认编码 utf-8
文件系统编码 utf-8
标准输出编码 utf-8
硬用 ascii 编码 报错:UnicodeEncodeError
报错原文 'ascii' codec can't encode characters in position 0-2
存成 utf-8 10 字节,头 6 字节 e8 ae ad e7 bb 83
存成 gbk 7 字节,头 6 字节 d1 b5 c1 b7 ca a6
用 utf-8 读 gbk 文件 会炸
显示宽度(终端里占几格,中文算 2):
内容 len 显示宽度
------------------------
AI 2 2
训练师 3 6
AI训练师 5 8
AI 2 4
表格对齐要按第三列算,按第二列一定歪。
字节数一行比一行明白:UTF-8 9 字节、GBK 6 字节、UTF-16-LE 6 字节。
三个数字都跟着编码变,跟字符数(3)无关。
三种编码的十六进制头完全不同:e8 ae ad 是 UTF-8 里「训」的三字节,
d1 b5 是 GBK 里「训」的两字节。看到 e8/e5/e4 开头的,基本就是 UTF-8 中文。
本机三个编码全是 utf-8,所以直接 open 也没问题。
但脚本要发到别人机器上,显式写 encoding='utf-8' 才是稳妥做法。
按 ascii 编码报的是 UnicodeEncodeError;用 UTF-8 读 GBK 文件是 UnicodeDecodeError。
编和解两个方向,两个异常名,别记混。
最后那张表是关键:训练师 的 len 是 3、宽度 6;AI训练师 的 len 是 5、宽度 8。
按 len 对齐必然歪。
30 秒实验
- 把「训练师」换成你的名字,量一下三种编码的字节数。
- 故意用一个非 UTF-8 的编码存文件,再用默认方式读,看报什么错。
- 想一想:为什么全角的
AI宽度是 4 而不是 2?
常见误区
- 「
len就是字节数」——中文状态下差三倍。 - 「本机没乱码就不用管编码」——换台机器立刻出事。
- 「读写编码随便填,反正能打开」——打开是运气,乱码才是常态。
自测三问(点开对答案)
1. 「训练师」在 UTF-8 和 GBK 下各占多少字节? —— UTF-8 占 9 字节,GBK 占 6 字节。 2. `UnicodeEncodeError` 和 `UnicodeDecodeError` 分别在哪一步发生? —— 前者是把字符转成字节时,后者是把字节转回字符时。 3. 为什么按 `len` 排的中文表格会歪? —— 因为一个汉字宽度是 2、`len` 只算 1,得按显示宽度排。能打出中文了,接下来把数据装进变量——不同类型装法差别很大。
- ☐ (本节完,下一节 3.2.3 设置Python变量赋值:定义不同数据)