🧠 第三章 编程语言
📄 一句话概括

「训练师」三个字,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 秒实验

  1. 把「训练师」换成你的名字,量一下三种编码的字节数。
  2. 故意用一个非 UTF-8 的编码存文件,再用默认方式读,看报什么错。
  3. 想一想:为什么全角的 AI 宽度是 4 而不是 2?

常见误区

  1. 「len 就是字节数」——中文状态下差三倍。
  2. 「本机没乱码就不用管编码」——换台机器立刻出事。
  3. 「读写编码随便填,反正能打开」——打开是运气,乱码才是常态。
自测三问(点开对答案) 1. 「训练师」在 UTF-8 和 GBK 下各占多少字节? —— UTF-8 占 9 字节,GBK 占 6 字节。 2. `UnicodeEncodeError` 和 `UnicodeDecodeError` 分别在哪一步发生? —— 前者是把字符转成字节时,后者是把字节转回字符时。 3. 为什么按 `len` 排的中文表格会歪? —— 因为一个汉字宽度是 2、`len` 只算 1,得按显示宽度排。

能打出中文了,接下来把数据装进变量——不同类型装法差别很大。

相关笔记