Unicode 十六进制码点范围 | UTF-8 二进制 |
---|---|
0000 0000 - 0000 007F | 0xxxxxxx |
0000 0080 - 0000 07FF | 110xxxxx 10xxxxxx |
0000 0800 - 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000 - 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
2000-206F:常用标点
映射
在 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 开头的了,所以这个字节是非法的。
可能是多个字节:
- 跟了部分正确但是数量不足
- 过长的编码,如
\xC0\xAF
(0b1100000010101111),可以用一个的硬是要用两个,所以不行 - 编码了代理对码点
- 编码超出最大码点
UTF-16
BMP 是 Unicode 的第一个平面,包含了从 U+0000
到 U+FFFF
的码点,这里面有我们日常使用的大部分字符,比如常见的汉字、拉丁字母、数字等。Unicode 标准在 BMP 内部“挖出来”了一段特殊的码点范围专门用于代理对机制。这个范围是 U+D800
至 U+DFFF
。BMP 之外的字符 (U+10000 至 U+10FFFF,也称为辅助平面字符)
- 高代理项 (High Surrogates):
U+D800
至U+DBFF
(共0x400
或1024
个码点) - 低代理项 (Low Surrogates):
U+DC00
至U+DFFF
(共0x400
或1024
个码点)
如果在 BMP 里面的。就直接放进去没啥好讲的
但要编码一个在 U+10000
到 U+10FFFF
范围内的码点 (我们称之为 C
):
- 先从码点
C
中减去0x10000
,得到一个 20 位的值(范围是0x00000
到0xFFFFF
)。我们称这个值为C'
。 - 将这个 20 位的
C'
分为两部分:- 高 10 位 (
H_bits
) - 低 10 位 (
L_bits
)
- 高 10 位 (
- 高代理项 (
W1
) =0xD800 + H_bits
- 低代理项 (
W2
) =0xDC00 + L_bits
这样,W1
会落在 U+D800
至 U+DBFF
范围内,W2
会落在 U+DC00
至 U+DFFF
范围内。这一对 (W1, W2)
就共同表示了原始的辅助平面码点 C
。
UTF-32
直接放,没打过这么富裕的仗
UTF-7
UTF-7 是一种为了在严格限制为 7 位 ASCII 的传输环境(如一些老旧的电子邮件系统)中传递 Unicode 数据而设计的编码。现在它已经基本被废弃,并且由于安全风险,强烈不推荐使用
- 直接编码的字符 (Directly Encoded Characters):
- UTF-7 定义了一个“安全”的 ASCII 字符集,这些字符可以直接在 UTF-7 中使用其原始的 ASCII 形式。这个集合大致包括:
- 大写和小写英文字母 (A-Z, a-z)
- 数字 (0-9)
- 一些常见的标点符号:
' ( ) , . / : ?
- 空格、制表符、回车、换行这些常见的空白字符通常也直接编码。
- 重要:像
+
<
>
&
"
\
=
@
[
]
{
}
_
$
#
%
^
*
|
~
;
等 ASCII 字符不属于这个直接编码的安全集,如果它们需要被表示,则必须进入下面的编码模式。
- UTF-7 定义了一个“安全”的 ASCII 字符集,这些字符可以直接在 UTF-7 中使用其原始的 ASCII 形式。这个集合大致包括:
- 编码模式 (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
。
- 这些都是安全 ASCII 字符,所以 UTF-7 编码就是
- “£1” (英镑符号 U+00A3, 数字 1):
- ‘1’ 是安全字符。
- ’£’ (U+00A3) 不是安全字符。
- ’£’ (U+00A3) 的 UTF-16BE 是
00 A3
(十六进制)。 - 将
00 A3
进行修改版 Base64 编码:00000000 10100011
→ 分为 6 位一组000000
001010
001100
(如果需要,末尾补 0 凑齐 6 位,但这里正好)000000
(0) → ‘A’001010
(10) → ‘K’001100
(12) → ‘M’- 所以 Base64 结果是 “AKM”。
- ’£’ (U+00A3) 的 UTF-16BE 是
- 因此,“£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