深入理解Emoji(一) —— 字符集,字符集编码

96
黎清海
0.3 2018.11.02 17:02* 字数 3542

最近在开发中遇到了点Emoji相关的问题,便去了解了一下Emoji的编码规则,发现其中涉及了许多字符集与字符集编码的知识点,便趁这个机会做一次这方面的总结梳理。本篇内容主要是对字符集和字符集编码的知识整理。

1. 字符集与字符集编码

我们知道,计算机中的所有信息最终都是以二进制的形式存储,所以人机交互中其实伴随着二进制的转换,将我们输入到计算机的字符(信息)转换成计算机能识别的二进制数据,或将二进制数据输出为我们人能识别的字符。那什么是字符呢?在计算机领域,我们把诸如文字、标点符号、图形符号、数字等统称为字符。而由字符组成的集合则成为字符集,字符集由于包含字符的多少与异同而形成了各种不同的字符集。所以,为了让计算机能识别出我们字符集里的字符,就需要制定一套规则,这套规则,就是字符集编码。我们规定字符编码必须完成如下两件事:

  1. 规定一个字符集中的字符由多少个字节表示。
  2. 制定该字符集的字符编码表,即该字符集中每个字符对应的(二进制)值。

2. ASCII 码

上个世纪 60 年代,美国制定了一套字符编码标准,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。

ASCII(American Standard Code for Information Interchange),是一种字符编码标准,它的字符集为英文字符集,它规定字符集中的每个字符均由一个字节表示,指定了字符表编码表,称为 ASCII 码表。它已被国际标准化组织定义为国际标准,称为 ISO646 标准。

ASCII 码一共规定了 128 个字符的编码,比如空格“SPACE”是 32(二进制00100000),大写的字母 A是 65(二进制01000001)等。这 128 个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面 7 位,最前面的 1 位统一规定为 0。这种采用一个字节来编码 128 个字符的 ASCII 码称为标准 ASCII 码或者基础 ASCII 码。

但是,由于标准 ASCII 字符集字符数目有限,在实际应用中往往无法满足要求。为此,国际标准化组织又制定了 ISO 2022 标准,它规定了在保持与 ISO646 兼容的前提下将 ASCII 字符集扩充为 8 位代码的统一方法。 ISO 陆续制定了一批适用于不同地区的扩充 ASCII 字符集,每种扩充 ASCII 字符集分别可以扩充 128 个字符,这些扩充字符的编码均为高位为 1 的 8 位代码(即十进制数 128~255 ),称为扩展 ASCII 码。

但是需要注意,各种扩展 ASCII 码除了编码为 0~127 的字符外,编码为 128~255 的字符并不相同。比如,130 在法语编码中代表了 é,在希伯来语编码中却代表了字母 Gimel (?),在俄语编码中又会代表另一个符号。因此,ASCII 码的问题在于尽管所有人都在 0 - 127 号字符上达成了一致,但对于 128 - 255 号字符上却有很多种不同的解释。与此同时,亚洲语言有更多的字符需要被存储,一个字节已经不够用了。于是,人们开始使用两个字节来存储字符。各种各样的编码方式成了系统开发者的噩梦,因为他们想把软件卖到国外。于是,他们提出了一个“内码表”的概念,可以切换到相应语言的一个内码表,这样才能显示相应语言的字母。在这种情况下,如果使用多语种,那么就需要频繁的在内码表内进行切换。

3. Unicode

所以,人们最终意识到需要一种标准的规范来展示世界上的所有字符,于是,Unicode就应运而生了。Unicode最初代表着一个字符集,后来慢慢演化成为广义的一个标准,定义了一个字符集以及一系列的编码规则,即 Unicode 字符集和 UTF-8、UTF-16、UTF-32 等等编码…

3.1 码点

一个字符集一般可以用一张或多张由多个行和多个列所构成的二维表来表示。

二维表中行与列相交的点,称之为码点(Code Point代码点),也称之为码位(Code position代码位);每个码点分配一个唯一的编号,称之为码点值或码点编号,除开某些特殊区域(比如代理区、专用区)的非字符码点和保留码点,每个码点唯一对应于一个字符。通俗的说,码点就是字符在Unicode中所对应的二进制数

码点值最初用两个字节的十六进制数字表示,比如字母A的Unicode码点值为0041,常写作U+0041,这种形式称为Unicode码点名称。后来随着Unicode字符集的不断增补扩大(比如现在的Unicode字符集至少需要21位才能全部表示),码点值也扩展为用三个字节或以上的十六进制数字表示。

3.2 码元

在计算机存储和网络传输时,码点被映射到一个或多个码元(Code Unit)。码元可理解为字符编码方式CEF(Character Encoding Form)对码点值进行编码处理时作为一个整体来看待的最小基本单元(基本单位)。

3.3 平面

Unicode的编码空间从U+0000到+10FFFF,共有1,112,064个码点,可用来映射字符. 整个编码空间可以划分为17个平面(plane),每个平面包含216(65,536)个码位。17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从00到10,共计17个平面。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes)。基本多语言平面内,从U+D800到U+DFFF之间的码位区段是永久保留不映射到Unicode字符,称为代理码点(Surrogate Code Point)。UTF-16就利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。除代理码点之外的称为Unicode标量值 (Scalar Value)

3.4 编码方式

一个字符的Unicode码点是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode的编码方式有所不同。

