字符编码问题相信大多数程序员遇到过,令人很是头疼,本文在参考了一些文献、博客之后总结了一些编码的知识。
奇怪的bug
首先抛个现象,在windows的记事本中,写”联通”两个字,保存,然后再次打开,会发现什么?当删除了”联通”重新输入联通,再次保存,再次打开,会发现什么?
答案是第一次乱码,第二次就是正常的了,为什么呢?
那就从最基础的编码讲起。
ASCII码
一开始美国人搞出来一套编码,称为ASCII码,用一个字节表示,8位能够表示256个字符,足够美国人用的了,事实上他们只用到了128个字符,还有位就空着了。
接着,西欧人发现他们的语言没法用纯粹的英语字母表示(像法语会有音调),他们就扩展了ASCII码,因为原来还剩一位没用到,正好又可以加128个字符。
ASCII码表可以在这里找到:http://www.asciima.com/
MBCS
计算机在发展到其他国家的时候发现,每个国家的语言各种各样,ASCII码明显不可以用了,然后每个国家都开始编写自己国家的编码,统称为MBCS,全称:Muilti-Bytes Charecter Set,多字节字符集。其实大多数国家的字符都可以用两个字节表示,所以多字节字符集一般也认为是双字节字符集。
注:这样一来,不同的国家,同一个编码可能会表示不同的字符,这是个问题,后面再讲。
GB2312
MBCS在中国就是1980年发布的GB2312,就是国标的拼音,这个编码用区位码(94个区,每区94个字符)的方式表示了6763个汉字和包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符,它所收录的汉字已经覆盖中国大陆99.75%的使用频率,基本可以满足汉字计算机的需要了。
GB2312采用EUC存储方式,这个存储方式通俗地说就是用高位字节和低位字节的不同范围表示不同字符,可以和ASCII码兼容,也可以和日文、朝鲜文字兼容共存。
GBK
后来人们又发现,7000多个字符似乎还是涵盖不了所有的字符,比如某个国家领导人的名字有个比较生僻的汉字:”镕”。这就需要扩展,怎么扩展呢?GB2312用了两个字节,16位可以表示65536个字符,而GB2312其实才占用了7000多个字符,还有大量的字符可以扩展,所以GBK诞生了(国标扩展的拼音),同样两个字节,完全兼容GB2312编码。他把一些没包括的中文简体、中文繁体、日文片假也同时包含在内(可惜韩文没有包含)。
微软的CP936一般来说等同于GBK。
Unicode
重点来了,刚才说到每个国家的MBCS的编码不一样,同一个编码对应的字符会有冲突,这样就导致不同编码的计算机通讯成为了问题,为了解决这个问题,人们又重新搞出来一套编码:Unicode。Unicode一般使用2个字节,可以表示65536个字符,也就是UCS-2,理论上所有国家的字符加起来都可以包含进去,不过事实上还是有不常使用的字符没有包含进去,所以还有个4个字节的UCS-4,这样32位的编码毫无疑问可以全部包含字符。
你可以在http://www.unicode.org/Public/UCD/latest/charts/CodeCharts.pdf找到unicode的所有字符及编码(这个文档有98M…)。
utf-8
unicode是一个大的字符集,把世界上形形色色的字符用编码表示起来,一般我们说的Unicode编码是指UCS-2,也就是规定好2个字节表示一个字符,这样当然可以,但是本着存储空间节约的原则,不可能把所有字符都用2个字节来表示,这样一来,utf-8就诞生了,它使用可变长度的字节来表示字符,ASCII码中的字符用一个字节表示,带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码,像中文就需要三个字节表示。因为计算机世界的ASCII码字符使用较频繁,所以使用utf-8无疑会大大减少存储空间。
注意一点,unicode是严格意义上指的是字符集,utf-8是unicode的实现方式,某些地方的unicode编码指的是如上所说的UCS-2编码。
那么,utf-8具体是怎么对unicode码表进行编码的呢?
如表:
代码范围(十六进制) | 标量值(二进制) | UTF-8(二进制/十六进制) | 注释 |
---|---|---|---|
000000 - 00007F(128个代码) | 00000000 00000000 0zzzzzzz | 0zzzzzzz(00-7F) | ASCII字元范围,字节由零开始 |
000080 - 0007FF(1920个代码) | 00000000 00000yyy yyzzzzzz | 110yyyyy(C0-DF) 10zzzzzz(80-BF) | 第一个字节由110开始,接着的字节由10开始 |
000800 - 00D7FF 00E000 - 00FFFF(61440个代码) | 00000000 xxxxyyyy yyzzzzzz | 1110xxxx(E0-EF) 10yyyyyy 10zzzzzz | 第一个字节由1110开始,接着的字节由10开始 |
010000 - 10FFFF(1048576个代码) | 000wwwxx xxxxyyyy yyzzzzzz | 11110www(F0-F7) 10xxxxxx 10yyyyyy 10zzzzzz | 将由11110开始,接着的字节由10开始 |
表中显示:
- ASCII码表中的128个字符使用一个字节表示,字节以0开头
- 第80开始到7ff的字符用两个字节表示,第一个字节以110开头,第二个字节以10开头
- 000800 - 00D7FF 00E000 - 00FFFF范围(这里有一段空着的预留编码)的字符用三个字节表示,第一个字节以1110,其余字节以10开头
- 010000 - 10FFFF范围的字符用4个字节表示,第一个以11110,其余10开头
总的来说就是用多少字节,就要在第一个字节用多少个1加上一个0表示,其余字节以10开头,理论上utf-8是可以用6个字节表示的,大家可以想想为什么是6个字节。但是unicode没有这么多字符给它表示,2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,也就是4个字节。
有了这个规则,我们看下计算机怎么存储utf-8的字符的。
unicode转utf-8
比如读取一个字符串“联通”
计算机首先在unicode码表中找到联通两个字,编码分别为\u8054和\u901A,以”联”字为例,转换为二进制为1000 0000 0101 0100,查看上表的范围可知需要用utf-8的三个字节表示
这个来历不明的公式可以计算需要的字节数:(unicode编码的位数+3)/5。对应上面就是(16+3)/5 = 3
然后,计算机可以先把三个字节的开头几位写好:
**110***** 10****** 10********
然后根据unicode编码的二进制从低到高填坑,高位不足的补0,得到二进制为:
1110 1000 1000 0001 1001 0100
十六进制为
E88194
这个就是utf-8编码的”联”字。
utf-8转unicode
utf-8编码的字符串怎么找到对应的文字呢?还是以上面的题目为例
已知一个utf-8编码的字符串:E88194,二进制为1110 1000 1000 0001 1001 0100。
计算机首先读取第一个字节,发现是1110开头的字节,就往后再读两个字节,把这三个字节作为一个字符去解析。
接着把每个字节表示utf-8的位去掉,得到如下二进制:
1000 0000 0101 0100
转换为16进制就是
8054
然后查询unicode编码表,发现字符为中文”联”。
可以在这里http://www.mytju.com/classCode/tools/encode_utf8.asp便捷地查询字符编码。
答案揭晓
现在我们再看文章开头的问题,我们知道,windows记事本默认编码为ANSI,各个国家的ANSI编码不同,中国为GB2312,那么我们保存中文的时候是以GB2312编码保存的,我们查询一下GB2312的编码表,发现”联通”的编码为:
C1AA和CDA8
以”联”字为例
二进制为:
1100 0001 1010 1010
当我们再次打开这个文本的时候,计算机不知道我们原来文本的编码,它就开始识别文本的编码,计算机发现第一个字节为110开头的字节,第二个字节为10开头的字节,这不正好和utf-8的编码匹配吗?所以计算机就认为,这个文本是utf-8编码的。
我们可以再尝试把这个编码从utf-8转为unicode看看是不是有这个unicode编码。
转为unicode编码为:
1101010
只有7位,而7位理论上只能有一个字节表示,这就导致了冲突。
当我们重新输入联通,因为此时的记事本是以utf-8的格式打开的,所以再次保存是以utf-8格式保存的,再次打开就是正确的了。
有童鞋可能会想,那类似”联通”的GBK和utf-8冲突的字符会不会很多啊?这个问题可以用上述原理去查表,应该是不会,这个还没具体去查过。
OK,关于字符编码就说到这里,希望能给大家带来一点点的益处,写完收工。
【参考文献】
- https://zh.wikipedia.org/wiki/Unicode
- http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
- http://www.unicode.org/Public/UCD/latest/charts/CodeCharts.pdf
- https://zh.wikipedia.org/wiki/GB_2312
- https://zh.wikipedia.org/wiki/UTF-8
- https://zh.wikipedia.org/wiki/%E6%B1%89%E5%AD%97%E5%86%85%E7%A0%81%E6%89%A9%E5%B1%95%E8%A7%84%E8%8C%83