Unicode 基础知识
Unicode 简介
Unicode是一种标准,它精确地定义了字符集以及它的少数几种编码方式。它使您能够搞笑地处理任何语言的文本。它允许单个应用程序可执行文件为全球受众服务。 ICU 与 Java™、Microsoft® Windows NT™、Windows™ 2000 和其他现代系统一样,提供基于 Unicode 的国际化解决方案。
本章旨在介绍一般的 代码页,特别是 Unicode。欲了解更多信息,请参阅:
试试 在线 ICU 演示,了解基于 Unicode 的服务器应用程序如何处理多种语言和多种编码的文本。
传统字符集和 Unicode
在计算机中表示文本格式数据的方法是定义一组字符并为每个字符分配一个数字和一个 位表示。这一基本思想的基础是三个相关的概念:
- 字符集或 Repertoire 是可以用数值表示其元素的无序字符集合。
- 一个编码字符集将字符集中的字符或 Repertoire 映射到数值。
- 字符编码方案定义了一个或多个编码字符集中数值的位表示和字节表示。
译者注:Repertoire 是什么?
Repertoire 是某个系统、编码或标准所能表示或包含的全部字符、符号和标记的集合。
Repertoire 与字符集(character set)的关系是,字符集定义了一组特定的字符以及它们在给定编码方案中的表示方式,而 Repertoire 则是这些字符集所包含的所有字符的总和。换句话说,字符集是 Repertoire 的一个子集,专注于特定编码方案中的字符表示,而 Repertoire 更广泛地描述了编码系统能够表示的所有字符的集合。在 Unicode 标准中,Unicode 字符集的 Repertoire 包括了世界上所有主要书写系统的字符和许多其他符号和符号。
对于如 ASCII 这样的简单编码,最后两个概念基本上是相同的:ASCII 将 128 个字符和控制代码分配给从 0 到 127 的连续数字。这些字符和控制代码被编码为简单的、无符号的二进制整数。因此,ASCII 既是一个编码字符集,也是一种字符编码方案。
ASCII 仅编码 128 个字符,其中 33 个是控制代码,而不是图形可显示字符。它旨在为美国用户群表示英语文本,因此不足以表示除美式英语之外的几乎任何语言的文本。事实上,大多数传统编码仅限于几种语言和文字。
ASCII 提供了一种自然的扩展方式:设计于 1960 年代,用于在字节长度为 7 位的系统中工作,而自 1970 年代以来大多数计算机和 Internet 协议使用的字节都是 8 位的,额外的位允许另外 128 个值表示更多字符。支持不同语言的各种编码被开发出来,其中一些基于 ASCII,另一些则不是。
日语等语言需要编码远远超过 256 个字符。各种编码方案可以表示具有数千或数万个字符的大型字符集。大多数这些编码仍然是基于字节的,这意味着许多字符需要两个或更多字节的存储空间。因此,必须开发一个东西用来解释这些字节。
各种字符集和编码方案都是独立开发的,各自只涵盖一种或几种语言,并且互不兼容。这使得单个系统很难同时处理多种语言的文本,尤其是以跨不同系统且具有互操作性的方式进行处理。
一般来说,文本数据可互操作交换的最低要求是必须在文档和协议中正确指定编码(字符集和编码方案)。例如,电子邮件 / SMTP 和 HTML / HTTP 提供了指定字符集的方法,正如在 互联网标准 中所称。然而,经常发生的情况是编码未被指定、指定错误,或者发送方和接收方在其实现上存在分歧。
发明 ISO 2022 编码方案是为了存储多种不同语言的文本,它允许通过预先声明从而嵌入其他编码。若要完全支持 ISO 2022 的所有功能和可能的编码,需要很复杂的操作。对于东亚语言,它的子集仅涵盖一种或几种语言,但更易于管理。 ISO 2022 不太适合在内部处理中使用,因为它是为数据交换而设计的。
字形与字符的区别
程序员经常需要区分字符和字形。字符是书写系统中最小的语义单位,是一个抽象概念,例如字母 A 或感叹号。字形是一个或多个字符的视觉呈现,并且通常取决于多个相邻字符。
字符和字形之间并不总是存在一对一的映射。在许多语言中(阿拉伯语就是一个典型的例子),字符的外观在很大程度上取决于周围的字符。标准印刷阿拉伯语对于字母表中的每个字母都有多达四种不同的印刷表示形式(字形)。在许多语言中,两个或多个字符可以组合在一起形成单个字形(称为连字),或者单个字符可以用多个字形显示。
尽管特定字母有不同的视觉变体,但它仍然保留其身份。例如,阿拉伯字母 heh 有四种不同的常用视觉表示。无论使用哪一个,它仍然保持字母 heh 的身份。Unicode 编码的正是这种标识,而不是视觉表示,这也减少了所需的独立字符值的数量。
Unicode 概述
Unicode 是作为单一编码字符集开发的,包含对世界上所有语言的支持。 Unicode 的第一个版本使用 16 位空间,无需复杂的多字节方案即可编码 65536 个字符。随着添加更多字符,并满足许多不同平台的实现需求,Unicode 被扩展为允许超过一百万个字符,并添加了其他几种编码方案,这给 Unicode 标准带来了更多的复杂性,但还是远不及管理大量不同的编码负责复杂。
从 Unicode 2.0(1996 年发布)开始,Unicode 标准开始分配从 0 到 1114111 的数字,这需要 21 位(尽管并没有完全使用它们),这为世界上所有书面语言提供了足够的空间。最初的 Repertoire 涵盖了计算中常用的所有主要语言。随着 Unicode 不断发展,越来越多的文字被加入其中。
Unicode 的设计与传统字符集和编码方案在这几个方面有区别:
- 它的 Repertoire 使用户能够在单个文档中高效地包含几乎所有语言的文本。
- 它可以以基于字节的方式进行编码,每个字符一个或多个字节,但默认编码方案使用 2 字节,这使得对所有常见字符的处理更加简单。
- 许多字符(例如带有重音符号和变音符号的字母)可以由基本字符和重音符号或变音符号修饰符组合而成,这种组合减少了需要单独编码的不同字符的数量。为了保证兼容性,包含了当时常见字符集中存在的字符的“预组合”变体。
- 字符及其用法是 良定义 的,且有明确的描述。传统字符集通常仅提供字符的名称或图片及其编号和编码,而 Unicode 具有可供下载的综合属性数据库。它还定义了许多用于处理文本的算法,以使其更具互操作性。
Unicode 在早期就包含了常用字符集的所有字符,这使得它成为一个有用的中转站——用于在传统字符集之间进行转换,并且使得处理非 Unicode 文本成为可能:先将文本转换为 Unicode,处理文本,然后将其转换回原始编码,而不会丢失数据。
👉 前 128 个 Unicode 码位被分配给了 ASCII 中相同的字符。与 ISO 8859-1(Latin-1)相比,Unicode 的前 256 个代码位值也是如此,ISO 8859-1(Latin-1)本身就是 ASCII 的直接超集。这使得许多应用程序可以轻松地适应 Unicode,因为许多语法上重要的字符的数字是相同的。
Unicode 的字符编码形式和方案
Unicode 为字符分配从 0 到 1114111 的数字,从而提供足够的 空间 来对常用的每个字符进行明确的编码,这样的字符编号称为“码位”。
译者注
上面这段话中,“空间”的原文是 "Elbow Room",其意为自由度或活动空间。
在原文中,"Elbow Room" 指的是 Unicode 编码系统提供的广阔编码范围,确保了为每种字符分配唯一编码的空间,从而避免了混淆和重叠。
👉 Unicode 码位只是一定范围内的非负整数,它们没有隐式二进制表示形式,也没有 21 或 32 位的宽度,二进制表示和单位宽度是为编码形式定义的。
对于内部处理,该标准定义了三种编码形式;对于文件存储和协议,其中一些编码形式具有字节顺序不同的编码方案。编码形式和编码方案之间的区别在于,编码形式将字符集代码映射到适合内部数据类型的值(如 C / C++ 中的 short
),而编码方案映射到位和字节。对于传统编码,它们是相同的,因为编码形式已经映射到字节。
不同的 Unicode 编码形式针对各种不同的用途进行了优化:
- UTF-16 是默认编码形式,将字符码位映射到一个或两个 16 位整数。
- UTF-8 是一种基于字节的编码,可向后兼容基于 ASCII、面向字节的 API 和协议。一个字符用 1 至 4 个字节存储。
- UTF-32 是最简单但最占用内存的编码形式:它为每个 Unicode 字符使用一个 32 位整数。
- SCSU 是一种提供 Unicode 文本简单压缩的编码方案。它仅设计用于输入和输出,不供内部使用。
ICU 内部使用 UTF-16,且从 ICU 2.0 起,完全支持 增补字符(码位为 65536 到 1114111)。
对于输入输出,字符编码方案定义文本的字节序列化。UTF-8 本身既是一种编码形式,又是一种编码方案,因为它是基于字节的。对于 UTF-16 和 UTF-32 中的每一个,定义了两种变体:一种以大端序(最高有效字节在前)序列化 码元 ,另一种以小端序(最低有效字节在前)序列化码元。相应的编码方案称为 UTF-16BE、UTF-16LE、UTF-32BE 和 UTF-32LE。
👉 名称“UTF-16”和“UTF-32”是不明确的。根据上下文,它们要么指处理 16 或 32 位并自然存储在平台字节序中的字符编码形式,要么指 互联网号码分配局 注册的字符集名称,即字符编码方案或字节序列化。除了简单的字节序列化之外,具有这些名称的字符集还使用可选的字节顺序标记(请参阅下面的 序列化的格式)。
UTF-16 概述
Unicode 标准的默认编码形式使用 16 位码元。最常见字符的码位在 0 到 65535 范围内,并且仅使用一个 16 位单元进行编码。从 65536 到 1114111 的码位使用两个通常称为“代理项”的码元进行编码,当它们一起时,它们被称为“代理对”,并正确编码一个 Unicode 字符。一对中的第一个代理项必须在 55296 到 56319 范围内,第二个代理项必须在 56320 到 57343。每个 Unicode 码位只有一种可能的 UTF-16 编码,是一个不是代理项的码元,或者是一个正确的代理对。码位 55296 到 56319 专门为此机制预留,并且永远不会单独分配任何字符。
大多数常用字符的码位不超过 65535,但 Unicode 3.1 分配了超过 40000 个使用 UTF-16 中代理对的增补字符。
请注意,根据 16 位码元对 UTF-16 字符串进行 基于字典序的比较 不会产生与比较码位相同的顺序。这通常不是问题,因为只有很少使用的字符受到影响。必要时,可以对字符串比较作简单的修改,这仍然允许高效的基于码元的比较,并使它们与码位比较兼容。ICU 为此提供了 C 和 C++ API 函数。
UTF-8 概述
为了满足面向字节、基于 ASCII 的系统的要求,Unicode 标准定义了 UTF-8。UTF-8 是一种可变长度、基于字节的编码,并且可以兼容 ASCII。
UTF-8 兼容所有的 ASCII 码(0 到 127)。这些值不会出现在转换结果的任何字节中,除非直接表示 ASCII 值。因此,ASCII 文本也是 UTF-8 文本。
UTF-8 的特性包括:
- Unicode 码位 0 到 127 均使用相同值的单个字节进行编码,因此,UTF-8 编码的 ASCII 字符占用的空间比 UTF-16 少 50%。
- 所有其他码位均使用多字节序列进行编码,第一个字节(前导字节)指示后面要附加多少字节(尾随字节),这使得解析变得非常高效。前导字节的范围为 192 到 253,尾随字节的范围为 128 到 191。字节值 254 和 255 从未被使用。
- UTF-8 在使用欧洲文字编码文本所需的字节数方面相对紧凑且节省空间,但对于东亚文本使用的空间比 UTF-16 多 50%。 2047 以下的码位占用两个字节,65535 以下的码位占用三个字节(比 UTF-16 多 50% 的内存),其他的码位占用四个字节。
- 对 UTF-8 编码的字符串作基于字典序的比较,其结果与比较码位值的顺序相同。
UTF-32 概述
UTF-32 编码形式始终为每个 Unicode 码位使用一个 32 位整数。这导致了非常简单的编码。
缺点是它的内存消耗:由于码位值仅使用 21 位,因此约三分之一的内存始终未使用,并且由于最常用的字符的码位值在 65535 以内,因此它们在 UTF-16 中仅占用 16 位(减少 50%),在 UTF-8 中最多占用 3 个字节(减少 25%)。
UTF-32 主要用于为码位和码元定义相同数据类型的 API。支持 Unicode 的现代版本 C 标准库使用具有 UTF-32 语义的 32 位 wchar_t
。
译者注:wchar_t
? what_cr(azy)!
至少我不会使用一个在 Windows 系统和其他系统上表现行为不同的类型。作为一个号称“相同的代码可以移植到各种平台”的项目,写出这样的文档是否有些过于草率?同样的,后文提到 wchar_t
的地方也有相似的问题。我无意在技术性文档中夹带私货,只想提醒读者:不要使用 wchar_t
,而总是使用 char
char8_t
char16_t
char32_t
。
基础类型 - cppreference
Relax requirements on wchar_t to match existing practices
SCSU 概述
SCSU(Unicode 标准压缩方案)旨在减少输入和输出的 Unicode 文本大小,这是一种将文本转换为字节流的简单压缩。对于文字总量小的语言,每个字符用一个字节;对于文字总量大的东亚语言,每个字符使用两个字节。
它通常比任何 UTF-x 编码都短。然而,SCSU 是 有状态 的,这使得它不适合内部处理。它还使用所有可能的字节值,这可能需要对 SMTP(电子邮件)等协议进行额外处理。
另请参阅 https://www.unicode.org/reports/tr6/。
其他 Unicode 编码
随着时间的推移,出于各种目的,还开发出了其他 Unicode 编码。其中大部分在 ICU 中被实现,参见 source/data/mappings/convrtrs.txt。
- BOCU-1:二进制有序压缩的 Unicode。这是一种Unicode 编码方式,其紧凑程度与 SCSU 相近,但状态量要少得多。与 SCSU 不同,它保留了码位顺序,并且可以在不需要传输编码的情况下用于 8 位邮件。BOCU-1 不 保留 ASCII 字符的 ASCII 可读形式。请参阅 Unicode 技术说明 #6。
- UTF-7:为 7 位邮件设计,简单但不太紧凑。UTF-7 中大多数 ASCII 字符是可读的,其他字符则使用 Base64 编码。由于电子邮件系统多年前就已经安全支持 8 位,UTF-7 已不再必要且不推荐使用。请参阅 RFC 2152。
- IMAP-mailbox-name:一种 UTF-7 的变体,将 Unicode 字符串表示为 ASCII 字符,从而可以用于 Unix 系统的文件名。 名称“IMAP-mailbox-name”仅在 ICU 中使用! 请参阅 RFC 2060 互联网消息访问协议 - 版本 4rev1 第 5.1.3 节《邮箱国际命名约定》。
- UTF-EBCDIC:一种对 EBCDIC 编码友好的编码,类似于UTF-8。请参阅 Unicode 技术报告 #16。 自 ICU 2.6 起,ICU 不再实现 UTF-EBCDIC。
- CESU-8:UTF-16的八位元相容编码方案:一种与 UTF-8 不兼容的变体,它保留了 16 位 Unicode(UTF-16)的字符串顺序,而不是码位顺序,它不适用于 Open Interchange。参见 Unicode 技术报告 #26。
使用 UTF-x 编码进行编程
尽管 UTF-8 和 UTF-16 也是可变宽度编码,但使用任何 UTF-x 编码进行编程都比使用传统的多字节字符编码简单得多。
在每种 Unicode 编码形式中,Singletons、首字节和尾字节的取值范围都是不相交的,这对实现方式有着重要的影响:
- 使用首字节就能确定一个码位需要几个(尾)字节,这对于 UTF-8 尤为重要,因为每个字符最多可以有 4 个字节,也就是 1 个首字节和 3 个尾字节。
- 如果 ICU 用户随机访问文本,您可以用简单的方式确定码元序列中最近的 Unicode 码位边界。
- 码元序列不会重叠,因此用户无需转换为码位——也就是可以直接在字节序列中——即可进行字符串搜索,绝对不会出错,因为一个码元序列的末尾永远不会与另一个码元序列的开头相同,从而一定能找到正确的序列首部。重叠是 Shift-JIS 等常见多字节编码的最大问题之一,所有的 UTF-x 编码都避免了这个问题。
- 迭代方式简单,只需要一点点操作就能获取序列中的下一个或上一个码位。
- 可以使用 UTF-16 编码,它实际上是完全对称的。ICU 用户可以从任何一个单一的码元判断它是某个码位的第一个、最后一个,或是唯一的一个码元。无论是正向还是逆向遍历 UTF-16 文本,移动(迭代)都是同样快速和高效的。
- 按码位的索引很慢,这是所有可变宽度编码的缺点。除了 UTF-32 之外,在找到对应第 n 个码位的码单位边界,或者找到包含第 n 个码单位的码位偏移量时都是低效的,因为不同的码位可能对应不同的码元数量,所以必须顺序遍历。ICU 和大多数常见 API 一样,它计数的是码元而不是码位。
INFO
我目前并不太理解第 5 条是什么意思,故在此放出原文:
Can use UTF-16 encoding, which is actually fully symmetric. ICU users can determine from any single code unit whether it is the first, last, or only one for a code point. Moving (iterating) in either direction through UTF-16 text is equally fast and efficient.
等我理解了会来更新这里的内容并加上解释的。
不同 UTF-x 之间的转换非常快,与 Latin-2 等传统编码之间的转换不同,UTF-x 之间的转换不需要查表。
ICU 为 Unicode 提供了两种基本数据类型定义。 UChar32
是代码位的 32 位类型,用于单个 Unicode 字符,可以是有符号的或无符号的,宽度可能与 wchar_t
相同(如果 wchar_t
是 32 位的)。 UChar
是 UTF-16 代码单元的无符号 16 位整数,宽度有可能与 wchar_t
相同(如果 wchar_t
是 16 位的)。
译者注:wchar_t
? what_cr(azy)!
此处再次提到 wchar_t
,详细情况见 上一处。
一些更高级的 API,尤其是用于格式化的,使用更接近字形表示的字符。这种“用户字符”也称为 字位 或 字位簇,并且需要字符串以便可以容纳字位簇。
序列化的格式
在文件、输入输出以及网络协议中,文本必须附有其字符编码方案的 说明,以便客户端能够正确解释它。(在互联网协议中,这被称为 charset
)。然而,如果文本仅在单一平台、协议或应用程序中使用,并且编码是什么已经很清楚了,那么就不需要编码说明。
这一节中对编码规范的讨论适用于使用字符集名称字符串的标准 网际协议,其他协议可以使用数字编码标识符,并且给这些标识符赋予与网际协议不同的语义。
通常,编码说明以协议和文档格式相关的方式完成。然而,Unicode 标准提供了一种机制,用于在协议无法识别字符编码方案的情况下使用“签名”来标记文本文件。
可以通过在文件或流的最前面添加
不同的字符编码方案为
- UTF-8:
EF BB BF
- UTF-16BE:
FE FF
- UTF-16LE:
FF FE
- UTF-32BE:
00 00 FE FF
- UTF-32LE:
FF FE 00 00
- SCSU:
0E FE FF
- BOCU-1:
FB EE 28
- UTF-7:
2B 2F 76 ( 38 | 39 | 2B | 2F )
- UTF-EBCDIC:
DD 73 66 73
ICU 提供函数 ucnv_detectUnicodeSignature()
用于探测 Unicode 签名。
CESU-8 的签名与 UTF-8 相同。UTF-8 和 CESU-8 实际上使用相同的字节对
在 UTF-16 和 UTF-32 中,签名也用于区分大小端,它也被称为端序标记(Byte Order Mark,BOM,字节顺序标记)。这个签名对 UTF-16 是有效的,因为编码字节交换后的码点
签名不是内容的一部分,处理后必须将其删除,否则会产生错误的行为。例如,盲目连接两个文件将给出不正确的结果。但也不能盲目地删除签名,应在转换后从 Unicode 流中删除签名,在转换前移除签名字节可能会导致像 BOCU-1 和 UTF-7 这样有状态的编码转换失败。
签名是否被识别取决于协议或应用程序。
如果一个协议指定了一个字符集名称,那么必须根据该名称的定义解释字节流。只有 "UTF-16" 和 "UTF-32" 这两个名称要求识别端序标记,并且 ICU 转换器会自动为这些名称执行此操作。其他所有 Unicode 字符集都没有定义包括任何签名处理。
如果没有提供字符集名称,例如对于大多数文件系统中的文本文件,应用程序通常必须依靠 启发式算法 来确定文件编码。许多文档格式包含嵌入或隐式编码声明,但对于纯文本文件,使用 Unicode 签名作为简单可靠的启发式算法是合理的。这在 Windows 系统上尤其常见。然而,一些用于纯文本文件处理的工具(例如许多 Unix 命令行工具)没有为 Unicode 签名做好准备。
Unicode 标准就是行业标准!
与 ISO 10646-1 类似,Unicode 标准是一项行业标准,两个标准都具有相同的 Repertoire 和编码形式与方案。1993 年左右,这两个标准合并了。
过去的一个区别是 ISO 标准定义的码点值从 0 到 2147483647,而不仅仅是到 1114111。ISO 工作组决定对该标准进行修订,该修订规定不会有超过 1114111 的码点。ISO 工作组做出此决定的主要原因是为了保证 UTF-x 编码之间的互操作性,UTF-16 无法对任何高于 1114111 的码点进行编码。这意味着现在 Unicode 和 ISO 10646 的码点空间是相同的了!这些对 ISO 10646 的更改是最近进行的,应该在 ISO 10646:2003 版中完成,该版本还将标准的所有部分合并为一个。 ISO 10646 有着更大的代码空间,这是 ISO 对 UTF-8 定义指定 5 字节和 6 字节序列以覆盖整个范围的原因。
另一个区别是 ISO 标准定义了编码形式 "UCS-4" 和 "UCS-2"。UCS-4 本质上是 UTF-32 ,理论上限为 2147483647,使用 32 位中的 31 位。但实际上,ISO 委员会已经决心让 1114111 以上的字符不会被编码,因此这些形式(UCS-4 和 UTF-32)基本上没有区别。"4" 代表“四字节形式”。
UCS-2 是 UTF-16 的子集,仅限于从 0 到 65535 的码点,不包括代理项对应的码点。因此,它不能表示增补字符。UCS-2 和 UTF-16 之间无需转换,区别仅在于对代理项的解释。
这两个标准在它们提供的信息类型上有所不同:Unicode 标准提供了更多的字符属性并描述了算法等,而 ISO 标准定义了集合、子集及类似概念。
这些标准是同步的,各自的委员会共同工作以添加新字符和分配码点值。