Unicode字符集与UTF编码规则
2024-04-28 15:58:47 阿炯

先说ASCII

在计算机种中,1 字节对应 8 位二进制数,而每位二进制数有 0、1 两种状态,因此 1 字节可以组合出 256 种状态。如果这 256 中状态每一个都对应一个符号,就能通过 1 字节的数据表示 256 个字符。美国于是就制定了一套编码规则(其实就是个字典),描述英语中的字符和这 8 位二进制数的对应关系,这被称为 ASCII 码。其一共定义了 128 个字符,例如大写的字母 A 是 65(这是十进制数,对应二进制是0100 0001)。这 128 个字符只使用了 8 位二进制数中的后面 7 位,最前面的一位统一规定为 0。用七位二进制表示(0x00 - 0x7F),这些字符组成的集合就叫做 ASCII 字符集;故(0x80 - 0xFF)则代表了非ASCII字符集(non-ASCII characters)。

随着计算机的普及,在不同的地区和国家又出现了很多字符编码,比如:大陆的 GB2312、港台的 BIG5、日本的 Shift JIS等等。字符集(Character Set)是多个字符与字符编码组成的系统,由于历史的原因,曾经发展出多种字符集,例如:


由于字符编码不同,计算机在不同国家之间的交流变得很困难,经常会出现乱码的问题,比如:对于同一个二进制数据,不同的编码会解析出不同的字符;当互联网迅猛发展,地域限制打破之后,人们迫切的希望有一种统一的规则,对所有国家和地区的字符进行编码,于是 Unicode 就出现了。

Unicode全称为Unicode标准(The Unicode Standard),其官方机构Unicode联盟所用的中文名称为统一码,又译作万国码、统一字符码、统一字符编码,是信息技术领域的业界标准,其整理、编码了世界上大部分的文字系统,使得电脑能以通用划一的字符集来处理和显示文字,不但减轻在不同编码系统间切换和转换的困扰,更提供了一种跨平台的乱码问题解决方案。由非营利机构Unicode联盟(Unicode Consortium)负责维护,该机构致力让Unicode标准取代既有的字符编码方案,因为既有方案编码空间有限,亦不适用于多语环境。


Unicode伴随着通用字符集ISO/IEC 10646的标准而发展,同时也以书本的形式对外发表。Unicode至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为2022年9月公布的15.0,已经收录超过14万个字符(第十万个字符【用于马拉雅拉姆语】在2005年获采纳)。Unicode标准不仅仅只是为文字指定代码。除了涵盖视觉上的字形、编码方法、标准的字符编码资料外,联盟官方出版品还包含了关于各书写系统的细节及呈现方式,如规范化的准则、拆分、测序、绘制、双向文本显示、书写方向、字符特性(如大小写字母)等等。此外还提供参考资料和视觉图像,以帮助开发者和设计师正确应用标准。

Unicode备受认可,为ISO纳入国际标准,成为通用字符集,即 ISO/IEC 10646。Unicode兼容ISO/IEC 10646,能完整对应各个版本标准。其广泛应用于电脑软件的国际化与本地化过程。很多新科技,如可扩展置标语言(Extensible Markup Language,简称:XML)、编程语言以及现代操作系统,都采用Unicode来编码。Unicode最普遍的编码格式是和ASCII兼容的UTF-8,以及和UCS-2兼容的UTF-16。

Unicode字符集的编码范围是 0x0000 - 0x10FFFF,可以容纳一百多万个字符,每个字符都有一个独一无二的编码,也即每个字符都有一个二进制数值和它对应,这里的二进制数值也叫码点。

起源与发展

Unicode为解决传统字符编码方案的局限而产生,例如ISO 8859-1所定义的字符虽然在不同的国家中广泛地使用,可是在不同国家间却经常出现不兼容的情况。很多传统的编码方式都有共同的问题,即容许电脑处理双语环境(通常使用拉丁字母以及其本地语言),但却无法同时支持多语言环境(指可同时处理多种语言混合的情况)。

Unicode编码包含了不同写法的字,如“ɑ/a”、“強/强”、“戶/户/戸”。然而在汉字方面引起了一字多形的认定争议,详见中日韩统一表意文字。

在文字处理方面,统一码为每一个字符而非字形定义唯一的代码(即一个整数)。换句话说,统一码以一种抽象的方式(即数字)来处理字符,并将视觉上的演绎工作(例如字体大小、外观形状、字体形态、文体等)留给其他软件来处理,例如网页浏览器或是文字处理器。

目前几乎所有电脑系统都支持基本拉丁字母,并各自支持不同的其他编码方式。Unicode为了和它们相互兼容,其首256个字符保留给ISO 8859-1所定义的字符,使既有的西欧语系文字的转换不需特别考量;并且把大量相同的字符重复编到不同的字符码中去,使得旧有纷杂的编码方式得以和Unicode编码间互相直接转换,而不会丢失任何信息。举例来说,全角格式区段包含了主要的拉丁字母的全角格式,在中文、日文、以及韩文字形当中,这些字符以全角的方式来呈现,而不以常见的半角形式显示,这对竖排文字和等宽排列文字有重要作用。

