unicode i18n programming text-processing

Unicode 内部机制:字形群、代码点和规范化形式

深入探讨 Unicode 的内部工作原理,涵盖字形群、代理对和文本规范化的基本规则 (NFC, NFD)。

Unicode 内部机制:字形群、代码点和规范化形式

如果你曾经纳闷为什么你的代码显示字符串 "é" 的长度是 2 而不是 1,或者为什么一个简单的表情符号就能让你的数据库崩溃,那么你已经遇到了 Unicode 隐藏的复杂性。在现代全球化的数字世界中,理解文本已不再像将一个字节映射到一个字符那么简单。

在本指南中,我们将探索 Unicode 的基本概念,从原始的代码点到高级的字形群,并解释为什么文本规范化是字符串比较中默默无闻的英雄。


1. 构建块:代码点和代码单元

从本质上讲,Unicode 是人类使用过的每一个字符的巨型列表,从古埃及象形文字到最新的表情符号。

代码点(“ID”)

代码点 (Code Point) 是分配给字符的唯一编号。它写作 U+ 后跟一个十六进制数。例如:

  • U+0041 是 'A'
  • U+1F600 是 '😀'

代码单元(“字节”)

代码单元 (Code Unit) 是用于表示代码点的物理存储单位。大小取决于编码方式(UTF-8 使用 8 位单元,UTF-16 使用 16 位单元)。

UTF-16 代理对

UTF-16 是 JavaScript、Java 和 C# 使用的内部编码。因为它使用 16 位代码单元,所以它只能直接表示 $2^{16} = 65,536$ 个字符。为了表示超出此范围的字符(如大多数表情符号),UTF-16 使用 代理对 (Surrogate Pair) —— 两个 16 位单元组合在一起表示一个代码点。

  • 示例: '😀' 表情符号是一个代码点,但它占用 两个 UTF-16 单元。这就是为什么在 JavaScript 中 "😀".length 返回 2

2. 字形群:用户看到的

程序员看到的是代码点,而用户看到的是 字形 (Graphemes)

什么是字形群?

字形群 (Grapheme Cluster) 是显示为单个视觉单元的一个或多个代码点的序列。

  • 示例: 字符 'é' 可以存储为:
    1. 单个代码点:U+00E9 (LATIN SMALL LETTER E WITH ACUTE)
    2. 两个代码点的组合:U+0065 (字母 'e') + U+0301 (组合用锐音符) 对于用户来说,它们看起来一模一样。对于计算机来说,它们是完全不同的字符串。

3. 规范化的力量:NFC 和 NFD

为了使字符串比较可靠,我们必须“规范化”我们的文本,使视觉上相同的字符具有相同的二进制表示。

规范化形式 D (NFD) - 规范分解

NFD 将字符分解为其组成部分。

  • 'é' 变为 'e' + '´'(两个代码点)。

规范化形式 C (NFC) - 规范组合

NFC 尽可能将组成部分组合成单个字符。

  • 'e' + '´' 变为 'é'(一个代码点)。
  • 大多数 Web 应用程序将 NFC 作为标准。

兼容性规范化 (NFKC, NFKD)

这些形式更进一步,对“视觉相似”但在含义上不完全相同的字符进行规范化。例如,它会将用于表示“平方”的符号 '²' 转换为数字 '2'。这对于搜索索引很有用,但可能会丢失重要的格式信息。


4. 开发者最佳实践

  1. 始终规范化用户输入: 在比较字符串(如用户名或密码)时,在存储或检查之前始终将它们规范化为 NFC。
  2. 使用感知字形的库: 如果你需要正确计算字符串的长度(按照用户看到的),不要使用 .length。在现代浏览器中请使用库或 Intl.Segmenter API。
  3. 警惕 UTF-16 长度: 记住许多字符是代理对。在 Python 或 Rust 中,字符串默认是 UTF-8,但在 JS/C#/Java 中,你必须小心索引操作。

结论

Unicode 是现代工程的杰作,旨在解决遗留字符编码的混乱局面。通过理解代码点和字形之间的区别,并掌握 NFC 和 NFD 等规范化形式,你可以构建出能为每位用户(无论其语言或使用的设备如何)正确处理文本的应用程序。