Unicode 与你

我是Unicode新手。但像许多新手一样,一旦我的兴趣被激发,我就有学习的冲动。 Unicode简介.

Unicode并不难理解,但它确实涉及一些底层的计算机科学概念,比如 字节顺序。阅读Unicode的内容,是了解设计权衡和向后兼容性的好课程。

以下是我的想法。你可以单独阅读,或者作为对上面Joel的Unicode文章的后续。如果你像我一样,你会渴望阅读Unicode规范或维基百科上的细节。真的,这可以很酷,我保证。

Key concepts

让我们对一些概念达成共识:

想法和数据是不同的。 “A”这个概念,与纸上的标记、声音“aaay”或计算机内部存储的数字65是不同的东西。

一个想法有多种可能的编码方式。 编码只是一种将想法(比如字母“A”)转换为原始数据(比特和字节)的方法。“A”的概念可以用多种不同的方式编码。编码在效率和兼容性上有所不同。

了解你的编码。 读取数据时,必须知道所使用的编码才能正确解释它。这是一个简单但重要的概念。如果你看到二进制中的数字65,它到底是什么意思?ASCII中的“A”?你的年龄?你的智商?如果没有上下文,你永远不会知道。想象一下,如果有人走到你面前说“65”。你根本不知道他们在说什么。现在想象他们说“以下数字是一个ASCII字符:65”。虽然奇怪,但你看这样清楚多了吧?

接受这样一种哲学:概念和存储它的数据是不同的。让它在你的脑海中萦绕……

明白了?让我们深入探讨。

Back to ASCII and Code Pages

你可能听说过ASCII/ANSI字符集。它们将数值0-127映射到各种西方字符和控制码(换行、制表符等)。注意,数值0-127适合8位字节中的低7位。ASCII没有明确定义数值128-255的映射。

现在,ASCII编码对于英文文本(使用西方字符)来说效果很好,但世界很大。阿拉伯语、中文和希伯来语呢?

为了解决这个问题,计算机制造商定义了“代码页”(Code Pages),利用ASCII中从128到255的未定义空间,将其映射到他们需要的各种字符。不幸的是,额外的128个字符对整个世界来说不够用:代码页因国家而异(俄罗斯代码页、希伯来代码页等)。

如果使用相同代码页的人交换数据,一切正常。我机器上的第200号字符和你机器上的第200号字符是一样的。但如果代码页混合(俄罗斯发送者,希伯来接收者),事情就变得奇怪了。

映射到#200的字符在俄语和希伯来语中不同,你可以想象这在电子邮件和生日邀请中造成的混乱。别人是否使用与你写文本时相同的代码页来阅读你的信息,这是一个大问题。例如,如果你访问一个国际网站,浏览器可能会尝试 猜测 如果未指定代码页,则猜测代码页(“嗯……这个文本有很多字符#213和#218……可能是希伯来语”)。但显然这种方法容易出错:代码页需要被拯救。

Unicode to the Rescue

世界面临一个难题:他们无法在ASCII中哪些数字对应哪些字母上达成一致。Unicode组织回归基础:字母是抽象概念。Unicode用“码点”标记每个抽象字符。例如,“A”映射到码点U+0041(该码点是十六进制;十进制是65)。

Unicode组织完成了将每种语言中的每个字符映射到某个码点的艰巨工作(我确信这并非没有激烈的争论)。完成后,Unicode标准为超过100万个码点留出了空间,足以容纳所有已知语言,甚至还有余裕给尚未发现的文明。有趣的是,你可以使用字符映射表工具(开始菜单 > 运行 > Charmap)或在线上浏览码点, Unicode.org.

这就引出了我们的第一个设计决策: 兼容性.

为了与ASCII兼容,码点U+0000到U+007F(0-127)与ASCII相同。纯粹主义者可能不喜欢这样,因为完整的拉丁字符集在其他地方定义,而现在一个字母有两个码点。此外,这使西方字符“优先”,而中文、阿拉伯语和“非标准”语言则被锁在需要2字节存储的不优雅码点中。