在表示一个Unicode的字符时,通常会用“U+”然后紧接着一组十六进制的数字来表示这一个字符。在基本多文种平面里的所有字符,要用四个数字(即2字节,共16位,例如U+4AE0,共支持六万多个字符);在零号平面以外的字符则需要使用五或六个数字。旧版的Unicode标准使用相近的标记方法,但却有些微小差异:在Unicode 3.0里使用“U-”然后紧接着八个数字,而“U+”则必须随后紧接着四个数字。

标准与历程

位于美国加州的Unicode组织允许任何愿意支付会费的公司和个人加入,其成员包含了主要的电脑软硬件厂商,例如Adobe系统、苹果公司、惠普、IBM、微软、施乐等。

20世纪80年代末,组成Unicode组织的商业机构,和国际合作的国际标准化组织在电脑普及和信息国际化的前提下,分别各自成立了Unicode组织和ISO-10646工作小组。他们不久便发现对方的存在,而大家工作目的一致。1991年,Unicode Consortium与ISO/IEC JTC1/SC2同意保持Unicode码表与ISO 10646标准保持兼容并密切协调各自标准进一步的扩展。虽然实际上两者的字集编码相同,但本质上两者确为不同的标准。Unicode 1.1对应于ISO 10646-1:1993,Unicode 3.0对应于ISO 10646-1:2000,Unicode 3.2对应于ISO 10646-2:2001,Unicode 4.0对应于ISO 10646:2003,Unicode 5.0对应于ISO 10646:2003及附录1–3。

Unicode自2.0版本开始保持了向后兼容,即新版本仅增加字符,原有字符不会删除或更名。但从Unicode 14.0起,既有的区段可扩展或缩减(必须在没有字符使用空间的前提下,若已有字符占用空间不可缩减区段),第一个自Unicode 1.1以来扩展的既有区段为阿洪姆文(Ahom)。

统一码联盟在1991年首次发布了The Unicode Standard。Unicode的开发结合了国际标准化组织所制定的ISO/IEC 10646,即通用字符集。Unicode与ISO/IEC 10646在编码的运作原理相同,但The Unicode Standard包含了更详尽的实现信息、涵盖了更细节的主题,诸如比特编码(bitwise encoding)、校对以及呈现等。The Unicode Standard也枚举了诸多的字符特性,例如必须支持两种阅读方向的字符(由左至右或由右至左的文字阅读方向,例如阿拉伯文是由右至左)。Unicode与ISO/IEC 10646两个标准在术语上的使用有些微的不同。

编码和实现

大概来说,Unicode编码系统可分为编码方式和实现方式两个层次。

十大设计原则

《The Unicode Standard Version 6.2 – Core Specification》文档给出了其十大设计原则:
Universality:提供单一、综合的字符集,编码一切现代与大部分历史文献的字符。
Efficiency:易于处理与分析。
Characters, not glyphs:字符,而不是字形。
Semantics:字符要有良好定义的语义。
Plain text:仅限于文本字符。
Logical order:默认内存表示是其逻辑序。
Unification:把不同语言的同一书写系统(scripts)中相同字符统一起来。
Dynamic composition:附加符号可以动态组合。
Stability:已分配的字符与语义不再改变。
Convertibility:Unicode与其他著名字符集可以精确转换。

编码方式

统一码的编码方式与ISO 10646的通用字符集概念相对应。目前实际应用的统一码版本对应于UCS-2,使用16位的编码空间。也就是每个字符占用2个字节。这样理论上一共最多可以表示216(即65536)个字符。基本满足各种语言的使用。实际上当前版本的统一码并未完全使用这16位编码,而是保留了大量空间以作为特殊使用或将来扩展。

上述16位统一码字符构成基本多文种平面。最新(但未实际广泛使用)的统一码版本定义了16个辅助平面,两者合起来至少需要占据21位的编码空间,比3字节略少。但事实上辅助平面字符仍然占用4字节编码空间,与UCS-4保持一致。未来版本会扩充到ISO 10646-1实现级别3,即涵盖UCS-4的所有字符。UCS-4是更大而尚未填充完全的31位字符集,加上恒为0的首位,共需占据32位,即4字节。理论上最多能表示231个字符,完全可以涵盖一切语言所用的符号。

基本多文种平面的字符的编码为U+hhhh,其中每个h代表一个十六进制数字,与UCS-2编码完全相同。而其对应的4字节UCS-4编码后两个字节一致,前两个字节则所有位均为0。

实现方式

Unicode的实现方式不同于编码方式。一个字符的Unicode编码确定。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)。

如果一个仅包含基本7位ASCII字符的Unicode文件,如果每个字符都使用2字节的原Unicode编码传输,其第一字节的8位始终为0。这就造成了比较大的浪费。对于这种情况,可以使用UTF-8编码,这是变长编码,它将基本7位ASCII字符仍用7位编码表示,占用一个字节(首位补0)。而遇到与其他Unicode字符混合的情况,将按一定算法转换,每个字符使用1-3个字节编码,并利用首位为0或1识别。这样对以7位ASCII字符为主的西文文档就大幅节省了编码长度(具体方案参见UTF-8)。类似的,对未来会出现的需要4个字节的辅助平面字符和其他UCS-4扩充字符,2字节编码的UTF-16也需要通过一定的算法转换。

