字符编码漫谈:从乱码说起
你有没有遇到过这种情况:打开一个文本文件,满屏的”锟斤拷”、”烫烫烫”、”��”,或者一串乱七八糟的问号?
这背后,是一段长达半个多世纪的”字符编码进化史”。
一、一切的起点:计算机只认 0 和 1
我们先回到最朴素的问题:计算机怎么”看见”文字?
答案是:它根本看不见。
计算机内部只有电压的高低,对应着 0 和 1 两种状态。无论是图片、音乐、视频,还是你正在读的这段文字,在计算机眼里都是一长串的二进制数字。
1 | 你看到的: "Hi" |
那么问题来了:01001000 凭什么代表 H?
答案是:因为我们约定它代表 H。
这个”约定”就是字符编码——人类和计算机之间的一份”翻译合同”。
二、ASCII:美国人先定了规矩(1963)
故事的开端,是 1963 年的美国。
当时的计算机还是”贵族玩具”,主要用户是说英语的科学家和工程师。他们需要解决的问题很简单:怎么让计算机存储和显示英文?
于是 ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)诞生了。
ASCII 的设计
- 用 7 位二进制(后来扩展为 8 位,即 1 字节)表示一个字符
- 总共可以表示 128 个字符(2⁷ = 128)
- 包括:26 个大写字母、26 个小写字母、10 个数字、各种标点符号、一些控制字符
| 字符 | 十进制 | 二进制 |
|---|---|---|
A |
65 | 01000001 |
a |
97 | 01100001 |
0 |
48 | 00110000 |
| 空格 | 32 | 00100000 |
对于只用英文的世界,ASCII 完美无缺。
但是…世界不只有英文。
三、混乱年代:每个国家都自己搞一套
当计算机走向世界,问题来了:ASCII 只有 128 个位置,根本装不下中文、日文、阿拉伯文、希伯来文……
于是各个国家和地区开始自己设计编码方案,这造就了一段”群雄割据”的混乱年代。
中国:从 GB2312 到 GBK 再到 GB18030
GB2312(1980)
中国国家标准发布,收录了 6,763 个汉字 和 682 个其他符号(标点、希腊字母等)。
它的思路很巧妙:
- 一个英文字符仍然用 1 字节(兼容 ASCII)
- 一个中文字符用 2 字节,且每个字节的最高位都是 1(避免和 ASCII 冲突)
GBK(1995)
GB2312 只收录了 6,763 个汉字,但实际汉字有 9 万多个。很多人名(比如朱镕基的”镕”)、古汉语用字、繁体字都打不出来。
微软为了在 Windows 95 中支持更多汉字,扩展出了 GBK,收录了 21,003 个汉字,向下兼容 GB2312。
GB18030(2000)
国家强制标准,收录了所有 CJK(中日韩)统一汉字,包括少数民族文字,共 70,000+ 字符。采用 1/2/4 字节变长编码。
其他国家也在”各自为政”
| 编码 | 地区 | 字符数 | 字节数 |
|---|---|---|---|
| ASCII | 美国 | 128 | 1 |
| ISO-8859-1 | 西欧 | 256 | 1 |
| GB2312/GBK | 中国大陆 | 2 万 | 1-2 |
| Big5 | 台湾、香港 | 1.3 万 | 1-2 |
| Shift-JIS | 日本 | 8 千 | 1-2 |
| EUC-KR | 韩国 | 1.1 万 | 1-2 |
混乱的代价:乱码遍地
由于每种编码对同一段字节序列的解释都不一样,问题接踵而至:
🔥 场景一:同事发来的文件打不开
你在中国大陆用 GBK 保存的文件,发给台湾同事。他用 Big5 打开,看到的全是乱码。
🔥 场景二:网页一片”锟斤拷”
浏览器以错误的编码解析网页,中文全部变成”锟斤拷烫烫烫”——这正是著名的”锟斤拷之谜”(GBK 解码 UTF-8 替换字符 � 时产生的现象)。
🔥 场景三:多语言无法共存
一个文档里既要写中文又要写日文?对不起,GBK 装不下日文,Shift-JIS 也装不下中文,根本无解。
整个世界的文字交流,被困在了一个个孤岛上。
四、Unicode:让全世界字符住进同一栋楼(1991)
20 世纪 90 年代,几家科技公司(Xerox、Apple、Sun、IBM 等)联合发起了 Unicode 联盟,目标是:
给世界上每一个字符,分配一个独一无二的编号。
这个编号叫做码点(Code Point),用 U+ 加十六进制数字表示。
1 | 'A' → U+0041 |
Unicode 的容量极其庞大:从 U+0000 到 U+10FFFF,理论上可以容纳 超过 100 万个字符。截至目前,已经收录了 14 万多个字符,涵盖了几乎所有现存语言、历史文字、emoji、数学符号、甚至麻将牌和扑克牌。
但 Unicode 只解决了一半问题
Unicode 只规定了”字符 → 编号” 的对应关系,没规定”编号 → 字节” 怎么存。
比如 中 的码点是 U+4E2D,需要 2 字节才能装下:4E 2D。
但问题来了:
- 那
A(U+0041)怎么办?也用 2 字节存成00 41?那英文文档体积要翻倍! - emoji
😀(U+1F600)需要 3 字节,又怎么和 2 字节的中文混存? - 字节里出现的
00,会让以\0结尾的 C 字符串提前截断。
于是,需要一种”编码方案”来解决 Unicode 的存储问题。
UTF-8、UTF-16、UTF-32 都是这种方案,其中 UTF-8 笑到了最后。
五、UTF-8:伟大的妥协(1992)
UTF-8 由 Unix 之父 Ken Thompson 和 Rob Pike 在 1992 年的一个晚餐餐巾纸上设计完成。它的天才之处在于:变长编码。
UTF-8 的核心设计
不同范围的字符,用不同长度的字节来存:
| Unicode 范围 | UTF-8 字节数 | 字节模式 |
|---|---|---|
| U+0000 ~ U+007F | 1 字节 | 0xxxxxxx |
| U+0080 ~ U+07FF | 2 字节 | 110xxxxx 10xxxxxx |
| U+0800 ~ U+FFFF | 3 字节 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 ~ U+10FFFF | 4 字节 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
看一个具体例子:中文”中”是怎么编码的?
1 | 1. 查 Unicode 码表:'中' = U+4E2D |
所以 "中" 在 UTF-8 中占 3 字节,即 E4 B8 AD。
UTF-8 的三大杀手锏
1. 完美兼容 ASCII
前 128 个字符(英文字母、数字、符号)的 UTF-8 编码和 ASCII 完全一致,都是 1 字节。这意味着所有过去 50 年写的英文文档、英文程序代码,不用任何改动就是合法的 UTF-8。
2. 自同步性(Self-synchronizing)
通过字节首部的特征,可以瞬间判断出当前位置:
0xxxxxxx→ 单字节字符10xxxxxx→ 多字节字符的”后续字节”110xxxxx、1110xxxx、11110xxx→ 多字节字符的”起始字节”
即使从一个文件的中间任意位置开始读取,也能快速找到下一个完整字符的边界。单个字节损坏,不会导致整个文件报废。
3. 没有字节序问题
UTF-16 和 UTF-32 因为每个字符跨越多个字节,需要规定哪个字节在前(大端序 or 小端序),所以要在文件开头加 BOM(Byte Order Mark)标记。
UTF-8 是按字节为单位读取的,根本不存在字节序问题,文件可以”裸奔”。
六、为什么中文要用 3 个字节?
这是一个常见的疑问:为什么英文 1 字节,中文却要 3 字节?GBK 不是只用 2 字节就够了吗?
数学上的限制
UTF-8 的 2 字节模板 110xxxxx 10xxxxxx 实际可用位只有 11 位,最多能表示 2¹¹ = 2,048 个字符。
而中文常用汉字就有 7,000 多个,加上扩展共 9 万多。11 位的”小房间”根本住不下。
只能升级到 3 字节模板,可用 16 位,能装 65,536 个字符——这才够用。
Unicode 的”分区”逻辑
Unicode 在分配码点时,大致遵循”字符多的语言往后排“:
1 | U+0000 ~ U+007F ASCII 英文 → 1 字节 |
中文汉字主要在 U+4E00 ~ U+9FFF,正好落在 3 字节区间。
这是不是对中文的”歧视”?
表面看,中文存储要花 3 倍空间,似乎”吃亏”了。但实际上:
| 维度 | UTF-8 | GBK |
|---|---|---|
| 中文存储 | 3 字节 | 2 字节 ✓ |
| 英文存储 | 1 字节 ✓ | 1 字节 ✓ |
| 全球字符支持 | 100 万+ ✓ | 仅 2 万 |
| 中日混排 | 支持 ✓ | 乱码 |
| Emoji 支持 | 支持 ✓ | ❌ |
| 跨平台兼容 | 全球标准 ✓ | 中文区限定 |
而且现实中:
- HTML、JSON、代码里有大量英文(标签、关键字、字段名)
- 经过 Gzip 压缩后,UTF-8 和 GBK 的体积差距几乎可以忽略
- 网络带宽和存储已经不再是稀缺资源
用一点点空间换全球通用性,这笔账太划算了。
七、今天的世界:UTF-8 一统江湖
进入 21 世纪后,UTF-8 几乎赢得了所有战场:
- Web:截至 2024 年,全球 98% 以上的网页使用 UTF-8 编码
- Linux / macOS:系统默认编码就是 UTF-8
- JSON:RFC 8259 强制要求使用 UTF-8
- Python 3:源码默认编码是 UTF-8,字符串内部用 Unicode
- MySQL:推荐使用
utf8mb4(完整的 UTF-8,支持 emoji) - HTML5:
<meta charset="UTF-8">是标配
只有 Windows 系统因为历史包袱,中文版默认还在使用 GBK(CP936),这就是为什么很多 Python 初学者在 Windows 上遇到”奇怪的乱码问题”——一个文件里 UTF-8 和 GBK 在打架。
八、实战:常见的”乱码”是怎么产生的?
理解了原理后,我们来看看几种典型的乱码场景。
场景一:UTF-8 文件被当成 GBK 打开
1 | # 用 UTF-8 编码保存"你好" |
原因:UTF-8 的 6 字节被 GBK 当成 3 个 2 字节的中文字符来解读。
场景二:著名的”锟斤拷”
当 GBK 程序遇到无法识别的 UTF-8 字符时,常会用 �(替换字符,UTF-8 编码为 EF BF BD)替代。两个连续的 � 在 UTF-8 中是 6 字节 EF BF BD EF BF BD,用 GBK 解读就成了:
1 | EF BF → 锟 |
→ “锟斤拷” 横空出世!
场景三:Windows 的”烫烫烫”
Visual Studio Debug 模式下,未初始化的栈内存被填充为 0xCC(int 3 中断指令)。两个 0xCC 在 GBK 中正好是汉字”烫”。
→ “烫烫烫” 由此而来。
九、给开发者的建议
- 新项目一律用 UTF-8,没有任何理由用 GBK
- 数据库选
utf8mb4而不是utf8(MySQL 的utf8是个历史 bug,只支持 3 字节) - 文件读写显式指定编码:
open(file, encoding='utf-8') - HTTP 响应头加上
Content-Type: text/html; charset=utf-8 - 遇到乱码先确认两件事:文件实际编码是什么?程序用什么编码读的?
- 不要相信”自动检测编码”:大多数库的自动检测都不靠谱
十、总结:编码的来龙去脉
让我们最后梳理一遍整个故事:
1 | 1963 ASCII 美国人定的规矩,128 个字符,只够英文用 |
字符编码的本质,是人类与机器之间的一份”翻译合同”。
从 ASCII 的”美国中心”,到各国分裂的”巴别塔”,再到 Unicode/UTF-8 的”世界大同”,这是一段技术不断追赶人类多样性的历史。
下一次当你流畅地在网页上看到中文、英文、日文、emoji 共存,请记得感谢一下 60 年前那批默默工作的工程师们——是他们的智慧,让全世界的文字可以在计算机里和平共处。