科普

Unicode 完全指南:从字符编码的混乱到全球统一标准

全面解析 Unicode 的起源、设计理念、编码原理与实际应用。深入理解码位、平面、UTF-8/UTF-16/UTF-32 编码方案的区别,掌握 Unicode 在编程中的正确处理方式。附在线 Unicode 编码/解码工具。

在互联网时代,我们每天都在和各种语言的文字打交道——中文、英文、日文、阿拉伯文、Emoji 表情……这一切能在同一个网页、同一条消息中和平共存,都要归功于一个伟大的标准:Unicode。本文将从字符编码的历史困境出发,全面解析 Unicode 的设计哲学、编码原理、UTF 变体和实际编程应用。

如果你需要快速进行 Unicode 编码与解码转换,可以使用我们的 Unicode 编码/解码在线工具,支持 Unicode 转义序列(\uXXXX)、HTML 实体、UTF-8 十六进制等多种格式。

1. 为什么需要 Unicode?

1.1 编码混乱的年代

在 Unicode 诞生之前,世界各地的计算机系统各自为政地定义字符编码:

编码标准覆盖语言说明
ASCII英语仅 128 个字符,7 位编码
ISO 8859-1(Latin-1)西欧语言扩展 ASCII,增加了重音字符
GB2312 / GBK / GB18030中文中国国家标准,逐步扩展
Big5繁体中文台湾和香港地区使用
Shift-JIS / EUC-JP日文日本工业标准
EUC-KR韩文韩国标准
Windows-1251俄文(西里尔文)微软定义的编码
TIS-620泰文泰国标准

这些编码互不兼容,同一个字节值在不同编码中可能代表完全不同的字符。当一个使用 GBK 编码的中文网页被 Shift-JIS 解码器打开时,就会出现经典的「乱码」(mojibake)现象。

1.2 统一的梦想

1987 年,Xerox 公司的 Joe Becker 和 Apple 公司的 Lee Collins、Mark Davis 开始构想一种能够涵盖全世界所有文字的编码方案。他们的目标是:

  • 通用性(Universal):覆盖全球所有现代书写系统
  • 统一性(Uniform):使用统一的编码空间
  • 唯一性(Unique):每个字符有且仅有一个码位

1991 年,Unicode 联盟(Unicode Consortium)发布了 Unicode 1.0,包含 7,161 个字符。此后,Unicode 不断扩展,截至 2024 年发布的 Unicode 16.0,已经收录了超过 154,000 个字符,覆盖 168 种现代和历史书写系统。

2. Unicode 核心概念

2.1 码位(Code Point)

Unicode 中最基本的单位是码位(Code Point)。每个字符被分配一个唯一的非负整数,表示为 U+ 后跟 4 到 6 位十六进制数字。

常见示例:

字符码位名称
AU+0041LATIN CAPITAL LETTER A
U+4E2DCJK UNIFIED IDEOGRAPH-4E2D
αU+03B1GREEK SMALL LETTER ALPHA
😀U+1F600GRINNING FACE
U+2660BLACK SPADE SUIT
U+2192RIGHTWARDS ARROW

码位的范围是 U+0000 到 U+10FFFF,总计 1,114,112 个可能的码位。

2.2 平面(Plane)

Unicode 的码位空间被划分为 17 个平面(Plane),每个平面包含 65,536(2¹⁶)个码位:

平面范围名称主要内容
第 0 平面U+0000 – U+FFFF基本多语言平面(BMP)绝大多数常用字符
第 1 平面U+10000 – U+1FFFF补充多语言平面(SMP)Emoji、古文字、音乐符号等
第 2 平面U+20000 – U+2FFFF补充表意文字平面(SIP)罕用 CJK 汉字
第 3 平面U+30000 – U+3FFFF第三表意文字平面(TIP)更多罕用 CJK 汉字
第 4-13 平面U+40000 – U+DFFFF未分配保留给未来使用
第 14 平面U+E0000 – U+EFFFF补充特殊用途平面(SSP)标签字符、变体选择器
第 15-16 平面U+F0000 – U+10FFFF私用区(PUA)用户自定义字符

重要提示:我们日常使用的绝大多数字符(中文、英文、日文假名、韩文等)都在 BMP(基本多语言平面)中。但 Emoji 和一些罕用汉字位于补充平面(第 1、2、3 平面),在编程中需要特别注意它们的编码处理。

2.3 字符与字形

Unicode 定义的是字符(抽象的含义单元),而不是字形(视觉上的呈现形式)。同一个码位在不同字体中可能呈现完全不同的外观。例如,U+82B1(花)在宋体、黑体、楷体中的样式各不相同,但它们都是同一个 Unicode 字符。