再如,如果直接使用与Unicode编码一致(仅限于BMP字符)的UTF-16编码,由于每个字符占用了两个字节,在麦金塔电脑(Mac)机和个人电脑上,对字节顺序的理解不一致。这时同一字节流可能会解释为不同内容,如某字符为十六进制编码4E59,按两个字节拆分为4E和59,在Mac上读取时是从低字节开始,那么在Mac OS会认为此4E59编码为594E,找到的字符为“奎”,而在Windows上从高字节开始读取,则编码为U+4E59的字符为“乙”。就是说在Windows下以UTF-16编码保存一个字符“乙”,在Mac OS环境下开启会显示成“奎”。此类情况说明UTF-16的编码顺序若不加以人为定义就可能发生混淆,于是在UTF-16编码实现方式中使用了大端序(Big-Endian,简写为UTF-16 BE)、小端序(Little-Endian,简写为UTF-16 LE)的概念,以及可附加的字节顺序记号解决方案,目前在个人电脑上的Windows系统和Linux系统对于UTF-16编码默认使用UTF-16 LE。

此外Unicode的实现方式还包括UTF-7、Punycode、CESU-8、SCSU、UTF-32、GB18030等,这些实现方式有些仅在一定的国家和地区使用,有些则属于未来的规划方式。目前通用的实现方式是UTF-16小端序(LE)、UTF-16大端序(BE)和UTF-8。在微软公司Windows XP附带的记事本(Notepad)中,“另存为”对话框可以选择的四种编码方式除去非Unicode编码的ANSI(对于英文系统即ASCII编码,中文系统则为GB2312或Big5编码)外,其余三种为“Unicode”(对应UTF-16 LE)、“Unicode big endian”(对应UTF-16 BE)和“UTF-8”。

目前辅助平面的工作主要集中在第二和第三平面的中日韩统一表意文字,因此包括GBK、GB18030、Big5等简体中文、繁体中文、日文、韩文以及越南喃字的各种编码与Unicode的协调性受重点关注。考虑到Unicode最终要涵盖所有的字符。从某种意义而言,这些编码方式也可视作Unicode的出现于其之前的既成事实的实现方式,如同ASCII及其扩展Latin-1一样,后两者的字符在16位Unicode编码空间中的编码第一字节各位全为0,第二字节编码与原编码完全一致。但上述东亚语言编码与Unicode编码的对应关系要复杂得多。


字符平面映射

Unicode 将编码空间分成 17 个平面,以 0 到 16 编号。

第 0 平面(或者说基本多文种平面)中的码点,都可以用一个 UTF-16 单位来编码,或者以 UTF-8 来编码的话,会使用一、二或三个字节。而第 1 到 16 平面(或称辅助平面)中的码点,UTF-16 会以代理对的方式来使用,而 UTF-8 则会编码成 4 个字节。

在每个平面中,会先将相关的字符集结为区段的形式。虽然区段可以是任意大小,但会以 16 个码点的倍数,且通常是 128 个码点的倍数。而一份文稿中使用到的区段,可能会散布在多个区段中。

非Unicode环境

在非Unicode环境下,由于不同国家和地区采用的字符集不一致,很可能出现无法正常显示所有字符的情况。微软公司使用了代码页(Codepage)转换表的技术来过渡性地部分解决这一问题,即通过指定的转换表将非Unicode的字符编码转换为同一字符对应的系统内部使用的Unicode编码。可以在“语言与区域设置”中选择一个代码页作为非Unicode编码所采用的默认编码方式,如936为简体中文GB码,950为繁体中文Big5(皆指PC上使用的)。在这种情况下,一些非英语的欧洲语言编写的软件和文档很可能出现乱码。而将代码页设置为相应语言中文处理又会出现问题,这一情况无法避免。只有完全采用统一编码才能彻底解决这些问题,但目前尚无法做到这一点。

代码页技术现在广泛为各种平台所采用。UTF-7的代码页是65000,UTF-8的代码页是65001。

字符集是很多个字符的集合,例如 GBK 是简体中文的字符集,它收录了六千多个常用的简体汉字及一些符号,数字,拼音等字符。

字符编码是字符集的一种实现方式,把字符集中的字符映射为特定的字节或字节序列,它是一种规则。

Unicode 只是字符集,UTF-8、UTF-16、UTF-32 才是真正的字符编码规则。

Unicode 是一个符号集, 它只规定了每个符号的二进制值,但是符号具体如何存储它并没有规定。其字符集的编码范围是 0x0000 - 0x10FFFF,因此需要 1 到 3 个字节来表示。那么对于三个字节的 Unicode字符,计算机怎么知道它表示的是一个字符而不是三个字符呢?

如果所有字符都用三个字节表示,那么对于那些一个字节就能表示的字符来说,有两个字节是无意义的,对于存储来说,这是极大的浪费,假如一个普通的文本,大部分字符都只需一个字节就能表示,现在如果需要三个字节才能表示,文本的大小会大出三倍左右。