然而,这种设计是必要的——ASCII是一个标准,如果Unicode要被西方世界采用,它必须毫无疑义地兼容。现在,大多数常用语言都位于前65535个码点内,这些码点可以用2字节存储。

呼。世界变得更好了,每个人都同意哪个码点对应哪个字符。

但问题仍然存在:我们如何将码点存储为数据?

Encoding to the Rescue

从上面来看,编码将想法转换为原始数据。在这种情况下,想法就是码点。

例如,让我们看看存储Unicode码点的ASCII“编码”方案。规则非常简单:

  • 码点从U+0000到U+007F存储在一个单字节中
  • 码点高于U+0080的被丢弃,再也看不到了

很简单,对吧?

如你所见,ASCII不适合存储Unicode——事实上,它完全忽略了大多数Unicode码点。如果你有一个Unicode文档,并将其保存为ASCII,那么所有特殊字符都会消失。你常会在一些文本编辑器中看到这样的警告,当你将Unicode数据保存到原本以ASCII保存的文件中时。

但这个例子是有意义的。编码是一种将想法转换为数据的系统。在这种情况下,这种转换可以礼貌地称为“有损”。

我用记事本(可以读写Unicode)和 Programmer’s Notepad一个十六进制编辑器进行了Unicode实验。我想看看记事本保存的原始字节。自己试试这些例子:

  • 打开记事本并输入“Hello”
  • 分别将文件保存为ANSI、Unicode、Unicode Big Endian、UTF-8
  • 用程序员记事本打开文件,执行View > View Hex

All about ASCII

让我们在记事本中写“Hello”,保存为ANSI(ASCII)并在十六进制编辑器中打开。它看起来像这样:

Byte:     48 65 6C 6C 6F
Letter:   H  e  l  l  o

ASCII很重要,因为许多工具和通信协议只接受ASCII字符。这是文本普遍接受的最低标准。由于其普遍接受性,一些Unicode编码会将码点转换为一系列ASCII字符,以便它们能够无问题地传输。

现在,在上面的例子中,我们知道数据是文本,因为我们写的。如果我们随机找到这个文件,我们可以根据其内容 假设 猜测它是ASCII文本,但就我们所知,它可能是一个账号或其他数据,只是碰巧在ASCII中看起来像“Hello”。

通常,我们可以根据某些头部或“魔术数字”(Magic Numbers,即特殊字符序列)出现在特定位置来猜测数据的类型。但你不能百分百确定,有时可能会猜错。

不相信我?好吧,做以下操作:

  • 打开记事本
  • 写入“this program can break”
  • 将文件保存为“blah.txt”
  • 在记事本中打开该文件

哇……哦……发生了什么?我将其留给读者作为练习。

UCS-2 / UTF-16

这是我最初听到“Unicode”时想到的编码方式——每个字符存储为2字节(多么浪费!)。在基本层面上,它可以处理码点0x0000到0xFFFF,也就是0-65535,对我们人类来说是0到65535。而且65535个字符对任何人来说应该都足够了(有办法存储大于65535的码点,但详情请阅读规范)。

多字节存储数据引出了我最喜欢的难题:字节顺序!有些计算机先存储小端字节,有些则先存储大端字节。

为了解决这个问题,我们可以这样做:

  • 选项1: 选择一种约定 它规定所有文本数据必须是大端或小端。这不会发生——位于错误决定一侧的计算机每次打开文件时都会效率低下,因为它们无法将其转换为另一种字节顺序。
  • 选项2: 所有人都同意使用字节顺序标记(BOM), 每个文件顶部的一个头部。如果你打开一个文件,发现BOM是反的,这意味着它是用不同的字节顺序编码的,需要转换。

解决方案是BOM头部:UCS-2编码可以将码点U+FEFF写入文件头部。如果你打开一个UCS-2字符串并看到FEFF,说明数据字节顺序正确,可以直接使用。如果你看到FFFE,说明数据来自另一种类型的机器,需要转换为你的架构。这涉及交换文件中的每一个字节。

