前面的字符集编码(上):Unicode 在此之前,我们谈到了20世纪90年代 Unicode 在出现之前,制造商和标准化组织设计了各种不兼容的字符集编码标准,以满足不同语言的编码需求,这使得软硬件开发商在处理多语言环境时相当困难。为了解决字符集编码的混乱,一些利益相关公司开始聚在一起,试图设计一编码标准,可以包括世界上所有的字符。
开端
1987 年,苹果(Apple)和施乐(Xerox)两家公司的三名技术人员聚集在一起,研究开发通用字符集的可行性。 1987 年末到 1988 在年初的几个月里,他们进行了一系列的调查统计,主要是为了找出这样的事情:
- 这个字符集需要包含多少个字符?
- 世界上使用双字节编码的字符数量?
- 应采用固定宽度(定长编码)还是混合宽度(变长编码)?
- 中、日、韩的表意文字能统一吗(也就是说,同一个字符同时出现在中、日、韩三种语言中,只能编码一次)?
在设计统一代码时,世界上有大量的双字节代码标准(如 GB 2312、Shift JIS、Big5 等待东亚编码标准),施乐当时也设计了一套双字节多语言编码标准,所以在最初设计统一代码时,人们倾向于使用是的,双字节最多可以表示的字符数量是 2^16 = 65536 所以下一个重点是验证世界上的字符总数是否大于这个数字。
工作组的验证结果是肯定的(虽然在我们看来有点出乎意料,因为光汉字不止这个数字)。当时工作组的原则是(即现代语言中使用的字符,不考虑古埃及、古巴比伦、古汉语等现代语言中不再使用的字符)(而不是单独编码复杂的字符),比如西班牙语 ? 由 n 和 ~ 由两个字符组成,大量的韩语也组合在一起(事实上,我们也考虑通过部分部分组合汉字,但发现汉字结构太复杂,所以我们放弃了)。在这些原则下,工作组统计了当时世界各地的报纸和其他出版物,得出结论:这两个字节足以包含世界各地的实用字符。
早期工作组的另一种设计倾向是采用。
在决定使用双字节编码后,有两种选择:一种是变长编码(类似) GB 2312),对于 ASCII 字符使用一个字节,其他字符使用两个字节;另一种是长编码,无论是否 ASCII 两个字节统一使用字符。
工作组主要从存储和处理效率的角度验证两种方案的优缺点。验证结论是,双字节长编码带来的文本尺寸增长是可以接受的(与文本格式信息、图片、视频等其他信息相比,字符本身的编码信息占用的空间很小),长编码形式的处理效率优于长编码形式,因此早期 Unicode 采用了定长编码形式。
工作组面临的第三个问题是。
由于中化文化的影响,东亚国家的语言包含了一些汉字(如日语和韩语)。这些(不同语言)中的相同字符(汉字)是否被视为相同的字符或不同的字符?
我们在上一篇关于字符编码模型的文章中说过,字符集编码(尤其是 Unicode)它是对抽象字符的编码,而不是对字体或意义的编码。根据这一原则,这些汉字应该被视为同一个字符,尽管它们有不同的含义(可能有一些不同)。
因此,工作组决定将汉字部分合并为中日韩语言(CJK,中、日、韩三种语言的首字母)-所以你在 Unicode 区块表中找不到,比如 “Han”、“Chinese” 是的,汉语是在 CJK 和 CJK 扩展块中。
Unicode 合并中日韩汉字是有争议的。因为 Unicode 它是针对抽象字符而不是字形代码,而同一汉字在不同语言中的写作方法(字形)可能不同(例如,中国汉字带在日语中的写作带,以及同一个词的古今写作方法可能不同),对于一些不同写作方法的汉字 Unicode 它也被认为是同一个字符。因此,有些人认为这种合并会让人觉得语言本身已经失去了独立性。
综上所述,最初的研究下:
- 使用双字节长编码;
- 中日韩汉字合并编码;
这些研究成果是 1988 年 8 以草案的形式发布(后称为 Unicode 88)在草案中正式使用 “Unicode” 一词,中文翻译成统一码。
后来,越来越多的公司加入了工作组,包括 Sun、微软、NeXT 等等。这些公司 1991 年 一月份决定在加州建立正式联盟,称为(Unicode Consortium),并于同年 10 月发布了 Unicode 的第一版(Unicode 1.0.0。注意 88 年的草案只公布了最初的研究成果,很多工作还没有完成)。这个版本只包括 24 共用语言和文字 7163 个字符。
注意第一版 Unicode 不包括标准 CJK 字符-此时 CJK 部分工作尚未完成。CJK 第二年(1992 年 6 月)的 Unicode 1.0.1 添加(此版本包含) 20902 个汉字)。
如今(2022 年 2 月)最新版 Unicode 已经到了 14.0.0(于 2021 年 9 每月发布),支持 159 种文,共包含 144,697 个字符(包括控制字符、文字符号、表达符号等。
另一个兄弟
Unicode 当这些人在设计时,联盟并不孤单 Unicode 另一个组织在标准时也在做同样的事情。
ISO 和 IEC 两家组织在 1984 成立联合工作组设计新的统一字符集——后来 (Universal Character Set),于 1989 年公布了 UCS 草案(Unicode 工作小组在 1988 年发布了 Unicode 草案)。
起初,这两个工作组没有人知道对方的存在。直到双方公布了自己的设计草案并被传阅,我们才逐渐意识到他们做了同样的事情,这将导致世界上有两套统一的编码字符集标准,这对双方和世界都不利。
于是这两个工作组就在了 1991 年开始谋求合并方案。
所谓合并,并不是一个工作组完全抛弃自己的东西,拥抱另一个工作组的标准。假如你花了很多人力物力去做一些事情,然后发现别人也做了几乎一样的事情,你愿意完全放弃这一套吗?此外,虽然两个组织都有相同的目标(统一的字符集),但在设计原则和一些细节上仍然有很多不同。更麻烦的是,当人们坐下来谈论合并时,Unicode 1.0 已发布,UCS 还处于草案阶段,但离发布不远。出于各方面的原因,合并的前提是仍然保持两个标准都独立存在和发展,在此基础上保持两个标准的兼容和同步(或者其中一个是另一个的子集,或者两个在编码字符集 CCS 层次完全一致)。
和 Unicode 使用 16 位编码空间不同,UCS 从一开始就选择使用 31 位编码空间,也就是说,UCS 最多能容纳 2^31 约 21 亿个字符。最开始,Unicode 打算作为 UCS 真子集,即 Unicode 每个字符都存在于中间 UCS 而且两者的码点是一样的,但是 UCS 中间的字符(过 64K )不一定存在 Unicode 中。
经过多次争论和投票,双方最终在字符集层面达成了一致,即两个标准中相同字符的代码(代码点)必须相同,Unicode 针对 1.0 版本做了一些调整(比如调整一些字符的编码,调整一些语言文本的块),最终 ISO/IEC 在 1993 年发布了 UCS 第一版 ISO/IEC 10646-1:1993,Unicode 同年,联盟还发布了兼容版本 Unicode 1.1。
当然,合并工作是分步进行的,合并工作成果也是分版发布的,1993 年版发布只是双方合并结果的初步(也是最重要的)。
Unicode 概览
网上有一个问题:Unicode 和 UTF-8 关系是什么?
许多回答说:许多回答说:说:Unicode 是字符编码,UTF-8 实现编码。
答案不准确。Unicode 字符编码很好,但大多数人理解的字符编码只指字符编号(字符编码模型的第二层),这让人感觉好像 Unicode 和 UTF-8 两个平行独立的东西。实际情况是 Unicode 从抽象字符集的定义到计算机编码方案,标准包括字符编码模型的各个层次,UTF-8 属于 Unicode 编码实现标准中的部分。
Unicode 在设计之初,我们只计划编码人类正在使用的字符,而不考虑曾经使用但现在已经废弃的古代文,所以我们最初认为双字节就足够了。然而,很快我们发现这个想法是不可行的,因为虽然一些文本符号在日常生活中不能使用,但它们将用于特殊领域(如历史、考古学、语言学等) Unicode 不包括这些字符,这些特殊领域必须设计其他字符集标准,这是违反的 Unicode 初衷。于是很快 Unicode 联盟决定扩大 Unicode 编码空间,从 16 位拓宽到 21 100多万字符(有的空间属于保留空间或私人空间,不能分配码点)。
Unicode 非人类语言编码,如腾格瓦语言(Tengwar,《魔戒》作者托尔金创作的精灵语言)、克林贡语(Klingons,克林贡人在星际迷航中使用的语言)。
一个可能有多种形状(字形),但它们都有相同的含义,在 Unicode 被视为同一个字符,只分配一个码点,如楷书、行、草、隶等写法,属于同一个字符的不同字形。需要注意的是,这里提到的字形是指由不同的写作方法引起的字形差异,而不是结构差异,如繁体中文和简体中文。从字源上看,简体中文是对繁体中文的简化写作方法,属于同意不同单词,但两者之间的差距体现在结构上书写形式上,所以要视为不同的字符。
一个字形到底是由一个字符构成的,还是由多个字符组合成的,是一件见仁见智的事情。比如字符 é,你可以认为它是一个独立的字符,也可以认为是由拉丁字母 e 和音调字符 ́ 构成。传统编码倾向于作为独立字符看待,而 Unicode 倾向于作为组合字符——。
另外,抽象字符不一定就存在可视化的字形,比如控制字符。
在 Unicode 字符集中,每个抽象字符都有唯一的名称,使用大写 ASCII 字符表示,比如拉丁字母 a 在 Unicode 中的名称是“LATIN SMALL LETTER A”。可在 http://www.unicode.org/Public/UNIDATA/NamesList.txt 查看所有字符的名称。
Unicode 使用整型数值对这些抽象字符编码,在书写上,用数值的十六进制表示,且至少是 4 位,少于 4 位的使用前导 0 填充,比如 61 要写成 0061。另外要在数值前面加上 U+ 表示是 Unicode 码点,因而拉丁字母 a 的 Unicode 码点写作 U+0061。
数值编码(码点)可能的范围叫(codespace)。起初 Unicode 的编码空间是 U+0000 ~ U+FFFF,大家很快发现 64K 的编码空间根本不够用,所以后来将编码空间扩大到了 U+0000 ~ U+10FFFF,可容纳一百多万的字符。
(planes,17 个大小相同的区域,编号 0 ~ 16),每个平面可容纳 2^16 即 65,536 个码点。每个平面的作用不一样: ,全世界日常使用的字符都在该平面中(早期 Unicode 就是以该平面作为整个编码空间);Plane 2 和 Plane 3 是给汉字扩展用的;最后两个平面是私有编码空间(PUA),不会分配字符码点,专门给软件自定义用的。
这些平面又进一步划分成(block),每个块放一组特定的字符,如 0000~007F 放基本拉丁字母(ASCII 字母),0590~05FF 放希伯来文,。
# Unicode 字符块(部分)
0000—007F 基本拉丁字母
0080—00FF 拉丁文补充1
0100—017F 拉丁文扩展A
0180—024F 拉丁文扩展B
...
0370—03FF 希腊字母及科普特字母
0400—04FF 西里尔字母
0500—052F 西里尔字母补充
0530—058F 亚美尼亚字母
0590—05FF 希伯来文
0600—06FF 阿拉伯文
...
0E00—0E7F 泰文
0E80—0EFF 老挝文
0F00—0FFF 藏文
1000—109F 缅甸文
...
2200—22FF 数学运算符
...
2E80—2EFF 中日韩部首补充
2F00—2FDF 康熙部首
2FF0—2FFF 表意文字描述符
...
4DC0—4DFF 易经六十四卦符号
4E00—9FFF 中日韩统一表意文字
A000—A48F 彝文音节
A490—A4CF 彝文字根
...
一种语言文字可能分散在多个块中,如汉字就存在很多扩展块,不过最常用的汉字都是在 4E00~9FFF 中。 其中一个扩展块中的汉字,都是我们没见过的,平时根本用不到
Unicode 的三种表示形式
Unicode 设计之初是采用双字节定长编码的,。比如汉字的“汉”的 Unicode 码点是 U+6C49,其计算机编码表示就是 6C49——这就是 UTF-16 的早期样子。这种编码方式的优点是高效,不需要检查标志位。
后来大家发现 16 位编码空间根本不够用,于是将编码空间拓展到 21 位。由于原始的 UTF-16 编码形式无法表示大于 FFFF 的码点,于是对 UTF-16 也进行了拓展,使其既能用 1 个码元表达 BMP 中的字符,也能用 2 个码元表示补充平面的字符——这就是现代版本的 UTF-16。
在 Unicode 认为自己的 16 位编码空间太小的同时,ISO/IEC 也觉得 UCS 的 31 位编码空间太多了,实际中根本没有几十亿字符。所以最终 Unicode 联盟和 ISO/IEC 工作组达成一致:两者使用统一的编码空间 0000 ~ 10FFFF(即 UCS 保证永远不分配大于 10FFFF 的字符码点),而且双方在字符编码上保持同步,即一方标准中增加了字符,也要通知另一方同步。
使用双字节定长编码还存在另一个——可能是更要命的——问题:它无法在编码形式层面兼容 ASCII 码。虽然 Unicode 在码点层面(第二层)兼容 ASCII(U+0000~U+007E 的码点分配和 ASCII 一致),但由于在计算机编码层面,Unicode 使用两个字节,而 ASCII 使用一个字节,这导致采用 Unicode 编码标准的软件无法正确处理现有的 ASCII 编码文件。
Unicode 1991 年才发布,ASCII 在 1968 年就发布了,这二十多年间产生了大量的 ASCII 文件和使用 ASCII 标准的软件,Unicode 置这些现存文件和软件不顾的后果就是新兴的 Unicode 标准很难被全世界(特别是计算机重度使用区欧美)广泛接受。Unicode 要想快速普及,就必须完全兼容 ASCII,因而 Unicode 联盟很快推出了 8 bit 码元编码方案:UTF-8。UTF-8 和改进后的 UTF-16 一样是变长编码方式,ASCII 字符采用单字节编码(最高位是 0),其它字符可能采用 2~4 字节,比如常用汉字用 3 字节(一些不常用汉字会用到 4 字节)。
关于为何现在 UTF-8 成为 Unicode 标准中最广泛使用的编码方式,网上大多数的回答都是说因为 UTF-8 在编码拉丁字母时更节约空间,所以欧美公司倾向于使用 UTF-8。
在我看来,这可能是原因之一,但并非主因。
节约空间并不是导致 UTF-8 被广泛使用的主因。想一想当时设计 Unicode 的都是谁,苹果、施乐、微软等等,这些都是当时或未来计算机行业的代表,他们肯定是按照自己的实际诉求来设计 Unicode 的,是在做了充分调研、实际测试验证后,得出双字节编码并不会造成拉丁语系文本空间显著增长的结论,才决定用双字节定长编码。
真正让 UTF-8 广为接受的恰恰就是历史遗留下来的那些 ASCII 文本和程序(以及操作系统、编程语言)。
这个论点从直觉上可能觉得不可思议——因为人们直觉总是觉得过去无关紧要,现在和未来才是重要的(比如 Unicode 设计之初就不打算考虑古文字,选用双字节编码也是不打算彻底兼容 ASCII),然而真正决定未来的往往就是历史——人类文明如此,项目的成败亦是如此。
我们必须正视两个事实:1. Unicode 比 ASCII 晚了二十多年;2. 其他传统编码基本上都兼容 ASCII。这两点导致在 Unicode 发布的时候,世界上必然存在大量的 ASCII 文本和使用或兼容 ASCII 的软件。
于是 Unicode 出来后,各大软硬件厂商面临几个选择:
- 完全拥抱 Unicode,这将造成新旧不兼容(比如文字处理软件 2.0 无法处理 1.0 生成的文件),这很可能导致新产品卖不出去;
- 完全不用 Unicode,以前该怎么苦逼继续怎么苦逼;
- 开发转换工具,为新老文本和工具做双向转换(但这种方式无疑是别扭的);
如果是你,你会怎么选呢?有可能是新产品用 Unicode,老产品继续用老编码标准,也有可能直接不用 Unicode——因为你还要考虑公司产品之间的兼容性问题。
所以当各大公司发现 UTF-8 能完美解决兼容性问题,自然都跑去用 UTF-8 了。大家都用,那 UTF-8 自然就被推广开了,而且人家都用,你不用,你的产品在跟别家互操作时就会出现兼容性问题,进而就会被市场淘汰——所以你也必须用。
我们用上帝视角设想,假如 1968 年的那个标准不是单字节编码的 ASCII 而是双字节编码的 Unicode(虽然从历史环境来说不太可能),那么很可能今天就压根没有 UTF-8 编码。UTF-8 编码也仅仅是在 ASCII 字符区域节约空间,在拉丁扩展区域(也就是欧洲拉丁字母)和 UTF-16 一样占两个字节,而在常用汉字区域则比 UTF-16 多使用一个字节。全世界最常用的字符都是在基本平面 BMP 中,UTF-16 在该平面恒定使用两个字节编码,其效率近似于定长编码方式,而 UTF-8 则使用 1~3 个字节编码,是真正的变长编码——文字处理软件可以针对 UTF-16 做定长假定优化,对 UTF-8 则不行(也即是说,文字处理软件可以假定 UTF-16 文本都是双字节编码,当真的遇到四字节时再做特殊处理,但不能对 UTF-8 这么做)。
所以,UTF-8 之所以会胜出,不是因为 UTF-8 在技术上比 UTF-16 有多大优势(虽然 UTF-8 设计得很巧妙),而是因为 UTF-8 在那时解决了各大公司的痛点——更准确地说,UTF-8 是为了解决大家的痛点才出现的。
参考资料:UTF-8 history; Early Years of Unicode;
另外,UCS 一开始就是支持 32 位码元的(人家从一开始就是 31 位编码空间),为了和 UCS 保持一致,Unicode 也支持 32 位码元:UTF-32。
UTF-X 中的 X 表示码元位数。
Unicode 在 2.0(1996 年) 中正式引入了 UTF-8 和改进后的 UTF-16,在 3.1(2001 年) 版本中引入了 UTF-32。
我们在下一篇将详细介绍三种编码方式的实现细节,此处仅做概要介绍。
妥协
Unicode 有两个设计原则:
- **抽象字符原则:**面向抽象字符而不是字形或字意编码;
- **动态组合原则:**使用简单的字符组合复杂字符,而不是为复杂字符单独编码;
为了让 Unicode 能够被广泛地接受,Unicode 联盟在设计之初做了一项重要决定:Unicode 必须完全兼容现有的所有字符集编码标准。这个兼容是“双程”的:任何现有字符集中的任何一个字符,可以转换成 Unicode 字符集中的字符,并且从 Unicode 中的这个字符再转换回去后还是原来那个字符,这个规则称为 round-trip rule。
这个规则让 Unicode 在实现上做了很多妥协。
我们在上篇文章中举过拉丁字母 K 的例子。在一些传统的编码标准中,拉丁字母 K 和热力学单位 K(开尔文)被当做两个不同的字符,
为了实现 round-trip 规则,Unicode 中也必须编码两个 K(分别是 Latin Capital Letter K U+004B 和 KELVIN SIGN U+212A)——否则那个传统编码中的两个 K(在那边的码点是不一样的)转换成 Unicode 编码后变成同一个 K 了,再转回去就不知道对应谁了。
类似的情况在汉语中也有很多。比如 U+2F08 和 U+319F 都是汉字“人”,U+2F17 和 U+3038 都是汉字“十”,U+03A9 和 U+2126 都是 Ω(一个是希腊字母,一个是电阻符号)。
中国的 GB 编码和日本的 JIS 编码在兼容 ASCII 的同时,又给 ASCII 中的可见字符做了个“全角”编码(原 ASCII 中的字符被称为“半角”字符)。所谓全角和半角字符,在字形和字意上都完全相同,只是全角字符占用宽度(注意不是字形本身的宽度)是半角字符的两倍(据说是为了中英文混排时的美观效果),按照 Unicode 的设计原则,这种问题应该交由文字渲染程序去处理,但由于传统编码标准中做了独立编码,所以 Unicode 中也必须支持,在 Unicode 编码表中也能看到一系列 Full Width 的拉丁字母。
另外有些传统编码标准中对不同书写体的拉丁字母也做了不同编码,Unicode 同样需要兼容之。
注意:因一些字符的不同书写体表达不同含义(如很多数学中的符号),比如拉丁字母 A 的不同书写体,在数学中是不同的意思。由于不同字形(glyph)表达的是不同的含义(semantic),因而尽管在通常意义上视为同一个字符的变体,仍然必须将其视为不同的字符(单独分配码点),否则便无法区分其真实含义(因为如果将字形交由文字处理软件渲染,而有些软件不支持特定字形,便渲染成普通字体——纯文本,于是便无法识别其含义)。 具体参见:官方说明。
源自东亚标准中的 FULL WIDTH
这些重复编码违背了 Unicode 设计中的。
在传统编码中很少有组合字符的说法,所以诸如 Å(瑞典语字符,以及长度单位“埃”)在传统编码(如 ISO/IEC 8859)中视作一个独立字符,但在 Unicode 中视作两个字符 A(U+0041)和 ̊(U+030A)的组合字符。为了兼容传统编码,Unicode 在支持组合的同时,还必须将该字形视作单独的字符分配额外码点(U+00C5)——Unicode 中称这种字符为(precomposed character)。
Å 不但存在动态组合与预合成的问题,该字符本身由于在一些传统编码标准中作为长度单位“埃”和作为拉丁字母 Å 做了不同的编码,Unicode 中也必须作此重复(U+00C5:LATIN CAPITAL LETTER A WITH RING ABOVE;U+212B:ANGSTROM SIGN)。
Unicode 中存在大量的这种违背的字符。
大部分韩语在 Unicode 中也是可以组合的,所以也存在多种编码的可能。
Unicode 中视预合成字符和动态组合字符是等效的,也就是说,如果文本中存在两个 Å,一个是组合的:<U+0041,U+030A>,另一个是预合成的:U+00C5,则软件应该将其视为一种字符,搜索的时候两个都应该能搜出来——不过目前貌似很多软件并没有实现这一点。
Unicode 基本概念就介绍到这里,下一篇我们讲讲 Unicode 的三种计算机编码实现:UTF-8、UTF-16 和 UTF-32。
原文链接:《字符集编码(中):Unicode》