因此 Unicode 出现了多种存储方式,常见的有 UTF-8、UTF-16、UTF-32,它们分别用不同的二进制格式来表示 Unicode 字符。

其中的 "UTF" 是 "Unicode Transformation Format" 的缩写,意思是"Unicode 转换格式",后面的数字表明至少使用多少个比特位来存储字符,比如:UTF-8 最少需要8个比特位也就是一个字节来存储,对应的 UTF-16 和 UTF-32 分别需要最少 2 个字节 和 4 个字节来存储。

Unicode 字符集与 ASCII 等字符集相比,在概念上相对复杂一些。小结一下上面的知识,需要从 2 个维度来理解 Unicode 字符集:编码标准 + 编码格式。

1、Unicode 编码标准

关键理解两个概念:码点 + 字符平面映射:
1.1、码点(Code Point):从 0 开始编号,每个字符都分配一个唯一的码点,完整的十六进制格式是 U+[XX]XXXX,具体可表示的范围为 U+0000 ~ U+10FFFF(所需要的空间最大为 3 个字节的空间),例如 U+0011。这个范围可以容纳超过百万个字符,足够容纳目前全世界已创造的字符。



1.2、字符平面(Plane):这么多字符并不是一次性定义完成的,而是采用了分组的方式。每一个组称为一个平面,每个平面能够容纳多个字符。Unicode 一共定义了 17 个平面:
1.2.1、基本多文种平面(Basic Multilingual Plane, BMP):第一个平面,包含最常用的通用字符。当然,基本平面并不是填满的,而是刻意空出一段区域。
1.2.2、辅助平面(Supplementary Plane):剩下的 16 个平面,包含多种语言的字符。

完整的 unicode 码点列表可以参考:unicode.org

2、Unicode 编码格式

Unicode 本身只定义了字符与码点的映射关系,相当于定义了一套标准,而这套标准真正在计算机中落地时,则有多种编码格式。目前常见到的有 3 种编码格式:UTF-8、UTF-16 和 UTF-32。UTF 是英文 Unicode Transformation Format 的缩写,意思是 Unicode 字符转换为某种格式。

别看编码格式五花八门,本质上只是出于空间和时间的权衡,对同一套字符标准使用不同的编码算法而已。如字符 A 的 Unicode 码点和编码如下:
1.图像:A
2.码点:U+0041
3.UTF-8 编码:0X41
4.UTF-16 编码:0X0041
5.UTF-32 编码:0X00000041

当根据 UTF-8、UTF-16 和 UTF-32 的编码规则进行解码后,你将得到什么结果呢?是的,它们的结果都是一样的 —— 0x41。


UTF-8

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用一至四个字节对Unicode字符集中的所有有效编码点进行编码,属于Unicode标准的一部分,最初由肯·汤普逊和罗布·派克提出。由于较小值的编码点一般使用频率较高,直接使用Unicode编码效率低下,大量浪费内存空间。UTF-8就是为了解决向后兼容ASCII码而设计,Unicode中前128个字符,使用与ASCII码相同的二进制值的单个字节进行编码,而且字面与ASCII码的字面一一对应,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字优先采用的编码方式。

UTF-8的设计有以下的多字符组序列的特质:
1.单字节字符的最高有效比特永远为0。
2.多字节序列中的首个字符组的几个最高有效比特决定了序列的长度。最高有效位为110的是2字节序列,而1110的是三字节序列,如此类推。
3.多字节序列中其余的字节中的首两个最高有效比特为10。

UTF-8的这些特质,保证了一个字符的字节序列不会包含在另一个字符的字节序列中。这确保了以字节为基础的部分字符串比对(sub-string match)方法可以适用于在文字中搜索字或词。有些比较旧的可变长度8位编码(如Shift JIS)没有这个特质,故字符串比对的算法变得相当复杂。虽然这增加了UTF-8编码的字符串的信息冗余,但是利多于弊。另外,资料压缩并非Unicode的目的,所以不可混为一谈。即使在发送过程中有部分字节因错误或干扰而完全丢失,还是有可能在下一个字符的起点重新同步,令受损范围受到限制。另一方面,由于其字节序列设计,如果一个疑似为字符串的序列被验证为UTF-8编码,那么我们可以有把握地说它是UTF-8字符串。一段两字节随机序列碰巧为合法的UTF-8而非ASCII的几率为32分1。对于三字节序列的几率为256分1,对更长的序列的几率就更低了。


自2009年以来,UTF-8一直是万维网的最主要的编码形式(对所有,而不仅是Unicode范围内的编码)(并由WHATWG宣布为强制性的“适用于所有事物(for all things)”,截止到2019年11月,在所有网页中,UTF-8编码应用率高达94.3%(其中一些仅是ASCII编码,因为它是UTF-8的子集),而在排名最高的1000个网页中占96%。第二热门的多字节编码方式Shift JIS和GB 2312分别具有0.3%和0.2%的占有率。Internet邮件联盟( Internet Mail Consortium, IMC)建议所有电子邮件程序都能够使用UTF-8展示和创建邮件,W3C建议UTF-8作为XML文件和HTML文件的默认编码方式。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。互联网邮件联盟(IMC)建议所有电子邮件软件都支持UTF-8编码。

1993年1月25-29日的在圣地亚哥举行的USENIX会议首次正式介绍UTF-8。

结构

UTF-8使用一至六个字节为每个字符编码(尽管如此,2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节):
1.128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。

2.带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080至U+07FF)。

