Unicode 十六进制码点范围UTF-8 二进制
0000 0000 - 0000 007F0xxxxxxx
0000 0080 - 0000 07FF110xxxxx 10xxxxxx
0000 0800 - 0000 FFFF1110xxxx 10xxxxxx 10xxxxxx
0001 0000 - 0010 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

2000-206F:常用标点


https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/

映射

在 Unicode 中,一个字母映射到称为码点 (code point)的东西。这个是一个理论概念,这个码点如何在内存或者在磁盘上表示,则完全是一回事。

在 go 中码点可以称为 rune(符文)

这个柏拉图式的 A 不同于 B,也不同于 a,但与 A、A 和 A 是相同的。认为 Times New Roman 字体中的 A 与 Helvetica 字体中的 A 是同一个字符,但与小写字母“a”不同,这一观点似乎并无太大争议,但在某些语言中,仅仅确定一个字母是什么就可能引发争议。德语字母ß是一个真正的字母,还是仅仅 ss 的一种花式写法?如果一个字母在词尾形状发生变化,那算不算另一个字母?希伯来语认为是,阿拉伯语则认为不是。无论如何,Unicode 联盟的聪明人们在过去十年左右的时间里一直在解决这些问题,伴随着大量高度政治化的辩论,而你已经无需为此担忧。他们已经把所有问题都搞清楚了。

柏拉图式 platonic: 指的是某个事物最完美、最本质的形态或概念。

Unicode 联盟为每种字母表中的每一个柏拉图式字母分配了一个神奇数字,其书写形式如下:U+0639。这个神奇数字被称为码点。U+ 代表“Unicode”,数字部分采用十六进制表示。U+0639 对应阿拉伯字母 Ain,而英文字母 A 则是 U+0041。

Unicode 对可定义的字母数量没有实际限制,事实上已经超过了 65,536 个,因此并非每个 Unicode 字母都能真正压缩到两个字节,但这本来就是个误解。Unicode 标准定义的最大码点是 U+10FFFF

Hello

在 Unicode 中,这对应以下五个码点

U+0048 U+0065 U+006C U+006C U+006F

实际上就是数字。我们还没有讨论如何将其存储在内存中或如何在电子邮件中表示。

编码

把每个数字用两个字节存储可以吗?大端还是小端呢?很遗憾,Unicode 已经有了两种存储方式,因此,人们被迫想出了一个奇怪的规定,在每个 Unicode 字符串的开头存储一个 FE FF,这是顺序标记(对应不同的就是 FF FE)。但是并不是所有的 Unicode 字符串开头都有这串标记

有一段时间,这似乎已经足够好了,但程序员们开始抱怨。“看看这些多余的零!”他们说,因为他们是美国人,主要处理英语文本,而这些文本很少用到 U+00FF 以上的码点。而且,这些加州自由派嬉皮士还想着要节约资源(嗤之以鼻)。要是德州人,才不会在乎多消耗一倍的字节。但这些加州软蛋无法接受字符串存储空间翻倍的想法,更何况,已经有大量文档使用了各种 ANSI 和 DBCS 字符集,谁来转换它们呢?我吗?仅因这一点,多数人决定多年忽视 Unicode,而在此期间情况却变得更糟。

于是,UTF-8 这一精妙概念应运而生。UTF-8 是另一种将 Unicode 代码点(那些神奇的 U+ 数字)以 8 位字节形式存储在内存中的系统。在 UTF-8 中,0 到 127 的每个代码点仅占用一个字节;而 128 及以上的代码点则需使用 2 个、3 个,甚至最多 6 个字节来存储。

这有一个巧妙的效果,即英语文本在 UTF-8 编码下看起来与 ASCII 编码时完全一致,因此美国人甚至察觉不到任何异常。只有世界其他地区的人们需要费些周折。具体来说,“Hello” 对应的 Unicode 码点是 U+0048 U+0065 U+006C U+006C U+006F,存储为 48 65 6C 6C 6F,瞧!这与 ASCII、ANSI 以及地球上所有 OEM 字符集中的存储方式完全相同。不过,如果你大胆地使用带重音符号的字母、希腊字母或克林贡字母,就需要用多个字节来存储一个码点,但美国人永远不会注意到这一点。(UTF-8 还有个优点,那些无知的老旧字符串处理代码若想用单个 0 字节作为空终止符,也不会截断字符串)。

  • 传统的以双字节存储的方法被称为 UCS-2(两个字节)或者 UTF-16(虽然还需要指定大小端才行)
  • 还有流行的 UTF-8,在英文文本无视 ASCII 之外字符的存在的时候表现的完全一致

