字符编码全书:从零到精通

这是一份从基础到深入的实战手册,目标是让初学者一步步吃透“字符、码点、编码、存储、传输、转换、安全”等全部要点。内容覆盖我们讨论到的每个知识点,不留空缺。


一、从“字符”到“字节”:概念坐标系

  • 字符(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);警惕不可见控制字符与双向控制符;用白名单策略。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容