3.其他基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode范围由U+0800至U+FFFF)。

4.其他极少使用的Unicode 辅助平面的字符使用四至六字节编码(Unicode范围由U+10000至U+1FFFFF使用四字节,Unicode范围由U+200000至U+3FFFFFF使用五字节,Unicode范围由U+4000000至U+7FFFFFFF使用六字节)。

对上述提及的第四种字符而言,UTF-8使用四至六个字节来编码似乎太耗费资源了。但UTF-8对所有常用的字符都可以用三个字节表示,而且它的另一种选择,UTF-16编码,对前述的第四种字符同样需要四个字节来编码,所以要决定UTF-8或UTF-16哪种编码比较有效率,还要视所使用的字符的分布范围而定。不过,如果使用一些传统的压缩系统,比如DEFLATE,则这些不同编码系统间的的差异就变得微不足道了。若顾及传统压缩算法在压缩较短文字上的效果不大,可以考虑使用Unicode标准压缩格式(SCSU)。

UTF-8编码字节含义

1.对于UTF-8编码中的任意字节B,如果B的第一位为0,则B独立的表示一个字符(ASCII码);
2.如果B的第一位为1,第二位为0,则B为一个多字节字符中的一个字节(非ASCII字符);
3.如果B的前两位为1,第三位为0,则B为两个字节表示的字符中的第一个字节;
4.如果B的前三位为1,第四位为0,则B为三个字节表示的字符中的第一个字节;
5.如果B的前四位为1,第五位为0,则B为四个字节表示的字符中的第一个字节;

因此,对UTF-8编码中的任意字节,根据第一位,可判断是否为ASCII字符;根据前二位,可判断该字节是否为一个字符编码的第一个字节;根据前四位(如果前两位均为1),可确定该字节为字符编码的第一个字节,并且可判断对应的字符由几个字节表示;根据前五位(如果前四位为1),可判断编码是否有错误或数据传输过程中是否有错误。

UTF-8的编码方式

UTF-8是UNICODE的一种变长度的编码表达方式《一般UNICODE为双字节(指UCS2)》,它由肯·汤普逊(Ken Thompson)于1992年建立,现在已经标准化为RFC 3629。UTF-8就是以8位为单元对UCS进行编码,而UTF-8不使用大尾序和小尾序的形式,每个使用UTF-8存储的字符,除了第一个字节外,其余字节的头两个比特都是以"10"开始,使文字处理器能够较快地找出每个字符的开始位置。但为了与以前的ASCII码兼容(ASCII为一个字节),因此UTF-8选择了使用可变长度字节来存储Unicode。


在UTF-8+BOM格式文件的开首,很多时都放置一个U+FEFF字符(UTF-8以EF,BB,BF代表),以显示这个文本文件是以UTF-8编码。

UTF-8 的编码规则可简述如下:

对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的, 所以 UTF-8 能兼容 ASCII 编码,这也是互联网普遍采用 UTF-8 的原因之一。

对于 n 字节的符号( n > 1),第一个字节的前 n 位都设为 1,第 n + 1 位设为 0,后面字节的前两位一律设为 10 。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码

下表是Unicode编码对应UTF-8需要的字节数量以及编码格式:
Unicode编码范围(16进制)UTF-8编码方式(二进制)
000000 - 00007F0xxxxxxx ASCII码
000080 - 0007FF110xxxxx 10xxxxxx
000800 - 00FFFF1110xxxx 10xxxxxx 10xxxxxx
01 0000 - 10 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx


表格中第一列是Unicode编码的范围,第二列是对应UTF-8编码方式,其中红色的二进制 "1" 和 "0" 是固定的前缀,字母 x 表示可用编码的二进制位。

根据上面表格,要解析 UTF-8 编码就很简单了,如果一个字节第一位是 0 ,则这个字节就是一个单独的字符,如果第一位是 1 ,则连续有多少个 1 ,就表示当前字符占用多少个字节

下面以 "中" 字 为例来说明 UTF-8 的编码,具体的步骤如下图, 为了便于说明,图中左边加了 1,2,3,4 的步骤编号。


首先查询 "中" 字的 Unicode 码 0x4E2D,转成二进制,总共有 16 个二进制位,具体如上图中步骤1 所示。

通过前面的 Unicode 编码和 UTF-8 编码的表格知道,Unicode 码 0x4E2D 对应 000800 - 00FFFF 的范围,所以, "中" 字的 UTF-8 编码 需要 3 个字节,即格式是 1110xxxx 10xxxxxx 10xxxxxx。

然后从 "中" 字的最后一个二进制位开始,按照从后向前的顺序依次填入格式中的 x 字符,多出的二进制补为 0, 具体如上图步骤2、步骤3所示;于是,就得到了 "中" 的 UTF-8 编码是 11100100 10111000 10101101, 转换成十六进制就是 0xE4B8AD, 具体如上图步骤4 所示。