实际上,Unicode 还有其他多种编码方式。有一种叫做 UTF-7 的编码,它与 UTF-8 非常相似,但能确保最高位始终为零。这样一来,即便你需要通过某种严苛到认为 7 位就足够了的邮件系统传递 Unicode 数据,它也能毫发无损地挤过去。还有 UCS-4,它将每个代码点存储在 4 个字节中,这样每个代码点都能以相同的字节数存储,这固然是个不错的特性,但天哪,就连得克萨斯人也不会如此大胆地浪费这么多内存。

存在着数百种传统编码,它们只能正确存储部分代码点,而将所有其他代码点变成问号。UTF-7、8、16 和 32 都具有能正确存储任何代码点的优良特性。

UTF-8

  • 1 字节: 0xxxxxxx (码点范围: U+0000 - U+007F)
  • 2 字节: 110xxxxx 10xxxxxx (码点范围: U+0080 - U+07FF)
  • 3 字节: 1110xxxx 10xxxxxx 10xxxxxx (码点范围: U+0800 - U+FFFF)
  • 4 字节: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (码点范围: U+10000 - U+10FFFF)

首先确定码点需要多少个字节,第一个字节有若干个 1 表示这个字符编码总占用的字节数,后面的均以 10 开头,将码点的二进制位,从低位到高位依次填入上面

  • 优点
    • 对 ASCII 字符集完全兼容,处理英文为主的文本时空间效率高。
    • 自同步:由于起始字节和延续字节的模式不同,即使数据流中出现错误,解析器也能较容易地重新定位到下一个字符的开始。
    • 不需要字节序标记 (BOM),虽然可以有,但通常不推荐。
  • 缺点
    • 对于中日韩等东亚字符,通常需要 3 个字节,比 UTF-16(通常 2 字节)占用更多空间。
    • 变长编码使得随机访问特定字符不如定长编码直接。

当识别到无效的序列的时候,就像前面是一个 11111 开头的,或者 1110 后面跟 1110 之类的,会确定从当前位置开始,尝试消耗掉构成一个“最长无效子序列 (maximal subpart of an ill-formed sequence)”的所有字节,并将这整个子序列替换为一个 U+FFFD

可能是一个单独的字节,如中间插入了一个 \xf2,因为后面没有以 10 开头的了,所以这个字节是非法的。

可能是多个字节:

  1. 跟了部分正确但是数量不足
  2. 过长的编码,如 \xC0\xAF (0b1100000010101111),可以用一个的硬是要用两个,所以不行
  3. 编码了代理对码点
  4. 编码超出最大码点

UTF-16

BMP 是 Unicode 的第一个平面,包含了从 U+0000U+FFFF 的码点,这里面有我们日常使用的大部分字符,比如常见的汉字、拉丁字母、数字等。Unicode 标准在 BMP 内部“挖出来”了一段特殊的码点范围专门用于代理对机制。这个范围是 U+D800U+DFFF。BMP 之外的字符 (U+10000 至 U+10FFFF,也称为辅助平面字符)

  • 高代理项 (High Surrogates): U+D800U+DBFF (共 0x4001024 个码点)
  • 低代理项 (Low Surrogates): U+DC00U+DFFF (共 0x4001024 个码点)

如果在 BMP 里面的。就直接放进去没啥好讲的

但要编码一个在 U+10000U+10FFFF 范围内的码点 (我们称之为 C):

  1. 先从码点 C 中减去 0x10000,得到一个 20 位的值(范围是 0x000000xFFFFF)。我们称这个值为 C'
  2. 将这个 20 位的 C' 分为两部分:
    • 高 10 位 (H_bits)
    • 低 10 位 (L_bits)
  3. 高代理项 (W1) = 0xD800 + H_bits
  4. 低代理项 (W2) = 0xDC00 + L_bits

这样,W1 会落在 U+D800U+DBFF 范围内,W2 会落在 U+DC00U+DFFF 范围内。这一对 (W1, W2) 就共同表示了原始的辅助平面码点 C

UTF-32

直接放,没打过这么富裕的仗

UTF-7