但不幸的是,事情没那么简单。BOM实际上是一个有效的Unicode字符——如果有人发送了一个没有头部的文件,而那个字符实际上是文件的一部分呢?

这是Unicode中的一个开放问题。建议是除了头部外避免使用U+FEFF,而使用替代字符(有等效的字符)。

这引出了设计观察#2: 多字节数据将有 字节顺序问题!

ASCII从来不用担心字节顺序——每个字符是一个字节,不会被误解。但实际情况是,如果你在文件开头看到0xFEFF或0xFFEE字节,这很可能是Unicode文本文件中的BOM。这很可能表明字节顺序。大概吧。

(题外话:UCS-2将数据存储为平坦的16位块。UTF-16允许最多20位分布在两个16位字符之间,称为代理对。代理对中的每个字符本身是无效的Unicode字符,但组合在一起可以提取出有效的字符。)

UCS-2 Example

在记事本中输入“Hello”并保存为Unicode(小端序UCS-2是Windows的原生格式):

Hello-little-endian:

FF FE  4800 6500 6C00 6c00 6F00
header H    e    l    l    o

再保存为Unicode Big Endian,你会得到:

Hello-big-endian:

FE FF  0048 0065 006C 006C 006F
header H    e    l    l    o

观察

  • 头部BOM(U+FEFF)按预期显示:小端为FF FE,大端为FEFF
  • 字母无论何种情况都使用2字节:“H”在ASCII中是0x48,在UCS-2中是0x0048
  • 编码很简单。将码点的十六进制写出为2字节。无需额外处理。
  • 编码过于简单。它对于不使用高阶字节的纯ASCII文本浪费了空间。而ASCII文本非常常见。
  • 编码插入了空字节(0x00),这可能有问题。老式ASCII程序可能会在遇到空字节时认为Unicode字符串结束了。在小端机器上,一次读取一个字节,你会先读到H(H=0x4800),然后遇到空字节停止。在大端机器上,你会先遇到空字节(H是0x0048),甚至在ASCII中都看不到H。这不好。

设计观察#3:考虑向后兼容性。旧程序如何读取新数据?忽略新数据是好的。在新数据上崩溃是坏的。

UTF-8

UCS-2 / UTF-16简单明了,但确实浪费了一些位。它不仅让ASCII大小翻倍,而且由于空字符,转换后的ASCII甚至可能无法阅读。

于是UTF-8登场了。它的目标是尽可能用单字节编码Unicode字符(如ASCII),并且不通过空字符破坏ASCII应用程序。它是XML的默认编码。

请阅读UTF-8规范了解更多细节,但从高层次来看:

  • 码点0 – 007F存储为常规的单字节ASCII。
  • 码点0080及以上被转换为二进制并存储(编码)在一系列字节中。
  • 第一个“计数字节”表明了码点所需的字节数,包括计数字节本身。这些字节以11..0开头:

    110xxxxx(开头的“11”表示序列中有2个字节,包括“计数字节”)

    1110xxxx(1110 -> 序列中有3个字节)

    11110xxx(11110 -> 序列中有4个字节)

  • 以10...开头的字节是“数据”字节,包含码点的信息。一个2字节的例子如下:

    110xxxxx 10xxxxxx

这意味着序列中有2个字节。X表示码点的二进制值,它需要塞进剩下的位中。

关于UTF-8的观察

  • No null bytes. All ASCII characters (0-127) are the same. Non-ASCII characters all start with “1” as the highest bit.
  • ASCII text is stored identically and efficiently.
  • Unicode characters start with “1” as the high bit, and can be ignored by ASCII-only programs (however, they may be discarded in some cases! See UTF-7 for more details).
  • There is a time-space tradeoff. There is processing to be done on every Unicode character, but this is a reasonable tradeoff.

设计原则#4

  • UTF-8 addresses the 80% case well (ASCII), while making the other cases possible (Unicode). UCS-2 addresses all cases equally, but is inefficient in the 80% case for solve for the 99% case. But UCS-2 is less processing-intensive than UTF-8, which requires bit manipulation on all Unicode characters.
  • Why does XML store data in UTF-8 instead of UCS-2? Is space or processing power more important when reading XML documents?
  • Why does Windows XP store strings as UCS-2 natively? Is space or processing power more important for the OS internals?

