暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Python出错信息中的“Non-BMP character”是个什么鬼?

语和言 2018-07-25
485

一、缘起


先不要管这是个什么鬼,这个鬼是探讨Unicode字符的存储编码的副产品。各位看官若有兴趣,请慢慢往下看。


通常情况下,我们碰到的字符对应的Unicode编码在0x0000至0xFFFF之间,字符的Unicode编码跟它在Unicode Big Endian编码格式保存的文件当中的编码是一致的。


例如,汉字“一”的Unicode编码是“4E00”,我们用Windows的记事本软件新建一个文件,写入汉字“一”(不带双引号哈),选择“unicode big endian”编码格式,保存为“一.txt”。然后我们用十六进制查看工具(例如EditPlus)打开这个文件,就可以看到,除了开头的BOM“FE FF”外,我们看到了“4E 00”这个符号串,恰好跟汉字“一”的Unicode编码一样。


但有的时候,我们会碰到一些字符的Unicode编码大于0xFFFF,这时候,字符在文件的存储编码跟它的Unicode编码就不一样了。它们究竟有什么样的关系呢?在此记录一下上网查到的资料。


注:BOM是Byte Order Mark的缩写,意思是“字节顺序标记”,它出现在文本文件头部,用两个字节来表示在Unicode编码标准中是采用unicode big endian编码格式还是unicode编码格式来保存数据的。unicode big endian编码格式的BOM是“FE FF”,unicode编码格式的BOM是“FF FE”。



二、Unicode简介


我们通常说的Unicode,正式名称是“UTF-16”,它在文件中用两个字节或者四个字节来存储一个字符。先介绍几个概念:


代码点

Unicode标准的本意很简单:希望给世界上每一种文字系统的每一个字符,都分配一个唯一的整数,这些整数叫做代码点(Code Points)。


代码空间

所有的代码点构成一个代码空间(Code Space),根据Unicode定义,总共有1,114,112个代码点,编号从0x000000到0x10FFFF。换句话说,如果每个代码点都能够代表一个有效字符的话,Unicode标准最多能够对1,114,112个字符进行编码,也就是说Unicode标准大概能表示110多万个字符。据说最新的Unicode7.0已经给超过11万个字符分配了代码点。


代码平面

Unicode标准把代码点分成了17个代码平面(Code Plane),编号为#0到#16。每个代码平面包含65,536(2^16)个代码点(17*65,536=1,114,112)。其中,Plane#0叫做基本多语言平面(Basic Multilingual Plane,BMP),其余平面叫做补充平面(Supplementary Planes)。Unicode7.0只使用了17个平面中的6个,并且给这6个平面起了名字:


Plane#0 BMP(Basic Multilingual Plane)

Plane#1 SMP(Supplementary Multilingual Plane)

Plane#2 SIP(Supplementary Ideographic Plane)

Plane#14 SSP(Supplementary Special-purpose Plane)

Plane#15 SPUA-A(Supplementary Private Use Area A)

Plane#16 SPUA-B(Supplementary Private Use Area B)


其中,基本多语言平面的码位范围为0x0000至0xFFFF,包含了最常见的字符,比如ASCII字符,汉字等。补充平面的码位范围为0x010000到0x10FFFF,其中补充平面2里面包含了基本多语言平面没有收录的一些汉字。例如,汉字“𥻗”就在这个平面。



三、基本多语言平面BMP简介


BMP是Unicode最重要的一个平面,它包含了大部分常用的字符。

0x00-0x7F:ASCII字符集,总共有128个字符,占据了BMP的前128个代码点。

0x80-0xFF:扩展SCII字符集,总共有128个字符,占据了BMP的第129-256个代码点

其中ASCII字符集和扩展SCII字符集合起来叫被称为ISO-8859-1字符集,该字符集向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号,欧洲很多国家的语言使用ISO-8859-1字符集。

0x4E00-0x9FFF:中日韩汉字区域,定义了两万多个汉字,其中前20,902个汉字(0x4E00-0x9FA5)是基本汉字区域,0x9FA6-0x9FEF这74个汉字是基本汉字补充区域。

0xD800-0xDFFF:这2048个码位区域的代码点没有分配给任何字符,它们有特殊的用处,被称为“Surrogate Code Points”,用来辅助表示Unicode编码位于补充平面内的那些字符。其中:

0xD800到0xDBFF:High-surrogate代码点,

0xDC00到0xDFFF:Low-surrogate代码点。

一个High-surrogate代码点和一个Low-surrogate代码点组成一个代理对(Surrogate Pair),可以在UTF-16里对补充平面内的字符进行编码,一共可以表示1024*1024+65,536=1,114,112个代码。



四、UTF-16编码简介


UTF-16是Unicode的一种文本保存的编码方式,它用两个字节来编码BMP里的代码点,用四个字节编码其余平面里的代码点。UTF-16编码在保存两个字节的Unicode字符时,要考虑字节序的问题,比如Windows的记事本,它要将文本保存为UTF-16编码的时候,就必须要选择“unicode”和“unicode big endian”当中的一个。UTF-16编码在保存四个字节的Unicode字符时,不考虑字节序问题,统统用类似“unicode big endian”的格式来存储。


由于BMP里只有65535个代码点,而两个字节恰好能表示65535个字符,所以BMP里面的Unicode字符直接把代码点转换成2个字节就可以了,保存成文件就是“unicode big endian”。


BMP之外的平面内的字符存储时要用四个字节,这需要先将代码点转化为一个代理对,然后再转为4个字节。假设要编码的补充平面内的代码点为X,具体的编码过程为:


X因为在补充平面,它的Unicode编码必定在0x010000到0x10FFFF之间,将X减去0x010000,得到的数在0x00000到0xFFFFF之间,正好可以用不超过20个二进制位来表示,我们就用20个二进制位,不足20位高位补0。然后将二十个二进制位分成两组,高位组含10个二进制位,低位组也含10个二进制位,高位组对应的数和0xD800相加,将低位组对应的数和0xDC00相加,得到的正好是一个代理对,也就是四个字节,将这四个字节写入文件就可以了。



五、举例


𥻗(chá)是《通用规范汉字表》中的第5989个汉字,它是Unicode字符集扩展B区的汉字,它在GB18030字符集中的编码是9731A435,其十六进制Unicode编码为25ED7。


由于𥻗的Unicode编码“25ED7”,大于0xFFFF,所以保存在文件中采用4个字节进行编码:

首先,0x25ED7减去0x10000,结果为0x15ED7,转化为20个二进制位:

0001 0101 1110 1101 0111。

将这二十位二进制数分成两组,十位一组,得到高位组和低位组:

高位组:0001010111

低位组:1011010111

变成16进制:

0x0057 和 0x02D7

高位组加上0xD800,得到UTF-16编码的前两个字节:0xD800 + 0x0057 = 0xD857。

低位组加上0xDC00,得到UTF-16编码的后两个字节:0xDC00 + 0x02D7 = 0xDED7。

所以,“𥻗”的UTF-16编码是:D8 57 DE D7


因此,在文件中以Unicode Big Endian编码存储字符“𥻗”的时候,除了开头的BOM外,我们会看到如下的四个字符:

D8 57 DE D7



六、心得


以前,听说Unicode用两个字节表示一个字符的编码,共有65535个字符。好像就相信了两个字节的说法。直到有一天,研究《通用规范汉字表》的时候,碰到第5989个汉字“𥻗”,发现它的Unicode编码是“25ED7”,懵了,怎么不是两个字节了?保存到文件的时候,更懵了,居然是四个字节“D8 57 DE D7”。一直想搞清楚为什么。今天总算明白通常说的Unicode仅仅指的是Unicode中的基本多语言平面,另外的平面还有字符,这些字符的Unicode编码是大于两个字节的,保存到文件就是四个字节。



七、扩展


汉字和汉字偏旁部首等在Unicode中的分布比较凌乱,为我们理解带来了困难。据不完全统计,大致包含如下几个区域:


┌──────┬────┬──────┐

│字符集      │字数    │Unicode 编码│

├──────┼────┼──────┤

│基本汉字    │20902字 │4E00-9FA5   │

├──────┼────┼──────┤

│基本汉字补充│74字    │9FA6-9FEF   │

├──────┼────┼──────┤

│扩展A       │6582字  │3400-4DB5   │

├──────┼────┼──────┤

│扩展B       │42711字 │20000-2A6D6 │

├──────┼────┼──────┤

│扩展C       │4149字  │2A700-2B734 │

├──────┼────┼──────┤

│扩展D       │222字   │2B740-2B81D │

├──────┼────┼──────┤

│扩展E       │5762字  │2B820-2CEA1 │

├──────┼────┼──────┤

│扩展F       │7473字  │2CEB0-2EBE0 │

├──────┼────┼──────┤

│康熙部首    │214字   │2F00-2FD5   │

├──────┼────┼──────┤

│部首扩展    │115字   │2E80-2EF3   │

├──────┼────┼──────┤

│兼容汉字    │477字   │F900-FAD9   │

├──────┼────┼──────┤

│兼容扩展    │542字   │2F800-2FA1D │

├──────┼────┼──────┤

│PUA(GBK)部件│81字    │E815-E86F   │

├──────┼────┼──────┤

│部件扩展    │452字   │E400-E5E8   │

├──────┼────┼──────┤

│PUA增补     │207字   │E600-E6CF   │

├──────┼────┼──────┤

│汉字笔画    │36字    │31C0-31E3   │

├──────┼────┼──────┤

│汉字结构    │12字    │2FF0-2FFB   │

├──────┼────┼──────┤

│汉语注音    │43字    │3105-312F   │

├──────┼────┼──────┤

│注音扩展    │22字    │31A0-31BA   │

├──────┼────┼──────┤

│〇          │1字     │3007        │

└──────┴────┴──────┘


对我们大多数人来说,掌握住基本汉字区域的20902个汉字也差不多够了。我们从可以从网上看到处理汉字的各种正则表达式,最常用是[\u4E00-\u9FA5],恰好对应Unicode的基本汉字区域,如果要处理全部中日韩汉字和偏旁部首标点符号等,可以用[\u2E80-\u9FF]来匹配。



八、什么鬼


用正则表达式[\u2E80-\u9FF]来匹配汉字和相关符号可能会错过《通用规范汉字表》中的一些像“𥻗”那样的位于Unicode补充平面内的汉字,但也是无可奈何的。Python这么强大都搞不定“𥻗”字的显示问题,我们也没必要纠结一些汉字不能处理的问题了。


试一下Python的出错界面(提前用Windows的记事本写一个“𥻗”字,以Unicode Big Endian编码格式保存为“米查.txt”):




这下子,连出错信息中的“Non-BMP character”也焕然大悟了,以前一直不知道这是个什么鬼。


参考资料:

https://blog.csdn.net/zxhoo/article/details/38819517

http://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php

http://blog.chinaunix.net/uid-21633169-id-4396998.html

文章转载自语和言,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论