2.4 代理对(Surrogate Pair)

BMP 之外的字符(码位大于 U+FFFF)在 UTF-16 编码中使用代理对来表示。代理区占据 U+D800 到 U+DFFF 共 2,048 个码位:

  • 高代理(High Surrogate):U+D800 – U+DBFF(1,024 个)
  • 低代理(Low Surrogate):U+DC00 – U+DFFF(1,024 个)

一对高代理 + 低代理可以编码 1,024 × 1,024 = 1,048,576 个补充字符,加上 BMP 的 65,536 个码位(减去代理区的 2,048),恰好覆盖了 Unicode 全部 1,112,064 个有效码位。

3. UTF 编码方案详解

Unicode 本身只定义了字符与码位的映射关系,而将码位编码为字节序列的具体方案称为 UTF(Unicode Transformation Format)。主要有三种:

3.1 UTF-8

UTF-8 是当今最广泛使用的 Unicode 编码,由 Ken Thompson 和 Rob Pike 于 1992 年设计。它是一种变长编码,每个字符使用 1 到 4 个字节。

编码规则:

码位范围字节数字节模板可表示位数
U+0000 – U+007F1 字节0xxxxxxx7 位
U+0080 – U+07FF2 字节110xxxxx 10xxxxxx11 位
U+0800 – U+FFFF3 字节1110xxxx 10xxxxxx 10xxxxxx16 位
U+10000 – U+10FFFF4 字节11110xxx 10xxxxxx 10xxxxxx 10xxxxxx21 位

编码示例:

以汉字「你」(U+4F60)为例:

  1. 码位 0x4F60 = 二进制 0100 1111 0110 0000
  2. 落在 U+0800 – U+FFFF 范围,使用 3 字节模板
  3. 将二进制位填入模板:1110**0100** 10**111101** 10**100000**
  4. 结果:0xE4 0xBD 0xA0
字符:你
码位:U+4F60
二进制:0100 1111 0110 0000
UTF-8:E4 BD A0(3 字节)

UTF-8 的优势:

  • 完全兼容 ASCII:ASCII 字符在 UTF-8 中仍然使用单字节表示
  • 无字节序问题:不需要 BOM(字节顺序标记)
  • 自同步:可以从任意位置开始识别字符边界
  • 节省空间:对英文文本特别高效
  • 互联网标准:超过 98% 的网页使用 UTF-8

UTF-8 的劣势:

  • ❌ 中文等 CJK 字符需要 3 字节,比 UTF-16 多一个字节
  • ❌ 变长编码导致无法通过索引直接定位第 N 个字符

3.2 UTF-16

UTF-16 是另一种变长编码,每个字符使用 2 或 4 个字节。

编码规则:

码位范围编码方式
U+0000 – U+D7FF 和 U+E000 – U+FFFF(BMP 标量值)直接使用 2 字节表示
U+10000 – U+10FFFF(补充平面)使用代理对(4 字节)

代理对计算方法:

对于码位 U(U > 0xFFFF):

  1. U’ = U - 0x10000(得到 0x00000 到 0xFFFFF 的值)
  2. 高代理 = 0xD800 + (U’ >> 10)
  3. 低代理 = 0xDC00 + (U’ & 0x3FF)

示例:Emoji 😀(U+1F600)的 UTF-16 编码:

  1. U’ = 0x1F600 - 0x10000 = 0xF600
  2. 高代理 = 0xD800 + (0xF600 >> 10) = 0xD800 + 0x3D = 0xD83D
  3. 低代理 = 0xDC00 + (0xF600 & 0x3FF) = 0xDC00 + 0x200 = 0xDE00
  4. 结果:0xD83D 0xDE00

UTF-16 的应用场景:

  • JavaScript 字符串的索引与长度基于 UTF-16 代码单元
  • Java 的 char 类型和 String
  • Windows API(Win32)
  • macOS 的 NSString(Objective-C/Swift)

注意:UTF-16 有字节序问题。UTF-16BE(Big-Endian)和 UTF-16LE(Little-Endian)的字节排列顺序不同。BOM(字节顺序标记,U+FEFF)用于在文件开头标识字节序。

3.3 UTF-32

UTF-32 是最简单的编码方式——每个字符固定使用 4 个字节,直接存储码位值。

字符:A    → 00 00 00 41
字符:你   → 00 00 4F 60
字符:😀  → 00 01 F6 00

UTF-32 的优势:

  • ✅ 定长编码,可以通过索引直接定位第 N 个字符
  • ✅ 编解码逻辑简单

