问题的起点

计算机只认识数字(二进制)。文字必须被映射成数字——这就是字符编码。

早期的混乱

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)1A = 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 会被当作内容输出)。

说明:本文为字符编码的知识整理。