Unicode的编码方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)。以汉字“汉”为例,它的 Unicode 码点是 0x6c49,对应的二进制数是 110110001001001,二进制数有 15 位,这也就说明了它至少需要 2 个字节来表示。可以想象,在 Unicode 字典中往后的字符可能就需要 3 个字节或者 4 个字节,甚至更多字节来表示了。这就导致了一些问题,计算机怎么知道你这个 2 个字节表示的是一个字符,而不是分别表示两个字符呢?这里我们可能会想到,那就取个最大的,假如 Unicode 中最大的字符用 4 字节就可以表示了,那么我们就将所有的字符都用 4 个字节来表示,不够的就往前面补 0。这样确实可以解决编码问题,但是却造成了空间的极大浪费,如果是一个英文文档,那文件大小就大出了 3 倍,这显然是无法接受的。于是,为了较好的解决 Unicode 的编码问题, UTF-8UTF-16 两种当前比较流行的编码方式诞生了。当然还有一个 UTF-32 的编码方式,也就是上述那种定长编码,字符统一使用 4 个字节,虽然看似方便,但是却不如另外两种编码方式使用广泛。

  • UTF-8
    UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,是目前互联网上使用最广泛的一种 Unicode 编码方式,它的最大特点就是可变长。它可以使用 1 - 4 个字节表示一个字符,根据字符的不同变换长度。编码规则如下:
    1. 对于单个字节的字符,第一位设为 0,后面的 7 位对应这个字符的 Unicode 码点。因此,对于英文中的 0 - 127 号字符,与 ASCII 码完全相同。这意味着 ASCII 码那个年代的文档用 UTF-8 编码打开完全没有问题。
    2. 对于需要使用 N 个字节来表示的字符(N > 1),第一个字节的前 N 位都设为 1,第 N + 1 位设为0,剩余的 N - 1 个字节的前两位都设位 10,剩下的二进制位则使用这个字符的 Unicode 码点来填充。
Unicode十六进制码点范围 UTF-8 二进制
0000 0000 - 0000 007F 0xxxxxxx
0000 0080 - 0000 07FF 110xxxxx 10xxxxxx
0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

以上面的“汉”为例,通过上面的对照表可以发现,0x0000 6c49位于第三行的范围,那么得出其格式为 1110xxxx 10xxxxxx 10xxxxxx。接着,从“汉”的二进制数最后一位开始,从后向前依次填充对应格式中的 x,多出的 x 用 0 补上。这样,就得到了“汉”的 UTF-8 编码为 11100110 10110001 10001001,转换成十六进制就是 0xE6 0xB7 0x89。

解码的过程也十分简单:如果一个字节的第一位是 0 ,则说明这个字节对应一个字符;如果一个字节的第一位1,那么连续有多少个 1,就表示该字符占用多少个字节。当中间出现不符合规则的字节编码,比如11100110 10110001 00011101,则系统会判定前面的 11100110 10110001是无效字节,从而舍弃掉,直接解析00001001,于是,我们在界面上会看到这一段会出现一个乱码,但后续是正常的.所以UTF-8是一种十分安全有效的编码方式

  • UTF-16
    上面我们说过码点和平面的概念,其中有提到UTF-16是利用代理码点来编码的一种可变长编码方式。UTF-16 编码介于 UTF-32 与 UTF-8 之间,同时结合了定长和变长两种编码方法的特点,字符处理方便且速度快,所以许多编程语言的内部编码都是UTF-16。比如java、js、c#、python。它的编码规则很简单:基本平面的字符占用 2 个字节(U+0000 到 U+FFFF),编码后的值与码点一致。辅助平面的字符占用 4 个字节(U+010000 到 U+10FFFF),编码后码点被映射成两个代理码点。系统可以通过是否是代理码点来判断该字符是用2个或者4个字节表示。
    辅助平面的码点编码规则:

    1. 码点减去0x10000,得到的值的范围为长度20位的0..0xFFFFF。
    2. 高位的10位的值(值的范围为0..0x3FF)被加上0xD800得到第一个码元或称作高位代理码元(High-Surrogate Code Point),范围是0xD800 ~ 0xDBFF
    3. 低位的10位的值(值的范围也是0..0x3FF)被加上0xDC00得到第二个码元或称作低代理编码单元(Low-Surrogate Code Unit),范围是0xDC00..0xDFFF.

    举个例子,我们日常中使用的笑脸Emoji表情“😃”的码点为U+1F603,是一个辅助平面上的码点,则减去0x10000得到0xF603,20位二进制数为0000 1111 0110 0000 0011,高位00 0011 1101十六进制数为0x3D,低位0 0000 0011十六进制数为0x03,分别加上0xD800和0xDC00得到0xD83D 0XDC03,这就是这个表情的UTF-16编码。


    UTF-16编码举例

    这里还会涉及到一个字节序的问题,我会在下篇中详细讲解,这里不做赘叙。

  • UTF-32
    UTF-32 (或 UCS-4)是一种将Unicode字符编码的协定,对每一个Unicode码位使用恰好32位元。因为UTF-32对每个字符都使用4字节,就空间而言,是非常没有效率的。特别地,辅助平面的字符在大部分文件中通常很罕见,以致于它们通常被认为不存在占用空间大小的讨论,使得UTF-32通常会是其它编码的二到四倍。虽然每一个码位使用固定长定的字节看似方便,它并不如其它Unicode编码使用得广泛。

下篇 深入理解Emoji(二) —— 字节序和BOM

计算机基础
Web note ad 1