UTF-8的特性

UTF-8图表说明
最小码位     0000
最大码位     10FFFF
每字节所占位数     8 bits
Byte order     N/A
每个字符最小字节数     1
每个字符最大字节数     4

UCS字符U+0000到U+007F(ASCII)被编码为字节0x00到0x7F(ASCII兼容),这也意味着只包含7位ASCII字符的文件在ASCII和UTF-8两种编码方式下是一样的。

所有>U+007F的UCS字符被编码为一个多个字节的串,每个字节都有标记位集。因此,ASCII字节(0x00-0x7F)不可能作为任何其他字符的一部分。

表示非ASCII字符的多字节串的第一个字节总是在0xC0到0xFD的范围里,并指出这个字符包含多少个字节。多字节串的其余字节都在0x80到0xBF范围里,这使得重新同步非常容易,并使编码无国界,且很少受丢失字节的影响。

可以编入所有可能的231个UCS代码。

UTF-8编码字符理论上可以最多到6个字节长,然而16位BMP字符最多只用到3字节长。

Bigendian UCS-4字节串的排列顺序是预定的。

字节0xFE和0xFF在UTF-8编码中从未用到,同时UTF-8以字节为编码单元,它的字节顺序在所有系统中都是一様的,没有字节序的问题,也因此它实际上并不需要BOM。

与UTF-16或其他Unicode编码相比,对于不支持Unicode和XML的系统,UTF-8更不容易造成问题,其是 1~4 个字节的变长编码,相对来说最节省空间。其核心规则可小结如下:
规则 1: 不同范围的码点值使用不同长度的编码;
规则 2: 字节编码总长度为 1 时前缀为 0、总长度为 2 时前缀为 110、总长度为 3 时前缀为 1110、总长度为 4 时前缀为 11110 ;
规则 3: 除了首个字节,字符编码中其余字节的前缀为 10。

可以看到,这种编码方式是不会存在前缀歧义的,也比较好理解。

UTF-8 编码举例



UTF-16

UTF-16是Unicode字符编码五层次模型的第三层:字符编码表(Character Encoding Form,也称为"storage format")的一种实现方式。即把Unicode字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位,需要1个或者2个16位长的码元来表示,因此这是一个变长表示;此种编码方式比较特殊,它将字符编码成 2 字节或者 4 字节。

UTF是"Unicode/UCS Transformation Format"的首字母缩写,即把Unicode字符转换为某种格式之意。UTF-16正式定义于ISO/IEC 10646-1的附录C,而RFC2781也定义了相似的做法。

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

编码规则小结如下:
1.对于 Unicode 码小于 0x10000 的字符,使用 2 个字节存储,并且是直接存储 Unicode 码,不用进行编码转换;
2.对于 Unicode 码在 0x10000 和 0x10FFFF 之间的字符,使用 4 个字节存储,这 4 个字节分成前后两部分,每个部分各两个字节,其中前面两个字节的前 6 位二进制固定为 110110,后面两个字节的前 6 位二进制固定为 110111,前后部分各剩余 10 位二进制表示符号的 Unicode 码 减去 0x10000 的结果;
3.大于 0x10FFFF 的 Unicode 码无法用 UTF-16 编码。

下表是Unicode编码对应UTF-16编码格式:
Unicode编码范围(16进制)具体Unicode码(二进制)UTF-16编码方式(二进制)字节
0000 0000 - 0000 FFFFxxxxxxxx xxxxxxxxxxxxxxxx xxxxxxxx2
0001 0000 - 0010 FFFFyy yyyyyyyy xx xxxxxxxx110110yy yyyyyyyy 110111xx xxxxxxxx4


表格中第一列是Unicode编码的范围,第二列是 具体Unicode码的二进制 (第二行的第二列表示的是 Unicode 码 减去 0x10000 后的二进制) ,第三列是对应UTF-16编码方式,其中红色的二进制 "1" 和 "0" 是固定的前缀, 字母 x 和 y 表示可用编码的二进制位, 第四列表示编码占用的字节数。前面提到过,"中" 字的 Unicode 码是 4E2D, 它小于 0x10000,根据表格可知,它的 UTF-16 编码占两个字节,并且和 Unicode 码相同,所以 "中" 字的 UTF-16 编码为 4E2D。


UTF-16的编码模式

UTF-16的大尾序和小尾序存储形式都在用。一般来说,以Macintosh制作或存储的文字使用大尾序格式,以Microsoft或Linux制作或存储的文字使用小尾序格式。为了弄清楚UTF-16文件的大小尾序,在UTF-16文件的开首,都会放置一个U+FEFF字符作为Byte Order Mark(UTF-16 LE以 FF FE 代表,UTF-16 BE以 FE FF 代表),以显示这个文本文件是以UTF-16编码,其中U+FEFF字符在UNICODE中代表的意义是 ZERO WIDTH NO-BREAK SPACE,顾名思义,它是个没有宽度也没有断字的空白。

