Windows
 
讨论UTF-8的码位规则
acmilan 2015-10-2 05:05:04
UTF-8是一种将Unicode随ASCII字节流输送的编码形式,它兼容ASCII,并且不需要特殊的算法便可处理。唯一不方便的地方是它是变长编码,因此截断字符串时要小心,不然在边界处可能会产生无效字符。由于设计问题,它存在很多无效码位,我们通常可以按照这些无效码位来识别UTF-8编码。

一、全能型UTF-8

现在的Unicode字符集被称为UCS-4,最初UCS-4具有31位的编码,因此UTF-8也是能承载31位编码。这里我称这种UTF-8为全能型UTF-8。全能型UTF-8的二进制定义如下:
0-7F (xxxxxxx) => 00-7F (0xxxxxxx)
80-7FF (xxx xx'xxxxxx) => C0-DF 80-BF (110xxxxx 10xxxxxx)
800-FFFF (xxxx'xxxx xx'xxxxxx) => E0-EF 80-BF 80-BF (1110xxxx 10xxxxxx 10xxxxxx)
10000-1FFFFF (xxx'xx xxxx'xxxx xx'xxxxxx) => F0-F7 80-BF 80-BF 80-BF (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
200000-3FFFFFF (xx' xxxxxx'xx xxxx'xxxx xx'xxxxxx) => F8-FB 80-BF 80-BF 80-BF 80-BF (111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx)
4000000-7FFFFFFF (x'xxxxxx' xxxxxx'xx xxxx'xxxx xx'xxxxxx) => FC-FD 80-BF 80-BF 80-BF 80-BF 80-BF (1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx)

也就是说,在全能型UTF-8中,字节范围不同,则功能不同,可概括如下:
字节如果是00-7F,则是ASCII
字节如果是80-BF,则是尾字节
字节如果是E0-FD,则是首字节,C0-DF跟一个尾字节,E0-EF跟两个,F0-F7跟三个,F8-FB跟四个,FC-FD跟五个
字节FE-FF,是未定义的

现在可初步总结出全能型UTF-8的基本规则:
单独的字节80-BF,是无效的
首字节C0-DF跟随1个80-BF字节,才可能是有效的
首字节E0-EF跟随2个80-BF字节,才可能是有效的
首字节F0-F7跟随3个80-BF字节,才可能是有效的
首字节F8-FB跟随4个80-BF字节,才可能是有效的
首字节FC-FD跟随5个80-BF字节,才可能是有效的
字节FE-FF,是无效的


二、Unicode码位大缩水的

虽然UCS-4设计上有31位,但是最后定下来的码位只剩下21位,值只能取000000-10FFFF。这样以来,最明显的是我们不再需要五字节或六字节编码了,于是F8-FD也成为了未定义字节。

除此之外,四字节编码也有所缩水。由于码位只能取000000-10FFFF而不是1FFFFFF,也就是只能取最高100'001111'111111'111111,因此实际上四字节编码中,首字节也只用了F0-F4,而没有使用F5-F7,并且首字节是F4时,紧挨着的一个尾字节也只能使用80-8F,而不能使用90-BF。也就是说,最大的UTF-8码位是F4 8F BF BF。

由此可以总结出下面两条规则:
字节F5-FF,是无效的
双字节F490-F4BF,是无效的


三、现实的UTF-8

随着Unicode码位的大缩水,我们的UTF-8也随之大缩水,接下来我们要对全能型UTF-8定义做个大瘦身,瘦身后的UTF-8定义如下:
0-7F (xxxxxxx) => 00-7F (0xxxxxxx)
80-7FF (xxx xx'xxxxxx) => C0-DF 80-BF (110xxxxx 10xxxxxx)
800-FFFF (xxxx'xxxx xx'xxxxxx) => E0-EF 80-BF 80-BF (1110xxxx 10xxxxxx 10xxxxxx)
10000-FFFFF (0xx'xx xxxx'xxxx xx'xxxxxx) => F0-F3 80-BF 80-BF 80-BF (11110'0xx 10xxxxxx 10xxxxxx 10xxxxxx)
100000-10FFFF (100'00 xxxx'xxxx xx'xxxxxx) => F4 80-8F 80-BF 80-BF (11110'100 10'00xxxx 10xxxxxx 10xxxxxx)

在UTF-8中,字节范围不同,则功能不同,可概括如下:
字节如果是00-7F,则是ASCII
字节如果是80-BF,则是尾字节
字节如果是E0-FD,则是首字节,C0-DF跟一个尾字节,E0-EF跟两个,F0-F4跟三个
双字节F490-F4BF,是未定义的
字节F5-FF,是未定义的

UTF-8的基本规则:
单独的字节80-BF,是无效的
首字节C0-DF跟随1个80-BF字节,才可能是有效的
首字节E0-EF跟随2个80-BF字节,才可能是有效的
首字节F0-F7跟随3个80-BF字节,才可能是有效的
双字节F490-F4BF,是无效的
字节F5-FF,是无效的


四、超长编码的问题

由于UTF-8并没有像UTF-16那样从算法上硬性规定某个码位一定按照某个字节长度编码,实际上有些人就可以动歪脑筋了。举例说明,Unicode码位0(空字符)既可以按照00来编码,又可以按照C0 80来编码,还可以按照E0 80 80来编码,还可以按照F0 80 80 80来编码,这就乱套了。一个根本不存在字节00的UTF-8字符串转换为UTF-16或UTF-32字符串之后可能会出现多个0,如果用这种方法藏一些危险代码,如rm -rf /之类的,后果可想而知。所以为了安全性的原因,大多数系统在转码时,都会将超长UTF-8编码视为无效编码。

超长UTF-8编码在现实的UTF-8中一般有这样几种表现形式:
0-7F的码位用二字节编码(110'0000x 10xxxxxx,即C0-C1 80-BF)
0-7FF的码位用三字节编码(1110'0000 10'0xxxxx 10xxxxxx,即E0 80-9F 80-BF)
0-FFFF的码位用四字节编码(11110'000 10'00xxxx 10xxxxxx 10xxxxxx,即F0 80-8F 80-BF 80-BF)

和第二节中的情况类似,我们也可以总结出以下规则,用以下的规则,就可以方便地识别超长编码,避免安全性问题。
字节C0-C1,是无效的
双字节E080-E09F,是无效的
双字节F080-F08F,是无效的


在某些应用场合下,超长编码有另外的应用。比如C字符串中是不允许出现等于0的空字符的,否则字符串会被截断,而另外一些语言则支持空字符。因此在交换字符串时,常将空字符转化为C0 80用以避免字符串被截断。然而这种做法也有一定的不安全因素,假如客户程序不做任何处理就将字符串转换为UTF-16,仍会导致字符串被截断。

五、扩展平面——四字节与六字节之争

UTF-16在远古的UCS-2时代(Unicode还是16位的时候)就产生了,那时只能支持0000-FFFF的码位。后来UCS-4加入了16个扩展平面,码位扩充到000000-10FFFF。其中小于等于00FFFF的字符仍按原来的方法表示,大于00FFFF的字符则用代理对表示。代理对首字范围是D800-DBFF (110110xx xxxxxxxx),第二字范围是DC00-DFFF (110111xx xxxxxxxx),共20位的有效位,扩展平面的字符减去010000后范围为000000-0FFFFF也刚好20位。

现在大多数软件都会先将代理对转换为大于00FFFF的扩展平面码位,然后再按四字节编码UTF-8,表现为F0-F4 80-BF 80-BF 80-BF;但是某些软件由于历史原因不能正确识别代理对,或者为了简化算法的原因,它们将代理对中的两个字各转换为两个三字节编码,表现为ED A0-AF 80-BF / ED B0-BF 80-BF。和第四节的情况一样,也乱套了。现在前者被称为标准UTF-8,后者被称为兼容UTF-8CESU-8

一般来说,标准UTF-8中不可能出现六字节形式,因此大多数系统都会将这种编码视为无效编码,即认为EDA0-EDBF为无效双字节。对于标准UTF-8文本,附加规则如下:
双字节EDA0-EDBF,是无效的

但是,如果需要读取含有六字节形式的兼容UTF-8文本(CESU-8文本),就不应该将EDA0-EDBF视为无效双字节,而是将它视为有效字节。反之,需要将F0以上的字节都视为无效字节,因为文本中并不需要含有四字节形式。另外,UTF-16代理对的规则也适用于CESU-8。对于兼容UTF-8文本(CESU-8文本),附加规则如下:
字节F0-FF,是无效的
双字节EDA0-EDAF后面没有跟80-BF、EDB0-EDBF、80-BF,是无效的
单独的双字节EDB0-EDBF,是无效的

具体需要支持那种形式,要看具体需求。一般来说标准UTF-8兼容性好,而兼容UTF-8(CESU-8)算法比较简单。

五、UTF-8编码规则大总结

标准UTF-8文本的规则:
单独的字节80-BF,是无效的
首字节C2-DF跟随1个80-BF字节,才可能是有效的
首字节E0-EF跟随2个80-BF字节,才可能是有效的
首字节F0-F4跟随3个80-BF字节,才可能是有效的
字节C0-C1,是无效的(超长双字节)
字节F5-FF,是无效的(F5-F7超码位、F8-FD被废弃、FE-FF未定义)
双字节E080-E09F,是无效的(超长三字节)
双字节EDA0-EDBF,是无效的(UTF-16代理对)
双字节F080-F08F,是无效的(超长四字节)
双字节F490-F4BF,是无效的(超码位)


兼容UTF-8文本(CESU-8文本)的规则:
单独的字节80-BF,是无效的
首字节C2-DF跟随1个80-BF字节,才可能是有效的
首字节E0-EF跟随2个80-BF字节,才可能是有效的
字节C0-C1,是无效的(超长双字节)
字节F0-FF,是无效的(F0-F7未使用,F8-FD被废弃,FE-FF未定义)
双字节E080-E09F,是无效的(超长三字节)

双字节EDA0-EDAF后面没有跟80-BF、EDB0-EDBF、80-BF,是无效的(代理对不匹配)
单独的双字节EDB0-EDBF,是无效的(代理对不匹配)

(完)

[修改于 4 年前 - 2015-10-02 22:04:09]

2015-10-2 05:37:07
acmilan(作者)
1楼
一个尾字节范围:8-B
三个多字节模式:C-D E F
两个大洞:C0-C1 F5-FF
四个小洞:E0[8-9] ED[A-B] F0[8] F4[9-B]
2楼
C#的字符截断很少出现问题,只有在一次字符串里有个很特殊的 占两个char大小的特殊字符 如果把它拆开会出现问题
acmilan(作者)
3楼
引用 张静茹:
C#的字符截断很少出现问题,只有在一次字符串里有个很特殊的 占两个char大小的特殊字符 如果把它拆开会出现问题
如果全是emoji的话,C#的字符串截断就会出很大问题。
acmilan(作者)
4楼
UTF-16的码位规则非常简单——
1.如果D800-DBFF后边没有跟DC00-DFFF,是无效的
2.单独出现的DC00-DFFF,是无效的
由于UTF-16码位空洞特别小,因此想通过码位空洞识别UTF-16是根本不可能的。识别UTF-16只能是通过BOM。
所以实际上UTF-16是最接近二进制数据的一种编码,也是空间利用率最高的一种编码。

[修改于 4 年前 - 2015-10-02 15:04:11]

acmilan(作者)
5楼
对于无效字符,一般将其替换为\uFFFD,它是一个带问号的菱形图案,可作为Unicode默认替换字符。
\uFFFD的UTF-8编码是EF BF BD,我们常见的『锟斤拷』就是EF BF BD EF BF BD按照GBK码解码的结果。

[修改于 4 年前 - 2015-10-02 13:35:58]

2015-11-15 21:27:12
acmilan(作者)
6楼
UTF-8码位图解(t表示尾字节,1表示有一个尾字节,2表示有2个尾字节,3表示有3个尾字节,空白表示非法字节,下边的注解表示下一个尾字节的合法取值范围)
255199
acmilan(作者)
7楼
如果仅仅是进行字处理而不是转换,实际上这时忽略四个小洞并不会造成什么后果。直接按期望尾字节数读取即可。
00-C1:期望0尾字节。
C2-DF:期望1尾字节。
E0-EF:期望2尾字节。
F0-F4:期望3尾字节。
F5-FF:期望0尾字节。

想参与大家的讨论?现在就 登录 或者 注册

{{submitted?"":"投诉"}}
请选择违规类型:
{{reason.description}}
支持的图片格式:jpg, jpeg, png