字符映射(CM, Character Map)就是把抽象字符库里的字符映射到字节序列。所以,一个简单的字符映射就隐含了编码字符集、编码字符形式和编码字符方案,从字符→代码点→代码单元序列→字节序列。一个复合字符映射包含一个复合字符编码方案和一个以上的编码字符形式和编码字符集,此时的抽象字符库就是所涉及的编码字符集所覆盖的字符库的集合。
Unicode 技术报告 # 22: Character Mapping Markup Language (CharMapML) 定义了字符映射的XML规范,还包含了对字符集之间映射问题的详细讨论。
Unicode 及其并行标准 ISO/IEC 10646 通用字符集(Universal Character Set, UCS)共同构成了现代统一的字符编码。它们不是将字符直接映射到八位字节,而是分别定义了哪些字符可用、字符对应的自然数(代码点)、这些数字如何编码成一系列固定尺寸的自然数(代码单元),以及这些代码单元如何被编码成八位字节流。之所以这么分解,是为了建立一个可以使用多种方式进行编码的通用字符集。
本文将重点介绍 Unicode 字符编码模型,旨在帮助大家理解现代字符编码的整个过程。
字符编码模型
Unicode 字符编码模型,可以概况为四个层级:
图一. Unicode 字符编码模型(中英版)
此外,还有两个非常有用的概念:
抽象字符库
字符库就是从字符集里取出的要进行编码的字符集合(一般是无序的),“抽象”一词意味着这些字符是按某种约定定义的。在很多情况下,字符库都是由熟悉的字母表(如26个字母/元素周期表/声母表韵母表/日语音节)或者符号集(如乐谱)组成的。
字符库有两种类型:固定的和开放的。在大多数字符编码中,库都是固定的(一旦确定就永远不会改变)且通常很小。向给定字符库添加新字符通常是创建一个新库(新库会有自己的目录号)构成一个新对象。
eg1.固定字符库
eg2.开放字符库
兼容性字符
由于历史原因,抽象字符库可能包括许多看起来不适当的字符,比如连字字形/上下文形式字形/宽度不同的字形/字符序列/装饰字形(如带圆圈的数字)。所以,字符和代码点之间不一定存在一对一的关系。
(字形可能需要根据周围的字形而
改变其形状/位置/宽度)
编码字符集
编码字符集是从抽象字符库到非负整数的映射,整数的范围可以不连续。非负整数即代码点(code point),分配到代码点的字符就是一个编码字符(an encoded character)。
编码字符集是 ISO 和字符编码委员会生成的基本对象。它们将字符库和代码点相关联,这样就可以用数字明确地指代特定字符了。
eg.编码字符集
代码空间
代码点是对“编码字符集”而言的,是编码字符集或者代码空间的值。代码空间就是代码点的整数范围,它通常和字符编码形式密切相关,因为字符到代码点的映射是在考虑特定编码形式(代码点→字节)的情况下完成的。
代码空间也可以有复杂的结构,这取决于整数范围是否连续,或者是否不允许使用特定范围的值。而大多数的“复杂”情况都源于字符编码形式的考虑,比如字符编码形式会限制字节里的控制位不能有值,这就导致了代码空间是多个不连续的整数范围。
eg.代码空间(16进制表示)
字符编码形式
字符编码形式是把代码点映射到代码单元序列。代码单元是一个占据特定二进制宽度的整数(比如 8-bit byte),代码单元大小是特定编码的“二进制宽度”的位度量(比如 8 bits)。
代码单元
eg.代码单元
bits 可以表示的整数:
所以,不同代码单元能表示的数据范围(16进制表示)是:
代码单元个数
字符编码形式定义了编码的一个基本方面:每个字符需要多少个代码单元(这对于国际化软件很重要)。以前,这相当于定义每个字符需要多少字节(bytes),但随着 Unicode 和 10646 为 UCS-2, UTF-16, UCS-4 和 UTF-32 引入更宽的代码单元,这就被概括成了两条信息:代码单元大小、每个字符需要的代码单元个数。UCS-2 编码形式是固定宽度的,它和 ISO/IEC 10646 相关,只能表示BMP(基本多语言平面)里的字符。相比之下,UTF-16 可以使用一个或两个代码单元,并且能覆盖 Unicode 的整个代码空间。
UTF-8 可以很好地描述“每个字符需要的代码单元个数”。在 UTF-8 中,用于表示字符的基本代码单元是 8 bits wide(即一个字节),不同字节数就可表示不同范围的字符。
E000..FFFF
固定/可变宽度
编码形式定义了代码单元和每个字符需要的代码单元个数(即代码单元序列的长度)。若序列的长度相同,则字符编码形式称为固定宽度;若序列的长度不同,则称为可变宽度。编码形式有多种类型,其中一些是 Unicode/10646 独有的。
eg1.固定宽度的编码形式
在10646中是 1\~6个 8-bit代码单元
编码字符集→字符编码形式
在许多情况下,给定的编码字符集只有一种字符编码形式。所以,有时就不显式提编码字符集了,而是直接说字符编码形式,大约就暗含了字符→代码点→代码单元序列。
Unicode 5.0
字符编码方案
字符编码方案是一个可逆变换,它把代码单元序列映射到字节序列。可能是以下三种方式之一:
比如 Unicode 标准的七种字符编码方案,分别是:
CES
可选的字节顺序标记+简单CES组成
跨平台
字符编码方案和跨平台持久数据有关(数据的代码单元比字节更宽),其中可能需要字节交换才能把数据放入用于特定平台的字节里。特别是:
字节顺序
在把多字节机器整数(即多字节的代码单元)映射到存储位置的处理上,不同处理器的做法有所不同。LE(Little Endian,小端)是将最低字节放在低地址,即从最低字节开始。而 BE(Big Endian,大端)是从最高有效字节开始。
这种差异在处理内存中的代码单元时无关紧要,但当使用特定的字符编码方案将代码单元序列化为字节序列时,字节顺序就变得很重要了。在读取数据流时,有两种类型的字节顺序:和处理器读数据的字节顺序相同或者相反。前者不需要什么特殊操作,后者需要在处理数据前对字节进行反转。
对于数据流的外部指定,有三种类型的字节顺序:大端(BE)、小端(LE)和默认/内部标记。在数据流的头部,字节顺序标记(byte order mark)的存在可用于明确地表示代码单元的字节顺序。
编码形式 vs 编码方案
不要混淆字符编码形式和字符编码方案:
一些 Unicode 编码方案和 Unicode 编码形式的名称一样,比如 UTF-8, UTF-16 和 UTF-32。当不加限定地使用时,这些术语的含义是不明确的。这种歧义对于 UTF-8 来说通常是无害的,因为 UTF-8 编码方案是从为 UTF-8 编码形式而定义的字节序列中派生出来的。但是,对于 UTF-16 和 UTF-32 来说二义性是有问题的。
作为编码形式,UTF-16 和 UTF-32 指的是代码单元,即是通过 16-bit 或 32-bit 的数据类型从内存中访问的,但是并没有关联字节方向。作为编码方案,UTF-16 和 UTF-32 指的是序列化字节,例如流数据或文件里的序列化字节,它们可能具有某种字节方向。
所以尽量使用完整术语,诸如 UTF-16 编码形式(UTF-16 CEF)、UTF-16 编码方案(UTF-16 CES)。
字符映射
字符映射(CM, Character Map)就是把抽象字符库里的字符映射到字节序列。所以,一个简单的字符映射就隐含了编码字符集、编码字符形式和编码字符方案,从字符→代码点→代码单元序列→字节序列。一个复合字符映射包含一个复合字符编码方案和一个以上的编码字符形式和编码字符集,此时的抽象字符库就是所涉及的编码字符集所覆盖的字符库的集合。
Unicode 技术报告 # 22: Character Mapping Markup Language (CharMapML) 定义了字符映射的XML规范,还包含了对字符集之间映射问题的详细讨论。
字符映射是从 IAB 架构中获取 IANA charset 标识符的实体。从 IANA charset 的角度来看,重要的是通过 charset 将编码字符序列明确映射到字节序列上。在所有情况下都必须指定 charset,就像在 Internet 协议中一样,其中文本内容被视为有序的字节序列,且文本内容必须可以从该字节序列中重新构造。
在 IBM CDRA 架构中,字符映射是获取编码字符集标识符(CCSID)值的实体。字符映射也称为 charset, character set, code page(广义上)或 CHARMAP.
在许多情况下,字符映射的名字和字符编码方案的相同,例如 UTF-16BE。
总结
本文详细介绍了 Unicode 字符编码的四层模型,如下图:
代码单元个数
字节顺序
主要参考
https://unicode.org/reports/tr17/