UTF-16可看成是UCS-2的父集。在没有辅助平面字符(surrogate code points)前,UTF-16与UCS-2所指的是同一的意思。但当引入辅助平面字符后,就称为UTF-16了。现在若有软件声称自己支持UCS-2编码,那其实是暗指它不能支持在UTF-16中超过2字节的字集。对于小于0x10000的UCS码,UTF-16编码就等于UCS码。

Windows操作系统内核中的字符表示为UTF-16小尾序,可以正确处理、显示以4字节存储的字符。但是Windows API实际上仅能正确处理UCS-2字符,即仅以2字节存储的,码位小于U+FFFF的Unicode字符。其根源是Microsoft C++语言把 wchar_t 数据类型定义为16比特的unsigned short,这就与一个 wchar_t 型变量对应一个宽字符、可以存储一个Unicode字符的规定相矛盾。相反,Linux平台的GCC编译器规定一个 wchar_t 是4字节长度,可以存储一个UTF-32字符,宁可浪费了很大的存储空间。

Windows 9x系统的API仅支持ANSI字符集,只支持部分的UCS-2转换。1996年发布的Windows NT 4.0的API支持UCS-2。Windows 2000开始,Windows系统API开始支持UTF-16,并支持Surrogate Pair;但许多系统控件比如文本框和label等还不支持surrogate pair表示的字符,会显示成两个字符。Windows 7及更新的系统已经良好地支持了UTF-16,包括Surrogate Pair。

Windows API支持在UTF-16LE(wchar_t类型)与UTF-8(代码页CP_UTF8)之间的转码。

从 Unicode字符表网站找了一个老的南阿拉伯字母,它的 Unicode 码是: 0x10A6F,可以访问此处查看字符的说明,Unicode 码对应的字符如下图所示:


下面以这个 老的南阿拉伯字母的 Unicode 码 0x10A6F 为例来说明 UTF-16 4 字节的编码,具体步骤如下;为了便于说明,图中左边加了 1,2,3,4,5的步骤编号。


首先把 Unicode 码 0x10A6F 转成二进制, 对应上图的步骤 1;

然后把 Unicode 码 0x10A6F 减去 0x10000,结果为 0xA6F 并把这个值转成二进制 00 00000010 10 01101111,对应上图的步骤 2;

然后从二进制 00 00000010 10 01101111 的最后一个二进制为开始,按照从后向前的顺序依次填入格式中的 x 和 y 字符,多出的二进制补为 0,对应上图的步骤 3、步骤 4;

于是就计算出了 Unicode 码 0x10A6F 的 UTF-16 编码是 11011000 00000010 11011110 01101111 , 转换成十六进制就是 0xD802DE6F,对应上图的步骤 5。


若 Unicode 码点最大需要 3 个字节,那么当 UTF-16 使用 2 个字节空间时,岂不是不够用了?

先说 UTF-16 的编码规则:
规则 1:基本平面的码点(编号范围在 U+0000 ~ U+FFFF)使用 2 个字节表示。辅助平面的码点(编号范围在 U+10000 ~ U+10FFFF 的码点)使用 4 个字节表示;

规则 2:16 个辅助平面总共有 个字符,至少需要 20 位的空间才能区分。UTF-16 将这 20 位拆成 2 半:
1.高 10 位映射在 U+D800 ~ U+DBFF,称为高位代理(high surrogate);
2.低 10 位映射在 U+DC00 ~ U+DFFF,称为低位代理(low surrogate)。

好复杂,为什么要这么设计?第一条规则比较好理解,1 个平面有最大的编码是 U+FFFF,需要用 16 位表示,用 2 个字节表示正好。第二条规则就不好理解了,重点说一下。

辅助平面最大的字符是 U+10FFFF,需要使用 21 位表示,用 4 个字节表示就绰绰有余了,例如说低 16 位 放在低 16 位,高 5 位放在高 16 位(不足位补零)。这样不是很简单也很好理解?

不行,因为前缀有歧义。这种方式会导致辅助平面编码的每 2 个字节的取值范围都与基本平面的取值范围重复,因此解码程序在解析一段 UTF-16 编码的字符流时,就无法区分这 2 个字节是属于基本平面字符,还是属于辅助平面字符。


为了解决这个问题,必须实现前缀无歧义编码(PFC 编码,类似的还有哈弗曼编码)。UTF-16 的方案是将用于基本平面字符编码的取值范围与辅助平面字符编码的取值范围错开,使得两者不会出现歧义(冲突)。这么做的前提,就需要在基本平面中提前空出一段区域,这就是上文提到基本平面故意空出一段区域的原因。

如下图所示,在基础平面中,浅灰色的 D8 ~ DF 为 UTF-16 代理区:


—— 图片引用自维基百科

UTF-16 编码举例



UTF-32

UTF-32 是固定长度的编码,始终占用 4 个字节,足以容纳所有的 Unicode 字符,所以直接存储 Unicode 码即可,编解码规则最简单,不需要任何编码转换。虽然浪费了空间,但提高了效率。



UTF-8、UTF-16、UTF-32 之间的关系