UTF-7 是一种为了在严格限制为 7 位 ASCII 的传输环境(如一些老旧的电子邮件系统)中传递 Unicode 数据而设计的编码。现在它已经基本被废弃,并且由于安全风险,强烈不推荐使用

  1. 直接编码的字符 (Directly Encoded Characters):
    • UTF-7 定义了一个“安全”的 ASCII 字符集,这些字符可以直接在 UTF-7 中使用其原始的 ASCII 形式。这个集合大致包括:
      • 大写和小写英文字母 (A-Z, a-z)
      • 数字 (0-9)
      • 一些常见的标点符号:' ( ) , . / : ?
    • 空格、制表符、回车、换行这些常见的空白字符通常也直接编码。
    • 重要:像 + < > & " \ = @ [ ] { } _ $ # % ^ * | ~ ; 等 ASCII 字符不属于这个直接编码的安全集,如果它们需要被表示,则必须进入下面的编码模式。
  2. 编码模式 (Encoded Unicode Characters):
    • 当遇到不属于上述安全集的字符时(包括所有非 ASCII 的 Unicode 字符,以及那些“不安全”的 ASCII 标点),UTF-7 会切换到一种特殊的编码模式。
    • 这个模式以一个加号 + 字符开始。
    • 紧随 + 之后的是目标 Unicode 字符序列(这些字符首先被转换为 UTF-16 Big Endian 字节流),然后这个字节流再通过一种修改版的 Base64 进行编码。
      • 为什么是 UTF-16BE? 因为 UTF-16 是早期 Unicode 实现中常见的内部表示形式,BE(Big Endian,大端序)是网络字节序的传统。
      • 修改版 Base64:标准的 Base64 使用的字符集是 A-Z, a-z, 0-9, +, / 以及 = 作为填充符。UTF-7 的 Base64 不能直接使用 /,因为它在某些文件名或 URL 中有特殊含义,所以 UTF-7 的 Base64 字符集中,标准 Base64 的 / 被替换为 , (逗号)。填充符 = 在 UTF-7 的 Base64 中是可选的,通常会省略。
    • 编码块以一个减号 - 字符结束,用于切换回直接编码模式。
      • 但是,如果 + 编码块之后紧跟着另一个可以直接编码的 Base64 字符集中的字符(即 A-Z, a-z, 0-9, ,),则这个结尾的 - 可以省略,编码器可以直接开始下一个字符的 Base64 编码。这是一个优化,但有时会使解码稍微复杂。
    • 如果要表示 + 字符本身,则编码为 +-

编码示例

  • “Hello”:
    • 这些都是安全 ASCII 字符,所以 UTF-7 编码就是 Hello
  • “£1” (英镑符号 U+00A3, 数字 1):
    • ‘1’ 是安全字符。
    • ’£’ (U+00A3) 不是安全字符。
      1. ’£’ (U+00A3) 的 UTF-16BE 是 00 A3 (十六进制)。
      2. 00 A3 进行修改版 Base64 编码:
        • 00000000 10100011 分为 6 位一组 000000 001010 001100 (如果需要,末尾补 0 凑齐 6 位,但这里正好)
        • 000000 (0) ‘A’
        • 001010 (10) ‘K’
        • 001100 (12) ‘M’
        • 所以 Base64 结果是 “AKM”。
    • 因此,“£1” 的 UTF-7 编码是 +AKM-1
  • ” 中文 +Test” (中: U+4E2D, 文: U+6587):
    • “Test” 是安全字符。
    • ” 中 ” (U+4E2D) UTF-16BE: 4E 2D
    • ” 文 ” (U+6587) UTF-16BE: 65 87
    • 4E 2D 65 87 进行修改版 Base64 编码:
      • 01001110 00101101 01100101 10000111
      • 010011 (19) ‘T’
      • 100010 (34) ‘i’
      • 110101 (53) ‘1’ (注意 Base64 字符集)
      • 100101 (37) ‘l’
      • 100001 (33) ‘h’
      • 110000 (48) ‘w’ (最后不足 6 位,实际编码时会处理,这里简化了 Base64 末尾处理细节,实际可能是 TnUwZg)
      • 我们用一个在线转换器得到 ” 中文 ” 的 UTF-16BE 4E 2D 65 87 的 UTF-7 Base64 部分是 TnUwZg
    • + 字符本身需要编码成 +-
    • 所以,” 中文 +Test” 的 UTF-7 编码是 +TnUwZg-+-Test