这是一份从基础到深入的实战手册,目标是让初学者一步步吃透“字符、码点、编码、存储、传输、转换、安全”等全部要点。内容覆盖我们讨论到的每个知识点,不留空缺。
一、从“字符”到“字节”:概念坐标系
- 字符(character):人类看到/理解的符号(如 A、中、😀)。
- 码点(code point):Unicode 给字符分配的编号,用 U+XXXX 表示。例:A 是 U+0041,“中”是 U+4E2D,“😀”是 U+1F600。
- 码元(code unit):某种 Unicode 变体编码的最小存储单元。
- UTF-8 的码元是 8 位字节。
- UTF-16 的码元是 16 位(两个字节)。
- UTF-32 的码元是 32 位(四个字节)。
- 字节序列(byte sequence):真正写入磁盘/网络的数据,是编码后的结果。
- 字形簇(grapheme cluster):用户感知的“一个字符”。可能由多个码点组成(如“国”+ 变体选择符、emoji 组合、含肤色/性别/ZWJ 的序列)。
核心区分:
- Unicode 是“字符-码点”的字典与语义标准。
- UTF-8/16/32 是把码点编码为字节的方案。
- GBK/Big5 等是独立于 Unicode 的旧时代编码体系。
二、U+ 表示法与最大码点
- “U+”是惯用前缀:
- U 代表 Unicode。
- 没有数学含义,仅是固定前缀。
- 示例:U+0041、U+4E2D、U+1F600。
- 合法码点范围:U+0000–U+10FFFF。
- U+10FFFF 是最大合法码点。不会超过此值。
- 范围内存在保留区与非字符(如 U+FDD0–U+FDEF、每个平面的最后两个非字符 U+FFFE/U+FFFF 等),以及 UTF-16 的代理项区 U+D800–U+DFFF 不是字符。
三、Unicode 的 17 个平面(Plane)
每个平面 65,536 个码点,共 17 个(0–16),总范围 U+0000–U+10FFFF。
平面号 | 名称 | 范围 | 主要内容 | 备注 |
---|---|---|---|---|
0 | 基本多文种平面 BMP | U+0000–U+FFFF | 现代语言绝大多数常用字符、常见符号 | 含代理项区 U+D800–U+DFFF(非字符) |
1 | 多文种补充平面 SMP | U+10000–U+1FFFF | 历史文字、符号、乐谱、部分 emoji | 大量非 BMP 字符 |
2 | 表意文字补充平面 SIP | U+20000–U+2FFFF | CJK 扩展汉字(扩展 B 等) | 汉字扩展 |
3 | 表意文字第三平面 TIP | U+30000–U+3FFFF | 更多 CJK 扩展 | 使用较少 |
4–13 | 保留 | U+40000–U+DFFFF | 预留未来分配 | 当前基本未分配 |
14 | 特别用途补充平面 SSP | U+E0000–U+EFFFF | 标签、变体选择符等 | 特殊用途 |
15 | 私用区 A PUA-A | U+F0000–U+FFFFF | 应用自定义 | 不在标准中定义含义 |
16 | 私用区 B PUA-B | U+100000–U+10FFFF | 应用自定义 | 不在标准中定义含义 |
要点:
- BMP 覆盖“日常需求”为主,但 emoji、更多汉字、历史文字常在辅助平面(1–16)。
- 许多平面暂未分配,将来可能逐步填充。
四、UTF-8、UTF-16、UTF-32:变长与定长
UTF-8(网络事实标准)
- 变长 1–4 字节;ASCII 兼容(U+0000–U+007F 用 1 字节)。
- 字节模式:
字节数 | 码点范围 | 模式 |
---|---|---|
1 | U+0000–U+007F | 0xxxxxxx |
2 | U+0080–U+07FF | 110xxxxx 10xxxxxx |
3 | U+0800–U+FFFF(排除 U+D800–U+DFFF) | 1110xxxx 10xxxxxx 10xxxxxx |
4 | U+10000–U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
- 设计优点:
- 自同步:10xxxxxx 一定是续字节;首字节 1 的计数标识总长度。
- ASCII 原样不变,兼容历史系统。
- 无字节序问题(以字节为单位)。
- 禁止过长形式和代理码点,唯一性与安全性更好。
UTF-16(广泛用于 Windows、Java/C#/JS 内部表示)
- 变长:1 个或 2 个 16 位码元(2 或 4 字节)。
- BMP 内(除代理区)用 1 个码元;BMP 外(U+10000–U+10FFFF)用代理对(2 个码元)。
- 代理对计算:
- 码点 − 0x10000 = 20 位值 N。
- 高 10 位放入高位代理:0xD800–0xDBFF。
- 低 10 位放入低位代理:0xDC00–0xDFFF。
- 字节序:
- UTF-16LE/UTF-16BE;文件/流开头可用 BOM 指示(U+FEFF 的编码)。
UTF-32(简单但占空间)
- 定长:每码点固定 4 字节。
- 处理简单、随机索引方便;空间效率低,少用于传输/存储,多用于内部中间表示或特定环境。
五、传统编码与 Unicode 的关系:GBK、Big5 等
- Big5:繁体中文为主,双字节为主,非 Unicode。
- GBK:大陆常用,兼容 GB2312,双字节为主,非 Unicode。
- 它们与 UTF-8/16 的关系:平行体系。非“子集/超集”,需“转码”互通。
- 转码流程:源字节 → 以源编码解码为字符(码点) → 以目标 UTF 编码为字节。
- 风险:
- 字符集不一致导致缺字或有争议映射,用替代字符(U+FFFD)或私有映射兜底。
- 多语言混排困难、跨平台易乱码。现代系统统一推 UTF-8/UTF-16。
六、标准化、合成与变体:真实世界“一个字符”有多复杂
- 规范化(Normalization):
- NFD:规范分解(分解为基底 + 组合音标)。
- NFC:先分解再规范组合(常用默认)。
- NFKD/NFKC:兼容分解(会把兼容字符分解,如全角等),NFKC 再组合(用于比对/安全)。
- 变体选择符:U+FE0E(文本呈现)、U+FE0F(emoji 呈现)。
- 零宽连接符(ZWJ, U+200D):把多个码点连接出一个组合字形(如家庭、职业性别变体)。
- 肤色修改符:U+1F3FB–U+1F3FF。
- 非字符与控制字符:排版、双向文本控制、不可见字符可能影响渲染/安全。
实践建议:
- 用户交互文本尽量以 NFC 保存与比对。
- 处理 emoji 和复杂文本时,以“字形簇”为单位,而非码元或码点。
七、语言与运行时中的字符串差异
语言/平台 | 内部存储 | length 语义 | 索引/遍历默认单位 | 备注 |
---|---|---|---|---|
JavaScript | UTF-16 | 码元数 | 码元 | for...of 按码点迭代;包含代理对 pitfalls |
Java | UTF-16 | 码元数 | 码元 | 有 codePoint API |
C#/.NET | UTF-16 | 码元数 | 码元 | Rune/Enumerator 支持 |
Python 3 | 动态(UCS-1/2/4) | 码点数 | 码点 | 大多按码点运算 |
Go | 字符串为只读字节(UTF-8 约定) | 字节数 | 字节 | 需用 range/utf8 包按 rune 遍历 |
Rust | String 为 UTF-8 验证字节 | 字节数 | 字节 | chars() 按 Unicode 标量值迭代 |
要点:
- length 往往不是“人眼字符个数”。JS/Java/C# 的 length 是码元数;Python 是码点数;Go/Rust 是字节数。
- 用户界面相关操作(截断、计数、游标移动、退格)要按字形簇处理。
八、编码检测、标识与转换
- 显式声明优先:
- HTTP 头/HTML meta:Content-Type/charset=utf-8。
- 文件格式/协议字段声明编码。
- 数据库连接/列字符集设置(MySQL 用 utf8mb4)。
- BOM(字节序标记):
- UTF-8 BOM:EF BB BF(可有可无,历史兼容性问题多,谨慎)。
- UTF-16LE:FF FE;UTF-16BE:FE FF。
- 启发式检测(无声明时):
- 验证 UTF-8 序列合法性(拒绝过长形式、非法代理、孤立续字节)。
- 统计字节分布与特征(GBK/Big5/Shift-JIS 各有模式)。
- 检测 BOM。
- 容易误判,仅用于辅助。
- 转换实践:
- 解码为 Unicode 内部表示 → 再编码为目标字节。
- 不可映射字符:替换(U+FFFD)、跳过、或失败。
- 保持/去除 BOM:依据目标环境要求。
九、Web、文件、数据库与工具链实务
- Web/HTML:
- <meta charset="UTF-8"> 与 HTTP 头一致。
- 服务器、模板、源代码文件统一 UTF-8。
- JavaScript:
- 用 TextEncoder/TextDecoder 做显式转码。
- 遍历/计数用 [...str] 或 for...of 获取码点;处理用户界面文本用 Intl.Segmenter 或第三方库按字形簇。
- 数据库:
- MySQL/MariaDB:utf8mb4(不要用历史的 utf8),排序规则用 utf8mb4_0900_ai_ci 或语言合适的 collations。
- PostgreSQL:UTF8。
- SQL Server:NVARCHAR(UTF-16)。
- 文件处理:
- 打开/保存显式指定编码。
- 大量旧资料批量转 UTF-8 时,先批量探测,再分批验证,记录不可映射项。
- 源代码与编译链:
- 源文件用 UTF-8 无 BOM,避免工具误判。
- 日志、配置、接口契约统一 UTF-8;边界处做校验。
十、安全与健壮性
- 严格验证 UTF-8 合法性(拒绝过长序列、孤立续字节、代理码点)。
- 输入统一正规化(常用 NFC/NFKC),防同形异义/混淆。
- 过滤/匹配前先解码→正规化→白名单匹配。
- 小心不可见字符、双向控制字符(RTL/LTR embedding/override),对源代码/标识符/域名显示做安全策略(如剔除或可视化标记)。
- 避免以“字符长度”等价于“显示宽度”;等宽终端需 East Asian Width/emoji 宽度处理。
十一、UTF-8 为何不是“最多 3 字节”?
常见误解是“Unicode 111 万个码点,3 字节(2^24)已经够了,为何 UTF-8 要 4 字节?”原因:
- UTF-8 的字节前缀模式要满足:
- ASCII 原样保留。
- 自同步、前缀唯一、错误定位简单。
- 保持排序关系与分段查找效率。
- 设计约束下,覆盖到 U+10FFFF 需 4 字节。不是纯粹“容量最小化”问题。
十二、代理项区与“非法码点”
- U+D800–U+DFFF 是 UTF-16 代理项区:保留给代理对使用,单独出现不代表字符。
- UTF-8/UTF-32 不应该编码代理项区值;解码遇到应报错或替换。
- Unicode 还定义了“非字符”(如 U+FDD0–U+FDEF,每个平面 U+FFFE/U+FFFF 等):不用于交换的内部保留,可在内部使用但不应出现在公开文本交换中。
十三、与旧编码打交道:策略清单
- 确定源编码(元数据优先,不能猜就询问/业务约定)。
- 小心“看起来对但其实错”的误判(尤其 GBK vs UTF-8)。
- 转码流水线要“字节→字符→字节”,不要“字节→字节”替换。
- 显示/搜索时做正规化;记录不可映射项,必要时保留原始字节作为审计字段。
- 渐进迁移:接口、存储、日志先统一到 UTF-8;保留少量边缘输入通道的探测与兜底。
十四、工程细节与性能
- JS/V8 内部字符串形式(了解内存/性能特征):
- OneByte/TwoByte、ConsString、SlicedString、ExternalString 等。
- 大量拼接导致 Cons 链与扁平化成本;建议用数组 join 或 builder。
- 切片可零拷贝,但过多切片会牵连大对象存活,注意内存。
- UTF-8 与 UTF-16 体积对比:
- 拉丁文本:UTF-8 更省空间。
- 中文日文韩文:UTF-8 常为 3 字节/字符,UTF-16 常 2 字节/字符,可能更省。
- Emoji/辅助平面字符:UTF-8 4 字节;UTF-16 4 字节(代理对),接近。
- 索引成本:
- UTF-8/变长:随机按“第 N 个字符”寻址需遍历或辅助索引。
- UTF-16:按码元 O(1),按码点/字形簇仍需遍历。
- UTF-32:按码点 O(1),但字形簇仍复杂。
十五、常用“对/错”用法对照
需求 | 错误做法 | 正确做法 |
---|---|---|
统计“字符数” | 直接用 JS length | [...str].length 或使用按字形簇的分段器 |
截断可视文本 | 按字节/码元裁剪 | 按字形簇边界裁剪,保留完整 emoji/组合 |
存储文本 | 混用 GBK/UTF-8 | 全部 UTF-8(或明确 UTF-16),统一声明 |
读取文件 | 不指明编码“随缘” | 明确 charset;无法确定就检测+回退策略 |
过滤输入 | 直接黑名单替换 | 解码→正规化→白名单验证 |
处理 UTF-8 | 容忍过长序列 | 严格拒绝过长/非法序列 |
十六、速查:关键数值与区段
- 合法码点:U+0000–U+10FFFF。
- 代理项区(非字符):U+D800–U+DFFF。
- 非字符样例:U+FDD0–U+FDEF;每平面 U+FFFE/U+FFFF。
- UTF-8 BOM:EF BB BF;UTF-16LE:FF FE;UTF-16BE:FE FF。
- Emoji 呈现变体:U+FE0E(文本)、U+FE0F(emoji)。
- ZWJ:U+200D。
十七、自问自答:核心问题回顾
Q: “U+10FFFF 是什么?是不是最大码点?”
A: U+10FFFF 是十六进制码点 0x10FFFF 的表示,是 Unicode 的最大合法码点。Unicode 合法范围是 U+0000–U+10FFFF,不会超过这个上限。
Q: “U+ 里的 U 和 + 各是什么意思?”
A: U 表示 Unicode;+ 没有数学意义,是固定写法前缀,后接十六进制码点。
Q: 为什么说有 17 个平面?都是什么?
A: 码点空间按每 65,536 个划成 17 个平面:Plane 0 是 BMP,Plane 1–16 是辅助平面;SMP、SIP/TIP、SSP、PUA-A/B 等用途明确,4–13 目前大多保留。范围总体 U+0000–U+10FFFF。
Q: UTF-8 和 UTF-16 都是变长,怎么判定长度?
A: UTF-8 用首字节前缀位型判定总长度,续字节以 10 开头;UTF-16 用是否命中代理对判定(BMP 内一码元;辅助平面用两个码元)。
Q: GBK、Big5 和 UTF-8 是什么关系?
A: 前两者是独立于 Unicode 的旧编码,字符集不同;UTF-8 是 Unicode 的一种编码。相互转换需“先解码为字符,再编码为目标”。不是子集/超集关系。
Q: 为什么 UTF-8 要用到 4 字节,三字节不够吗?
A: UTF-8 的前缀与自同步设计约束下,覆盖到 U+10FFFF 需要 4 字节。设计目标不仅是容量,还有兼容性、同步、排序与安全。
Q: UTF-16 的代理对到底怎么来的?
A: 码点减 0x10000 得到 20 位;高 10 位映射到 0xD800–0xDBFF,低 10 位映射到 0xDC00–0xDFFF。解码时反向合并再加回 0x10000。
Q: BMP 里也有“不能用”的码点吗?
A: 有。U+D800–U+DFFF 为代理项区,非字符;还有部分非字符与保留位,不应在互操作文本中出现。
Q: 实际工程中如何避免乱码?
A: 全链路统一编码(推荐 UTF-8),显式声明 charset;读取时指定编码;对未知来源进行检测/验证;避免 BOM/无 BOM 混用;数据库/HTTP/源文件一致。
Q: 如何按“人眼字符”遍历或截断?
A: 使用按字形簇的分段(如 ICU、Intl.Segmenter 或专业库)。不要按字节/码元/码点直接截断,以免破坏组合或 emoji 序列。
Q: 安全上要特别注意什么?
A: 严格验证 UTF-8,拒绝过长序列与代理码点;输入正规化(NFC/NFKC);警惕不可见控制字符与双向控制符;用白名单策略。