无论如何,UTF-8仍然需要一个头部来指示文本是如何编码的。否则,它可能被解释为纯ASCII,并使用某些代码页来处理大于127的值。它仍然使用U+FEFF码点作为BOM,但BOM本身是用UTF-8编码的(很聪明,对吧?)。

UTF-8 示例

Hello-UTF-8:

EF BB BF 48 65 6C 6C 6F
header   H  e  l  l  o

同样,ASCII文本在UTF-8中不会被改变。随意使用charmap复制一些Unicode字符,看看它们在UTF-8中是如何存储的。或者,你可以 在线实验.

UTF-7

虽然UTF-8对ASCII很好,但它仍然将Unicode数据存储为设置了高位(high-bit)的非ASCII字符。一些电子邮件协议不允许非ASCII值,因此UTF-8数据无法正确发送。能够处理高位中任意数据的系统称为“8位清洁”;要求数据值在0-127之间的系统(如SMTP)则不是。那么,我们如何通过它们发送Unicode数据呢?

于是有了UTF-7。其目标是用7位(0-127)编码Unicode数据,与ASCII兼容。UTF-7的工作原理如下:

  • Codepoints in the ASCII range are stored as ASCII, except for certain symbols (+, -) that have special meaning
  • Codepoints above ASCII are converted to binary, and stored in base64 encoding (stores binary information in ASCII)

如何判断哪些ASCII字母是真正的ASCII,哪些是base64编码的?很简单。在特殊符号“+”和“-”之间的ASCII字符被认为是base64编码的。

“-”充当转义后缀字符。如果它跟在某个字符后面,则该项被按字面解释。所以,“+-”被解释为“+”,不带任何特殊编码。这就是在UTF-7中存储实际的“+”符号的方法。

UTF-7 示例

Wikipedia上有一些UTF-7的例子,因为记事本无法保存为UTF-7。

“Hello”与ASCII相同——我们使用了所有ASCII字符,没有特殊符号:

Byte:     48 65 6C 6C 6F
Letter:   H  e  l  l  o

“£1”(1英镑)变成:

+AKM-1

字符“+AKM-”表示AKM应该被base64解码并转换为一个码点,映射到0x00A3,即英镑符号。“1”保持不变,因为它是一个ASCII字符。

UTF相当聪明,对吧?它本质上是一种Unicode到ASCII的转换,会移除所有最高位被设置的字符。大多数ASCII字符看起来相同,除了需要转义的特殊字符(-和+)。

总结一下——我学到了什么

我还是个新手,但已经学到了一些关于Unicode的知识:

  • Unicode does not mean 2 bytes. Unicode defines code points that can be stored in many different ways (UCS-2, UTF-8, UTF-7, etc.). Encodings vary in simplicity and efficiency.
  • Unicode has more than 65,535 (16 bits) worth of characters. Encodings can specify more characters, but the first 65535 cover most of the common languages.
  • You need to know the encoding to correctly read a file. You can often 猜测 that a file is Unicode based on the Byte Order Mark (BOM), but confusion can still arise unless you know the exact encoding. Even text that looks like ASCII could actually be encoded with UTF-7; you just don’t know.

Unicode是一个有趣的研究对象。它让我意识到设计中的权衡,以及将核心思想与用于保存它的编码分离的重要性。

本系列其他文章

  1. 数制(Number Systems)与进制(Bases)
  2. GUID 快速指南
  3. 理解 Quake 的快速平方根倒数(Fast Inverse Square Root)算法
  4. 计算机网络简明入门
  5. 使用异或(XOR)交换两个变量
  6. 理解大端(Big Endian)与小端(Little Endian)字节序
  7. Unicode 与你
  8. 关于二进制文件格式的一点小曲
  9. 排序算法(Sorting Algorithms)

加入 45 万月度读者

喜欢这篇文章?还有更多内容能帮你建立持久、直观的数学理解。加入通讯以获取额外内容和最新更新。