「中文显示成方块」背后有三个完全不同的毛病:缺字形、编码错、路径错。 现象不一样,查法也不一样,混着查会绕很久。
上一节已经摸到第一种(缺字形),这一节把三种凑齐,给出定位顺序。 学会这套,遇到乱码就不用再靠猜。
一、成因一:字体里没有这个字形
字体 非透明像素 看到的样子
arial 104 两个空心方框 msyh 389 两个正常汉字
同样的字、同样的字号,msyh 是 arial 的 3.7 倍。 这一类乱码的特征是「方块大小一致、排列整齐」——它画的是缺字符号,不是字。
注意它不报错、不警告,画面上就是一片整齐的方框。 新手很容易以为是「编码坏了」,其实是字体压根没这个字形。
二、成因二:编码不对,字被解成了别的字
环节 结果 长度
原文 水印 2 UTF-8 字节 e6b0b4e58db0 6 按 GBK 解 姘村嵃 3 按 latin1 解 æ´å° 6
关键看长度:2 个汉字只有 6 个字节,按 GBK 解出来变成 3 个字, 按 latin1 解出来变成 6 个字符——长度一变,就是解错了。
这一类乱码的特征是「全是怪字,但每个都长得不一样」。 与豆腐块的「整齐划一」正好相反,光看现象就能先分到哪一类。
三、成因三:路径写错,悄悄回落到默认字体
写法 会发生什么
C:\Windows\Fonts\msyh.ttc 冒号被当分隔符,路径只剩 C C\:/Windows/Fonts/msyh.ttc 正确 'C\:/Windows/Fonts/msyh.ttc' 命令行里带空格才需要
这种错最阴:它不报错,只是换成一个没有汉字的默认字体, 于是你看到的现象与「字体没字形」一模一样。
查半天查不到编码上去,原因就是它把你引到了错误的第一类。 所以定位顺序很重要:先看方块长什么样,再决定往哪儿查。
四、三步定位法
现象 先查什么
方块大小一样、排列整齐 字体里有没有这个字形 全是怪字、长度不对 编码,字节数与字符数比值 现象同第一类但字体明明对 fontfile 路径转义对不对
三行按顺序查,一分钟内能定案。 难的是很多人直接从第二行开始,把字体问题当成了编码问题。
一条通用经验:凡是从命令行传进去的中文, 先假设编码是对的,先怀疑「字体」与「路径」——这两类在实际项目里出现得更多。
五、动手 Demo:三种成因各造一次
先用 arial 与 msyh 各渲染两个汉字,量像素差。 再把同一个字符串按三种编码解读,看长度怎么变。
import numpy as np
from PIL import Image, ImageDraw, ImageFont
def px(text, fn):
f = ImageFont.truetype(u"C:/Windows/Fonts/" + fn, 24)
im = Image.new(u"RGBA", (80, 40), (0, 0, 0, 0))
ImageDraw.Draw(im).text((0, 0), text, font=f,
fill=(255, 255, 255, 255))
return int((np.array(im)[:, :, 3] > 0).sum())
pa, pb = px(u"中文", u"arial.ttf"), px(u"中文", u"msyh.ttc")
print(u"arial %d msyh %d 倍数 %.1f" % (pa, pb, pb / float(pa)))
s = u"水印"
b = s.encode(u"utf-8")
print(u"原文 %s 长度 %d" % (s, len(s)))
print(u"UTF-8 %s 长度 %d" % (b.hex(), len(b)))
print(u"GBK %s 长度 %d" % (b.decode(u"gbk", u"replace"),
len(b.decode(u"gbk", u"replace"))))
print(u"latin1 %s 长度 %d" % (b.decode(u"latin1"),
len(b.decode(u"latin1"))))
实跑输出:
--------------------------------------------------------------------
6.6.2 解决中文乱码问题——三种成因,三种查法
--------------------------------------------------------------------
**「中文显示成方块」这句话背后其实有三个完全不同的毛病。**
**① 成因一:字体里没有这个字形**
字体 非透明像素 看到的样子
-----------------------------------------
arial 104 两个空心方框
msyh 389 两个正常汉字
**同样的字、同样的字号,msyh 是 arial 的 3.7 倍。**
**这一类乱码的特征是「方块大小一致、排列整齐」——**
**因为它画的是缺字符号,不是字。**
**② 成因二:编码不对,字被解成了别的字**
环节 结果 长度
------------------------------------------
原文 水印 2
UTF-8 字节 e6b0b4e58db0 6
按 GBK 解 姘村嵃 3
按 latin1 解 æ°´å° 6
**关键看长度:2 个汉字只有 6 个字节,按 GBK 解出来变成 3 个字,**
**按 latin1 解出来变成 6 个字符——长度一变,就是解错了。**
**这一类乱码的特征是「全是怪字,但每个都长得不一样」。**
**③ 成因三:fontfile 路径写错,回落到默认字体**
写法 会发生什么
-----------------------------------------------------------------
C:\Windows\Fonts\msyh.ttc 冒号被当参数分隔,路径只剩 C
C\:/Windows/Fonts/msyh.ttc 正确
'C\:/Windows/Fonts/msyh.ttc' 命令行里带空格时必须这样套
**这种错最阴:它不报错,只是悄悄换成一个没有汉字的默认字体,**
**于是你看到的现象与「字体没字形」一模一样,查半天查不到编码上去。**
**④ 三步定位法**
现象 先查什么
-------------------------------------------------------
方块大小一样、排列整齐 字体里有没有这个字形
全是怪字、长度不对 编码,字节数与字符数比值
用的是默认字体 fontfile 路径转义对不对
**三行按顺序查,一分钟内能定案——**
**难的是很多人直接从第二行开始查,把字体问题当成了编码问题。**
固定 80×40 的画布,是为了不让画布尺寸干扰像素计数。 换了画布,同一个字的像素数不变,但如果画布太小,字会被裁掉。
decode(..., u"replace") 里的 replace 让解不出的字节变成问号,
而不是抛异常——排查乱码时,先看到东西比看到异常有用。
长度对比是最快的判据:字节数 6、GBK 解出 3 个字、latin1 解出 6 个字符, 三个数一对,就知道中间被谁动过手脚。
30 秒实验
- 造第三种:把 fontfile 写成
C:\Windows\...(不转义),看它怎么回落。 - 换个编码:把文件存成 GBK 再读,看字节数怎么变。
- 写个检查:给一段文字,写函数判断它「更像 UTF-8 还是 GBK」。
常见误区
- 看到方块就去查编码。方块多数是缺字形,不是编码错。
- 以为 fontfile 写错会报错。它悄悄换成默认字体,什么都不说。
- 只比字符数不比字节数。汉字 3 字节、字母 1 字节,不比字节看不出问题。
自测三问(点开对答案)
1. 豆腐块与编码错怎么区分? —— 豆腐块大小一致、排列整齐;编码错是各种怪字,长度也不对。 2. fontfile 写错为什么不报错? —— 它会回落到编译期默认字体,行为上是「成功」的,只是字不对。 3. 最快的编码判据是什么? —— 比字节数与字符数的比值:UTF-8 明显大于 1,GBK 小于 2。- ☐ (本节完,下一节 6.6.3 -vf 的 drawtext 中使用绝对路径)