UTF-32 的劣势:

  • ❌ 空间浪费极大(ASCII 字符也要占 4 字节)
  • ❌ 不兼容 ASCII
  • ❌ 实际使用很少

3.4 三种编码的对比

特性UTF-8UTF-16UTF-32
字节数1–42 或 4固定 4
ASCII 兼容✅ 是❌ 否❌ 否
英文效率⭐⭐⭐ 最优⭐⭐⭐ 最差
中文效率⭐⭐(3 字节)⭐⭐⭐(2 字节)⭐(4 字节)
字节序问题有(需 BOM)有(需 BOM)
使用场景文件存储、网络传输内存中的字符串内部处理(少见)
网页使用率~98%~1%极少

4. Unicode 转义表示法

在编程中,Unicode 字符常通过转义序列来表示。不同语言和场景有不同的写法:

4.1 常见转义格式

格式语法示例(「你」U+4F60)使用场景
Unicode 转义(4 位)\uXXXX\u4F60JavaScript, Java, C#, JSON
Unicode 转义(大括号)\u{XXXXX}\u{4F60}JavaScript (ES6+), Swift
Python Unicode 转义\uXXXX\u4F60Python 字符串
Python 长格式\UXXXXXXXX\U00004F60Python(补充平面字符)
HTML 十进制实体&#DDDD;你HTML
HTML 十六进制实体&#xHHHH;你HTML
CSS/URL 编码\HHHH / %XX\4F60 / %E4%BD%A0CSS / URL

4.2 编码示例

// JavaScript 中的 Unicode 转义
const str1 = '\u4F60\u597D';       // "你好"
const str2 = '\u{1F600}';          // "😀"(ES6 大括号语法)
const str3 = String.fromCodePoint(0x4F60);  // "你"

// 获取字符的码位
'你'.codePointAt(0).toString(16);   // "4f60"
'😀'.codePointAt(0).toString(16);  // "1f600"
# Python 中的 Unicode 转义
s1 = '\u4F60\u597D'        # "你好"
s2 = '\U0001F600'          # "😀"
s3 = chr(0x4F60)           # "你"

# 获取字符的码位
hex(ord('你'))              # '0x4f60'
hex(ord('😀'))             # '0x1f600'
<!-- HTML 中的 Unicode 实体 -->
<p>&#x4F60;&#x597D;</p>           <!-- 你好 -->
<p>&#20320;&#22909;</p>           <!-- 你好(十进制) -->
<p>&#x1F600;</p>                   <!-- 😀 -->

你可以使用我们的 Unicode 编码/解码在线工具 来快速进行这些格式之间的转换。

5. Unicode 的特殊区域

5.1 私用区(Private Use Area)

Unicode 预留了部分码位供用户自定义字符:

  • BMP 私用区:U+E000 – U+F8FF(6,400 个码位)
  • 补充私用区 A:U+F0000 – U+FFFFD(65,534 个码位)
  • 补充私用区 B:U+100000 – U+10FFFD(65,534 个码位)

许多自定义字体图标(如 Font Awesome 早期版本)就使用了 BMP 私用区的码位。

5.2 非字符(Noncharacter)

Unicode 永久保留了 66 个码位作为「非字符」,它们永远不会分配给任何字符:

  • 每个平面的最后两个码位(U+FFFE 和 U+FFFF,U+1FFFE 和 U+1FFFF,等等)
  • U+FDD0 – U+FDEF(BMP 中的 32 个)

这些非字符可以在应用内部使用(如作为哨兵值),但不应该在交换数据中出现。

5.3 组合字符与规范化

Unicode 中有些字符可以通过多种方式表示。例如,字母 é 可以是:

  • 预组合形式(NFC):U+00E9(LATIN SMALL LETTER E WITH ACUTE)—— 单个码位
  • 分解形式(NFD):U+0065 U+0301(字母 e + 组合锐音符号)—— 两个码位

这两种形式在视觉上完全相同,但在字节级别是不同的。这就是为什么 Unicode 定义了四种规范化形式

规范化形式全称说明
NFCCanonical Decomposition + Canonical Composition先分解再组合(推荐用于存储和传输)
NFDCanonical Decomposition完全分解
NFKCCompatibility Decomposition + Canonical Composition兼容分解再组合
NFKDCompatibility Decomposition兼容分解
// JavaScript 规范化示例
const e1 = '\u00E9';         // é(预组合)
const e2 = '\u0065\u0301';   // é(分解)

e1 === e2;                    // false(字节不同!)
e1.normalize('NFC') === e2.normalize('NFC');  // true

最佳实践:在比较或搜索 Unicode 字符串时,始终先进行规范化处理(通常使用 NFC)。

6. 编程中的 Unicode 陷阱

