阅读 345

UTF-8 BOM踩坑记——细说Unicode和UTF-8

在生产环境遇到了一个问题,系统访问外部webservice地址的时候出错,原因竟然是因为地址格式是UTF-8 BOM的格式,地址末尾多了几个不可见字符 %EF%BB%BF!!! 导致地址识别错误,字符编码知识的贫乏让我耗费了一整天来解决这个问题。王小波说过:人的一切痛苦,本质上都是对自己无能的愤怒!(王小波:瞎说,我哪里说过?)所以打算好好梳理一下字符编码的知识点,下面会对UnicodeUTF-8UTF-8 BOM等进行讲解,避免大家像我一样踩坑。

咱们首先带着两个问题来开始了解字符编码:

问题一:我们常常看到的如ANSIUnicodeUCSUTF-8GBK是什么?他们之间有什么关系?

问题二:UTF-8和UTF-8 BOM是一样的吗?我们应该使用哪一个?

ANSI

相信大家作为程序员对与ASCII码都不陌生,ASCII是美国人制定的字符编码,对于英文字符和二进制之间做了映射关系。ASCII规定了128个字符的编码,也就是用一个字节即可表示,只占用一个字节的后面7位,最前面的一位统一规定为0。

但是问题来了,ASCII是美国佬搞出来的,他们觉得一个字节表示英文世界所有的字符、数字、符号绰绰有余。

到了后来法国人和德国人就不服了,凭什么只有英文没有法语(如:é)、德语(如:Ä ä)?ASCII不是只用了一个字节里面的前128个编码嘛,所以剩下的没用完(128~255)的就让欧洲人给用去了。

当我们中国人开始使用计算机,我泱泱大中华文化博大精深,这明显字符就不够用了,汉字包括各种生僻字就有九万多个,于是国家标准总局出马了,GB 2312就诞生了。GB 2312采用两个字节来进行编码,只给了常用的几千个简体汉字进行了编码。咱们的台湾同胞看这怎么没有繁体字,于是就给编了个繁体字码——BIG-5。当然后来就是GBK诞生将GB2312BIG-5都收于旗下,并且仍旧采用两个字节进行编码。

陆陆续续地,各个国家开始为自己地文字进行独立编码,微软为了能够打进各个国家的市场,践行顾客就是上帝的原则,于是ANSI就诞生了。ANSI并没有做到统一各个国家的编码,相反的,它只是实现了不同的二进制在不同国家的电脑上采用不同国家的编码规则!比如在中国就用GBK,在韩国就用EUC-KR,在美国就用ASCII,属于曲线救国的一种做法。但是!这里存在乱码的问题!一个中国人发一段ANSI格式的文档给一个美国人,这个美国人只会是看到一堆的乱码。没有统一的字符编码,就如同上帝将巴别塔的人民分散到世界各地并打乱了天下人的语言,从此世界各地的人民语言互不相通而再也不能继续造塔。

Unicode和UCS

可以看到ANSI实属一种指标不治本的编码方案,建立世界统一的字符编码彻底解决乱码问题任重而道远。在上个世纪八九十年代,有两个组织就在致力于一统字符编码的天下。

  • Unicode联盟:成立于二十世纪八十年代末,发明了Unicode字符集,致力于以Unicode取代现存的字符编码。
  • 国际标准化组织(ISO):于1984年创建ISO/IEC/JTC1/SC2/WG2工作组,试图制定通用字符集(Universal Character Set, UCS),并且最终制定了ISO 10646标准。

世界不需要两个不同的字符集,两个组织的人也认识到了这一点,于是他们开始协同合作,目前二者虽然仍旧独立存在并独立公布标准,但严格保证了在两个标准中所有的字符都在相同的位置并且有相同的名字

UTF-8

需要注意的是,UCSUnicode都只是字符集,只规定了每一个符号的二进制代码,但是没有规定这个符号要如何存储。而规定存储方式称为UTF(Unicode Transformation Format),其中包括有UTF-8UTF-16UTF-32

