问题的起点
计算机只认识数字(二进制)。文字必须被映射成数字——这就是字符编码。
早期的混乱
ASCII(1963):用 7 位表示 128 个字符(英文字母、数字、标点、控制字符)。
问题:只能表示英文。
于是各国各自扩展:
- GB2312 / GBK(中文);
- Big5(繁体中文);
- Shift-JIS(日文);
- EUC-KR(韩文);
- ISO-8859-x 系列(西欧各种语言)。
后果:同一个字节序列,用不同的编码解读,会得到完全不同的字符——这就是乱码(Mojibake)的根源。
Unicode:统一字符集
Unicode 为世界上几乎所有字符分配一个唯一的编号(码点,Code Point)。
写法:U+4E2D(中)。
注意:Unicode 只定义"字符 ↔ 码点"的映射,不定义如何把这些码点存成字节——那是编码方案(UTF)的事。
UTF-8
UTF-8 是 Unicode 最常用的编码方案。
特点:变长编码,用 1–4 个字节表示一个字符。
| 字符范围 | 字节数 | 示例 |
|---|---|---|
| ASCII(U+0000–007F) | 1 | A = 0x41 |
| 拉丁扩展、希腊、西里尔等 | 2 | é |
| 中日韩等(U+0800–FFFF) | 3 | 中 = E4 B8 AD |
| 补充平面(emoji 等) | 4 | 😀 |
UTF-8 的优势:
- 兼容 ASCII:所有 ASCII 字符的 UTF-8 编码与 ASCII 完全相同——这让大量存量软件无需修改就能处理 UTF-8;
- 无字节序问题(不像 UTF-16/32 需要 BOM 来标识大端/小端);
- 节省空间:英文文本用 UTF-8 只需 1 字节/字符。
因此 UTF-8 成为互联网的主导编码——超过 95% 的网页使用 UTF-8。
为什么会出现乱码
乱码的本质:用错误的编码方案去解码字节序列。
典型场景:
- 文件以 UTF-8 保存,但用 GBK 打开 → 中文变成"锟斤拷"之类的怪字;
- 网页声明的编码与实际编码不一致;
- 数据库连接的字符集与表的字符集不一致;
- 程序读写文件时没有显式指定编码,使用了平台默认编码。
经典现象:
- "锟斤拷":UTF-8 的替换字符(U+FFFD,用于表示无法识别的字节)被再次按 GBK 解读的结果。链路是:UTF-8 字节被错误解读 → 生成替换字符 → 替换字符被 UTF-8 编码 → 再被 GBK 解读。
- "烫烫烫":与编码无关,是 Visual C++ 在调试模式下用
0xCC(未初始化栈内存的填充值)被按 GBK 解读的结果。
怎么避免
一、统一使用 UTF-8——这是最重要的原则。
二、显式指定编码,不要依赖平台默认:
open('file.txt', encoding='utf-8').read()三、在边界处声明编码:
- HTML:
<meta charset="utf-8">; - HTTP 响应头:
Content-Type: text/html; charset=utf-8; - 数据库连接:
charset=utf8mb4。
四、注意 MySQL 的 utf8 与 utf8mb4
MySQL 的 utf8 只支持最多 3 字节(即 BMP 平面),不支持 emoji 等 4 字节字符。
应该使用 utf8mb4(真正的完整 UTF-8)。
这是一个非常常见的坑——用 utf8 存 emoji 会报错。
一个延伸:字节序与 BOM
UTF-16/32 存在大端/小端问题,因此可能需要 BOM(Byte Order Mark)——文件开头的特殊字节序列,用来标识字节序。
UTF-8 不需要 BOM(也不推荐加)——加了反而可能导致某些程序解析出错(如 PHP 输出时 BOM 会被当作内容输出)。
说明:本文为字符编码的知识整理。