前面介绍过,UTF-8、UTF-16、UTF-32 是 Unicode 码表示成不同的二进制格式的编码规则,同样通过这三种编码的二进制表示,也能获得对应的 Unicode 码,有了字符的 Unicode 码,按照上面介绍的 UTF-8、UTF-16、UTF-32 的编码方法就能转换成任一种编码了。

UTF 字节序

最小编码单元是多字节才会有字节序的问题存在,UTF-8 最小编码单元是一字节,所以它是没有字节序的问题,UTF-16 最小编码单元是 2 个字节,在解析一个 UTF-16 字符之前,需要知道每个编码单元的字节序。

比如:前面提到过,"中" 字的 Unicode 码是 4E2D, "?" 字符的 Unicode 码是 2D4E, 当收到一个 UTF-16 字节流 4E2D 时,计算机如何识别它表示的是字符 "中" 还是字符 "?" 呢 ?

所以,对于多字节的编码单元,需要有一个标记显式的告诉计算机,按照什么样的顺序解析字符,也就是字节序,字节序又分为大端字节序和小端字节序

小端字节序简写为 LE( Little-Endian ),表示低位字节在前,高位字节在后,高位字节保存在内存的高地址端,而低位字节保存在内存的低地址端;

大端字节序简写为 BE( Big-Endian ),表示高位字节在前,低位字节在后,高位字节保存在内存的低地址端,低位字节保存在在内存的高地址端。

下面以 0x4E2D 为例来说明大端和小端,具体参见下图:


数据是从高位字节到低位字节显示的,这也更符合人们阅读数据的习惯,而内存地址是从低地址向高地址增加。

所以,字符0x4E2D 数据的高位字节是 4E,低位字节是 2D。

按照大端字节序的高位字节保存内存低地址端的规则,4E 保存到低内存地址 0x10001 上,2D 则保存到高内存地址 0x10002 上。

对于小端字节序,则正好相反,数据的高位字节保存到内存的高地址端,低位字节保存到内存低地址端的,所以 4E 保存到高内存地址 0x10002 上,2D 则保存到低内存地址 0x10001 上。

BOM

BOM 是 byte-order mark 的缩写,是 "字节序标记" 的意思,它常被用来当做标识文件是以 UTF-8、UTF-16 或 UTF-32 编码的标记。

在 Unicode 编码中有一个叫做 "零宽度非换行空格" 的字符 ( ZERO WIDTH NO-BREAK SPACE ),用字符 FEFF 来表示。

对于 UTF-16 ,如果接收到以 FEFF 开头的字节流,就表明是大端字节序,如果接收到 FFFE,就表明字节流 是小端字节序。

UTF-8 没有字节序问题,上述字符只是用来标识它是 UTF-8 文件,而不是用来说明字节顺序的。"零宽度非换行空格"字符的 UTF-8 编码是 EF BB BF,所以如果接收到以 EF BB BF 开头的字节流,就知道这是UTF-8 文件。

下面的表格列出了不同 UTF 格式的固定文件头
UTF编码固定文件头
UTF-8EF BB BF
UTF-16LEFF FE
UTF-16BEFE FF
UTF-32LEFF FE 00 00
UTF-32BE00 00 FE FF


根据上面的 固定文件头,下面列出了"中"字在文件中的存储(包含文件头)
编码固定文件头
Unicode 编码0X004E2D
UTF-8EF BB BF 4E 2D
UTF-16BEFE FF 4E 2D
UTF-16LEFF FE 2D 4E
UTF-32BE00 00 FE FF 00 00 4E 2D
UTF-32LEFF FE 00 00 2D 4E 00 00


用一张表总结一下 3 种编码格式:
 ASCIIUTF-8UTF-16UTF-32
编码空间0~7F0~10FFF0~10FFF0~10FFF
最小存储占用1124
最大存储占用1444



常见应用层字符编码的问题集


1、Redis 中文key的显示

需要向redis中写入含有中文的数据,然后在查看数据,但是会看到一些其这类十六进制的字符,而不是刚写入的中文。

直接向redis 写入了一个 "中" 字,通过 get 命令查看的时候无法显示刚写入的 "中" 字。

此时为redis-cli加一个'--raw'参数,重连服务即可在终端中直接显示刚写入的 "中" 字。

2、MySQL 中的 utf8 和 utf8mb4

MySQL字符编码集中有两套UTF-8编码实现:“utf8”和“utf8mb4”,其中“utf8”是一个字最多占据3字节空间的编码实现;而“utf8mb4”(2010年在5.5版引入)则是一个字最多占据4字节空间的编码实现,也就是UTF-8的完整实现。

参考资料

Unicode(维基百科)
UTF-8(维基百科)
UTF-16(维基百科)
Unicode 1x 发布公告
UTF-8 and Unicode FAQ for Unix/Linux
UTF-8, a transformation format of ISO 10646(互联网工程任务组(IETF))
UTF-16, a transformation format of ISO 10646(互联网工程任务组(IETF))
Unicode Format for Network Interchange(互联网工程任务组(IETF))
字符编码笔记:ASCII,Unicode 和 UTF-8(阮一峰)
阮一峰老师文章的常识性错误之 Unicode 与 UTF-8(刘志军)
gb、big5、unicode、utf-8等编码的区别