举个例子,我们刚才说了Unicode是一个字符集,它为每一个字符分配了一个唯一的ID(或者专业点说叫做码位),「掘」这个汉字的Unicode码位是25496,记作U+6398(25496的十六进制是6398),而他的UTF-8编码则为E68E98。

在最初提出来的是UTF-32UTF-16,顾名思义,UTF-32是用四个字节来表示Unicode码位,UTF-16则是用两个字节来表示Unicode码位,但由于两个字节实在不够用,所以会用四个字节来对一些不太常用的字符进行编码,所以UTF-16属于变长编码。

但想一想就会发现,不管是UTF-32还是UTF-16,在编码ASCII的时候,前面就会出现一堆的0,在互联网的世界中这是对带宽资源的一种极大浪费,而UTF-8解决了这个问题。UTF-8同样是一种变长编码,规则非常简单:

  • ASCII的单字节编码,编码方式和ASCII一致。
  • 多字节编码则满足:字节数为N时,第一个字节的前N为为1,第N+1位为0,后面的N-1个字节的前两位都为10,N字节中的其余位全部用来存储Unicode中的码位值。
字节数 Unicode UTF-8
1 U+0000 ~ U+007F 0xxxxxxx
2 U+0080 ~ U+07FF 110xxxxx 10xxxxxx
3 U+0800 ~ U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
4 U+10000 ~ U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

汉字在Unicode中为两个字节码位,位于U+0800 ~ U+FFFF范围内。所以,你现在知道为什么汉字在UTF-8中占三个字节了吧?

Little Endian 和 Big Endian

Little Endian 和 Big Endian 翻译过来就是小头和大头。维基上记载了这两个词来源于爱尔兰作家乔纳森·斯威夫特的《格列佛游记》,不过现在已经基本用于表示字节顺序了。

  • Big Endian 是指低地址端存放高位字节。
  • Little Endian 是指低地址端存放低位字节。

还是以汉字「掘」为例,Unicode码位为U+6398,需要两个字节存储。如果存储63在前,98在后(63在低地址端),就是 Big Endian;反之如果98在前,63在后(98在低地址端),就是 Little Endian。

根据上面UTF-8的规则可以看出其编码规则不需要判断 Little Endian 还是 Big Endian ,但是UTF-16UTF-32就需要。

那么计算机是怎么识别采用哪一种字节顺序的呢?

Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),用FEFF表示。如果一个文件的头两个字节是FEFF就是Big Endian,如果是FFFE就是Little Endian。而这个标识,就是BOM。

什么是BOM

BOM(byte-order mark),即字节顺序标记,是插入到以UTF-8UTF-16UTF-32编码的Unicode文件开头的特殊标记,用来识别Unicode文件的编码类型和字节顺序。

编码方式 BOM
UTF-8 EF BB BF
UTF-16 (Big Endian) FE FF
UTF-16 (Little Endian) FF FE
UTF-32 (Big Endian) 00 00 FE FF
UTF-32 (Little Endian) FF FE 00 00

带还是不带BOM?

为什么表格中还有UTF-8呢?因为UTF-8是分为带BOM和不带BOM的。

微软建议所有的Unicode文件都应该采用BOM,所以在自己的UTF-8格式的文本文件之前加上了EF BB BF三个字节, windows上面的notepad程序就是根据这三个字节来确定一个文本文件是ASCII的还是UTF-8的。

但是这只是windows的自定义的规则,其他系统是没有采用这个规则的。由于BOM只是一个标识,是不可见字符,违反了UNIX的设计原则,所以Linux/UNIX 是没有使用 BOM的。

带BOM还是不带BOM,这还是基于个人使用习惯,毕竟Unicode规范也允许二者皆可。如果是只用Linux系统,那么不带BOM的UTF-8就完全可以,但如果是一个windows用户,那么带BOM则更适合你。跨平台的话则最好结合实际具体分析,涉及windows的话采用带BOM的更为通用一些。

当然了,我作为一个JAVA程序员,在windows下使用IDEA,生产环境则是部署在Linux虚机上,吃一堑长一智,下次一定要注意去掉BOM!!!

本文使用 mdnice 排版