6.1 字符串长度的误解

在 JavaScript 和 Java 中,字符串的 .length 属性返回的是 UTF-16 代码单元的数量,而不是字符数量。对于补充平面的字符(如 Emoji),一个字符会被计为 2:

'A'.length;     // 1 ✅
'你'.length;    // 1 ✅
'😀'.length;   // 2 ❌(实际是 1 个字符)

// 正确获取字符数量
[...'😀'].length;                    // 1 ✅
Array.from('😀').length;            // 1 ✅

6.2 字符串截取的风险

如果在代理对的中间截断字符串,会产生无效的 Unicode 序列:

const emoji = '😀Hello';
emoji.slice(0, 1);   // '\uD83D'(无效!只取了高代理)
emoji.slice(0, 2);   // '😀'(正确,包含完整代理对)

// 安全的做法
[...emoji].slice(0, 1).join('');  // '😀'

6.3 正则表达式与 Unicode

JavaScript 默认的正则表达式不能正确处理补充平面字符。使用 u(ES6)或 v(ES2024)标志来启用 Unicode 模式:

// 不使用 u 标志
/^.$/.test('😀');    // false(被当成 2 个代码单元)

// 使用 u 标志
/^.$/u.test('😀');   // true ✅

// Unicode 属性转义(ES2018)
/\p{Script=Han}/u.test('你');    // true(匹配汉字)
/\p{Emoji}/u.test('😀');        // true(匹配 Emoji)

6.4 Emoji 的复杂性

现代 Emoji 可能由多个码位组合而成:

Emoji组成码位数
👨‍👩‍👧‍👦👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦7
🏳️‍🌈🏳 + VS16 + ZWJ + 🌈4
👍🏽👍 + 肤色修饰符2
🇨🇳🇨 + 🇳(区域指示符)2

ZWJ(Zero Width Joiner,零宽连接符,U+200D)是将多个 Emoji 组合在一起的关键。这意味着 '👨‍👩‍👧‍👦'.length 在 JavaScript 中返回 11,而不是 1。

// Emoji 长度陷阱
'👨‍👩‍👧‍👦'.length;           // 11
[...'👨‍👩‍👧‍👦'].length;      // 7(码位数)

// 使用 Intl.Segmenter 获取视觉字符数
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
[...segmenter.segment('👨‍👩‍👧‍👦')].length;  // 1 ✅

7. Unicode 在 Web 开发中的应用

7.1 HTML 中声明编码

<!-- 在 HTML 中声明 UTF-8 编码 -->
<meta charset="UTF-8">

<!-- HTTP 响应头 -->
Content-Type: text/html; charset=utf-8

最佳实践:始终使用 UTF-8 编码,并在 HTML 的 <head> 中尽早声明 <meta charset="UTF-8">

7.2 URL 编码

URL 中的非 ASCII 字符需要进行百分号编码(Percent-Encoding),将每个 UTF-8 字节转换为 %XX 形式:

"你好" 的 URL 编码过程:
你 → UTF-8: E4 BD A0 → %E4%BD%A0
好 → UTF-8: E5 A5 BD → %E5%A5%BD
结果:%E4%BD%A0%E5%A5%BD
encodeURIComponent('你好');    // "%E4%BD%A0%E5%A5%BD"
decodeURIComponent('%E4%BD%A0%E5%A5%BD');  // "你好"

7.3 JSON 中的 Unicode

JSON 规范要求使用 UTF-8 编码。非 ASCII 字符可以直接写入,也可以使用 \uXXXX 转义:

{
  "message": "你好世界",
  "escaped": "\u4F60\u597D\u4E16\u754C",
  "emoji": "😀",
  "emoji_escaped": "\uD83D\uDE00"
}

两种写法在解析后完全等价。

8. Unicode 安全问题

8.1 同形字攻击(Homoglyph Attack)

某些 Unicode 字符在视觉上与其他字符几乎相同,可能被用于钓鱼攻击:

字符码位看起来像
а(西里尔字母)U+0430a(拉丁字母,U+0061)
о(西里尔字母)U+043Eo(拉丁字母,U+006F)
Ⅰ(罗马数字 I)U+2160I(拉丁字母,U+0049)
ℓ(脚本小写 l)U+2113l(拉丁字母,U+006C)

攻击者可以注册看起来与知名网站几乎一模一样的域名(如用西里尔字母的 а 替换拉丁字母的 a),诱导用户访问恶意网站。

8.2 双向文本欺骗(Bidi Attack)

Unicode 支持双向文本(如阿拉伯文从右向左书写)。恶意使用 RLO(Right-to-Left Override,U+202E)等控制字符可以隐藏文件的真实扩展名:

