正在加载,请稍候…

程序员的 Unicode 指南:从 ASCII 到 UTF-8 及更远

面向开发者的 Unicode 入门指南,涵盖 ASCII、Unicode、UTF-8 编码、码点、代理对以及文本处理中的常见陷阱。

简介

每个开发者迟早都会遇到文本编码的字母汤:ASCII、Unicode、UTF-8、UTF-16、码点、代理对。把它们当作魔法咒语很容易,但理解基本原理将帮你避免 bug、安全漏洞和性能问题。本指南带你从 7 位 ASCII 时代走到现代 Unicode,解释每一层是什么、为什么重要以及如何在代码中正确使用它们。

ascii table on a computer screen

ASCII:7 位基础

ASCII(美国信息交换标准代码)是字符编码的鼻祖。它使用 7 位表示 128 个字符:控制字符(0–31)、可打印字符(32–126)和 DEL(127)。每个程序员都应该知道的关键范围:

类别 范围(十进制) 示例
控制字符 0–31 LF(10),CR(13)
空格 32 ' '
数字 48–57 '0'=48,'9'=57
大写字母 65–90 'A'=65,'Z'=90
小写字母 97–122 'a'=97,'z'=122

记忆技巧:'0'=48,'A'=65,'a'=97。小写 = 大写 + 32。

为什么 ASCII 仍然重要

  • UTF-8 向后兼容 ASCII。每个 ASCII 字符串都是有效的 UTF-8 字符串。
  • 许多网络协议(HTTP、SMTP)仍然在头部使用 ASCII。
  • 理解 ASCII 有助于调试编码问题:如果你看到 'A' 显示为 65,你就知道编码很可能是 ASCII 或 UTF-8。

Unicode:通用字符集

ASCII 的致命缺陷:128 个字符无法表示中文、阿拉伯语、表情符号,甚至法语带音调字母。Unicode 通过为每种书写系统(过去和现在)中的每个字符分配一个唯一数字(称为码点)来解决这个问题。

  • 码点用十六进制书写,前缀为 U+U+0041 表示 'A',U+4E2D 表示 '中'。
  • Unicode 编码空间有 1,114,112 个可能的码点(U+0000 到 U+10FFFF)。
  • 目前大约分配了 150,000 个;其余为保留或私用区。

平面和 BMP

编码空间分为 17 个平面,每个平面 65,536 个码点。平面 0 是基本多文种平面(BMP),涵盖大多数现代文字(拉丁、西里尔、中日韩、阿拉伯等)。平面 1–2 包含历史文字、表情符号和罕见的中日韩字符。平面 15–16 是私用区。

unicode plane map showing BMP and supplementary planes

编码 Unicode:UTF-8、UTF-16、UTF-32

码点只是一个抽象数字。要存储或传输它,我们需要一种编码。主要的三种:

UTF-32

  • 每个码点存储为固定的 4 字节整数。
  • 简单但浪费:一个 10 KB 的 ASCII 文件变成 40 KB。
  • 很少用于存储;有时在内部用于处理。

UTF-16

  • 每个码点使用 2 或 4 个字节。
  • BMP 码点(U+0000–U+FFFF)使用 2 个字节。
  • U+FFFF 以上的码点使用代理对:两个 16 位单元,范围在 U+D800–U+DFFF。
  • 代理不是字符;它们是编码产物。它们绝不应出现在 UTF-8 或 UTF-32 中。
  • 用于 JavaScript 字符串、Java 和 Windows API。

UTF-8

  • 变长:每个码点 1–4 个字节。
  • ASCII 字符(U+0000–U+007F)使用 1 个字节——与 ASCII 相同。
  • 非 ASCII 字符使用具有特定前缀模式的多字节序列:
字节 1 字节 2 字节 3 字节 4 码点范围
0xxxxxxx U+0000–U+007F
110xxxxx 10xxxxxx U+0080–U+07FF
1110xxxx 10xxxxxx 10xxxxxx U+0800–U+FFFF
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx U+10000–U+10FFFF

关键属性:字节 0x00–0x7F 从不出现在多字节序列中,因此基于 ASCII 的字符串操作(空终止、搜索 \n,)在 UTF-8 上无需修改即可工作。

工作示例:从码点到 UTF-8 字节

让我们编码表情符号 😄(U+1F604)。

  1. 码点:U+1F604 = 0x1F604 = 128,516(十进制)。
  2. 它在 4 字节范围内(U+10000–U+10FFFF)。
  3. 0x1F604 的二进制:0001 1111 0110 0000 0100(21 位)。
  4. UTF-8 4 字节模式:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  5. 用 21 位值填充 x 位:
    • 11110 000 (0xF0) | 000 1111111110000 10011111
    • 10 01100010011000
    • 10 00010010000100
  6. 结果:F0 9F 98 84(十六进制)。

在我们的 文本转 Unicode 转换器 中试试,看看字节。

常见陷阱

  • 混淆码点和字节:UTF-8 字符串中的“字符”可能是 1–4 个字节。永远不要假设 strlen() 返回字符数。
  • UTF-8 中的代理对:从不会出现;如果你看到它们,数据编码错误。
  • 字节顺序标记(BOM):UTF-16 文件通常以 U+FEFF 开头表示字节序。UTF-8 BOM(EF BB BF)是可选的,但可能破坏 ASCII 工具。
  • 规范化:像 'é' 这样的字符可以表示为单个码点(U+00E9)或 'e' + 组合重音(U+0065 U+0301)。它们在视觉上相同但字节不同。在比较之前使用 Unicode 规范化(NFC、NFD)。
  • 大小写转换依赖于区域设置:土耳其语 'i' → 'İ'(带点的大写 I),而不是 'I'。不要依赖简单的 ±32 来处理非 ASCII 字符。

何时使用哪种编码

编码 最适合 避免用于
UTF-8 网页、Unix/Linux、API、存储 需要按码点随机访问
UTF-16 JavaScript、Java、Windows API 需要 ASCII 兼容性或 ASCII 密集型文本的空间效率
UTF-32 内部处理(罕见) 存储或网络传输

常见问题

Unicode 和 UTF-8 有什么区别?

Unicode 是字符集——从数字到字符的映射。UTF-8 是 Unicode 的一种编码——一种将这些数字表示为字节的方式。其他编码包括 UTF-16 和 UTF-32。

为什么我的字符串长度看起来不对?

在许多语言中,len()length 返回代码单元的数量(UTF-8 的字节,UTF-16 的 16 位字),而不是码点或可见字符。例如,JavaScript 中 "😄".length 返回 2,因为它是一个代理对。使用库函数来计数码点或字素簇。

什么是 BOM,我应该使用它吗?

UTF-16 文件开头的字节顺序标记U+FEFF)告诉读取者字节是大端还是小端。对于 UTF-8,BOM(EF BB BF)是不必要的,因为 UTF-8 没有字节序问题,但某些 Windows 工具会添加它。它可能混淆 Unix 工具;除非必要,否则避免使用。

如何在代码中处理表情符号?

表情符号是 U+FFFF 以上的码点,因此在 UTF-8 中需要 4 个字节,在 UTF-16 中需要代理对。有些表情符号是序列(例如,肤色 + 基础表情符号)。使用支持字素簇的库(例如 Python 的 grapheme,JavaScript 的 Intl.Segmenter)来计数可见字符。

我可以在任何地方使用 UTF-8 吗?

几乎可以。UTF-8 是网页和 Unix/Linux 中的主导编码。Windows 历史上偏爱 UTF-16,但最新版本更全面地支持 UTF-8。对于新项目,UTF-8 通常是最安全的选择。