乱码看起来是「随机的鬼画符」,其实恰恰相反:每一种乱法都是确定性的——同样的字节、同样的错误路径,产出的乱码一个字都不会差。这意味着两件事:看形态就能反推出错环节,以及大部分乱码可以精确地逆向还原。
先建立一个模型:文本在计算机里只是字节,「编码」是字符↔字节的映射表。乱码只有一种成因——写入时用了 A 表,读取时查了 B 表。剩下的全部差异,只在于 A 和 B 分别是谁。
第一类:浣犲ソ——GBK 误读 UTF-8,可还原
「你好」的 UTF-8 字节是 E4 BD A0 E5 A5 BD。GBK 是双字节编码,解码器把这 6 个字节两两切分:
E4 BD → 浣
A0 E5 → 犲
A5 BD → ソ
于是「你好」变成「浣犲ソ」。特征非常好认:中文膨胀成 1.5 倍数量的生僻汉字,偶尔混进日文假名和符号——因为 UTF-8 里一个汉字 3 字节,GBK 里一个字 2 字节,3:2 的错位让切分点在字与字之间漂移。
还原是纯机械操作:把「浣犲ソ」按 GBK 编码回 E4 BD A0 E5 A5 BD,再按 UTF-8 解码,「你好」原样回来。GBK 的双字节空间很满,绝大多数字节对都有字可查,所以这条路径几乎无损可逆。
第二类:䏿–‡——Latin-1 误读 UTF-8,可还原
西文世界的对应版本。Latin-1(及其超集 Windows-1252)是单字节编码,每个字节独立成字:
「中文」的 UTF-8: E4 B8 AD E6 96 87
按 Latin-1 逐字节: ä ¸ æ – ‡
特征:每个汉字变成 3 个带音标的西欧字母,法文的 é 变成 é。高发于老邮件客户端、没有声明 charset 的网页、以及 MySQL 用 latin1 表存 UTF-8 数据这个经典事故现场。
由于 Latin-1 的 256 个码位与字节值一一对应,这条路径理论上完全无损,还原成功率是所有乱码里最高的。它还有个常见变体:乱码文本又被按 UTF-8 保存了一次(双重编码),é → é → é 越滚越长——多逆转一轮即可,同样可逆。
第三类:锟斤拷——替换符的尸体,不可还原
这是最出名、也最没救的一类。链路上多了关键一步:
- 某次解码失败,程序把读不懂的字节替换成 Unicode 官方替换字符 U+FFFD(就是你见过的 �)
- U+FFFD 的 UTF-8 编码是
EF BF BD;两个连着就是EF BF BD EF BF BD - 这串字节再被按 GBK 切分:
EFBF=锟、BDEF=斤、BFBD=拷
注意第 1 步的性质和前两类完全不同:前两类只是查错了表,字节原封未动;而这里原始字节已经被 EF BF BD 覆盖了。信息在那一刻物理性丢失,后面无论怎么转换,都只是在处理替换符的尸体。
判断标准就一条:乱码字符和原始字节是否还保持一一对应。对应关系还在的(浣犲ソ、䏿–‡)能救;已经被 �、? 统一抹平的(锟斤拷、满屏问号)救不了,止损方式是找备份或从源头重新导出。
同理还有文件开头孤零零的「锘」——那是 UTF-8 的 BOM(EF BB BF)被按 GBK 读出的产物(EFBB=锘),删掉即可,正文通常无恙。BOM 本身的来龙去脉见怎么判断一个文件的编码。
第四类:烫烫烫——根本不是编码问题
Visual C++ 的 Debug 模式会给未初始化的内存填充固定值:栈填 0xCC(恰好是 x86 的 int3 断点指令,代码跑飞时能立即断住),堆填 0xCD。这些字节被当成 GBK 字符串打印时:
CC CC → 烫(栈)
CD CD → 屯(堆)
所以「烫烫烫」是 C/C++ 程序读了未初始化变量的症状,和文件编码毫无关系——该修的是代码,不是文件。顺带一提,Release 模式不做填充,同一个 bug 会表现为随机垃圾而非整齐的三个「烫」,反而更难发现。
容易误判成乱码的两种情况
- 方块 □ 或零星 �,但前后文字正常:多半是字体缺字形(生僻字、emoji、少数民族文字),换设备或字体就能显示。字节没有任何问题。
- 简体系统打开整段「繁体怪字」:Big5 文本被按 GBK 解码。属于可还原的查错表类型,繁简双字节结构相同,多数字符可逆。
实际修复
判断出类型后,修复就是做一次逆向转换。用文件编码转换的乱码修复功能,粘入乱码文本,工具会自动尝试 GBK↔UTF-8、Latin-1↔UTF-8 以及双重编码等常见误读组合,给出候选结果供比对——整个过程在本地完成,文本不会上传。
修复前唯一要紧的纪律:别再保存。乱码文件被记事本「另存为」一次,还能还原的字节就可能被二次转换抹掉。同样的字节视角问题,在 ZIP 中文文件名乱码和字幕文件乱码里也会遇到,机制同源。想亲手看字节,可以把文本丢进 HEX ↔ 文本对照验证。