文件名显示为:readme‮fdp.exe
实际文件名:readme\u202Eexe.pdf  →  显示为 readmefdp.exe

8.3 防御建议

  • 对用户输入进行 Unicode 规范化处理
  • 使用 Punycode 检测可疑的国际化域名(IDN)
  • 过滤或警告不可见的 Unicode 控制字符
  • 在代码审查中使用能够显示不可见字符的编辑器

9. 实用 Unicode 速查

9.1 常用 Unicode 区块

区块名称范围包含内容
Basic LatinU+0000 – U+007FASCII 字符
CJK Unified IdeographsU+4E00 – U+9FFF常用汉字(20,992 个)
HiraganaU+3040 – U+309F日文平假名
KatakanaU+30A0 – U+30FF日文片假名
Hangul SyllablesU+AC00 – U+D7AF韩文音节
ArabicU+0600 – U+06FF阿拉伯文
CyrillicU+0400 – U+04FF西里尔文(俄文等)
Emoji & PictographsU+1F600 – U+1F64FEmoji 表情
Mathematical SymbolsU+2200 – U+22FF数学符号
Currency SymbolsU+20A0 – U+20CF货币符号

9.2 常用特殊字符

字符码位名称用途
U+200B零宽空格不可见的换行点
U+200D零宽连接符Emoji 组合
U+200E左到右标记控制文本方向
U+200F右到左标记控制文本方向
U+00A0不换行空格防止在此处换行
U+2014长破折号中文破折号
U+2026省略号省略标记
©U+00A9版权符号版权声明
U+2122商标符号商标标记
°U+00B0度数符号温度、角度

10. 常见问题(FAQ)

Q1: Unicode 和 UTF-8 是什么关系?

Unicode 是字符集标准,定义了字符与码位的映射关系;UTF-8 是一种将 Unicode 码位编码为字节序列的方案。Unicode 是「字典」,UTF-8 是「书写方式」。类似的编码方案还有 UTF-16 和 UTF-32。

Q2: 为什么 UTF-8 成为了互联网的主流编码?

主要原因有三个:① 完全兼容 ASCII,现有英文文本无需修改;② 变长编码对英文文本空间效率最高;③ 没有字节序问题,简化了网络传输。

Q3: JavaScript 中 '😀'.length 为什么是 2?

JavaScript 字符串的索引和 .length 基于 UTF-16 代码单元。Emoji 😀(U+1F600)超出了 BMP 范围(>U+FFFF),需要使用代理对(两个 16 位代码单元)来表示,所以 .length 返回 2。使用 [...'😀'].lengthArray.from('😀').length 可以得到正确的字符数 1。

Q4: 什么是 BOM(字节顺序标记)?

BOM 是 U+FEFF 字符,放在文件开头用于标识编码方式和字节序:

  • UTF-8 BOM:EF BB BF(不推荐使用)
  • UTF-16 BE BOM:FE FF
  • UTF-16 LE BOM:FF FE

UTF-8 文件通常不需要 BOM,因为 UTF-8 没有字节序问题。但 Windows 的记事本等软件可能会添加 UTF-8 BOM,这有时会导致兼容性问题。

Q5: GBK 和 Unicode 有什么区别?

GBK 是中国的国家标准字符集,主要覆盖中文字符。Unicode 是国际标准,覆盖全球所有文字。GBK 中的中文字符在 Unicode 中都有对应的码位,但编码值不同。现代系统推荐使用 UTF-8(Unicode 的一种编码形式)来替代 GBK。

Q6: 如何在代码中正确处理 Unicode?

关键原则:

  1. 内部统一使用 Unicode(如 UTF-8)
  2. 在输入/输出边界明确指定编码
  3. 使用语言提供的 Unicode 感知 API(如 JavaScript 的 codePointAt() 而非 charCodeAt()
  4. 字符串比较前进行规范化(NFC)
  5. 注意处理 Emoji 和补充平面字符

11. 总结

Unicode 是人类信息技术史上最雄心勃勃的标准化工程之一。它将全世界数百种文字、数十万个字符统一在一个编码空间中,让不同语言、不同文化的信息能够在数字世界中自由流通。

理解 Unicode 的核心概念(码位、平面、UTF 编码)和常见陷阱(字符串长度、代理对、规范化),是每个程序员的必备技能。尤其在全球化应用开发中,正确处理 Unicode 是保证用户体验的基础。

想要快速进行 Unicode 编码与解码?试试我们的 Unicode 编码/解码在线工具,支持 Unicode 转义序列、HTML 实体、UTF-8 十六进制等多种格式的双向转换。