Date Wed 20 November 2013 Tags encoding

每个软件开发人员应该无条件掌握的知识!


字符编码是一个基础话题,不管你从事哪种语言的开发,还是前端、后端或网络开发,乱码问题一直困扰着像我这样的低水平的生手。接下来的内容是我参考了好多的文章,并整理的便于自己理解的脉络。
就像别人一样的开场:计算机比较傻,在它的世界里只有 1 0,如何才让人类可理解的字符(Character,如字母、数字,一些符号,汉字...)打印到显示屏幕上呢。这需要有一个从由1 0组合成的序列到可打印字符的一个映射 (编码Encoding),可打印的字符是一个集合(字符集Charset)。在这里就引出了几个概念:

  • 字符(Character):是文字与符号的总称,包括文字、图形符号、数学符号等。
  • 字符集(Charset):即字符的集合,规定了在这些集合里面有哪些字符。
  • 字符编码(Encoding):就是规定用一个字节还是多个字节来存储一个字符,用固定的二进制码值表示某个字符。
  • 字节(byte):计算机中存储数据的单元,一个8位的二进制数,是一个很具体的存储空间。

一看到这样定义我就头大,还是来具体的例子,故事开始了

美国人发明了计算机

很久很久以前,计算机世界只有美国人。英语语言里字符很少,大小写字母共26*2个,阿拉伯数字10个,其它的一些符号(如 ! " # $ % &等),这些可打印字符一共95个。计算机的的回车键,删除键等控制符33个。总和只有128个。于是,对于他们来说,很自然,那么计算机中用8位就可以表示他们的所有字符了吧。于是他们将8位称作一个字节,计算机的8位表示的每个数字对应了一个英文字符,画了一张表(ASCII码表),这就是ASCII编码(American Standard Code for Information Interchange,美国信息互换标准代码)。
这里的128个字符集合,就叫字符集,对应的二进制,就是字符编码。你可以做个 测试 ,新一个文本文件,内容为abcdabcd,保存为ASCII编码格式,用十六进制编辑器打开,看是否与ASCII码表中的值对应。 win7系统中

Encoding Img

linux系统中

Encoding Img

上面除了对应的61 62 63 64,还有0D 0A,这是windows系统中断行的标志,linux中是0A,默认最后一行有添加该标志。详细请参见将DOS格式文本文件转换成UNIX格式

计算机传到了欧洲

欧洲是有好多个国家的,他们的每个国家也都有自己的文字,比如拉丁文,希腊文等。怎么办呢?于是想到,你美国人指定的ASCII码表里面不是只有127个字符吗,后面128-255的字符不是说待定吗,好吧,我们就不客气了。于是欧洲人就将各种奇怪的语言塞入127后面的字符中,形成了一系列的ISO 8859字符集。比如希腊文塞入ASCII,就形成了ISO/IEC 8859-7,西欧语种塞入ASCII就形成了ISO/IEC 8859-1,ISO/IEC 8859-1也叫做latin-1。(对,就是mysql里面经常见到的编码)

下面是ISO 8859现有的15个字符集

  • ISO/IEC 8859-1 (Latin-1) - 西欧语言
  • ISO/IEC 8859-2 (Latin-2) - 中欧语言
  • ISO/IEC 8859-3 (Latin-3) - 南欧语言。世界语也可用此字符集显示。
  • ISO/IEC 8859-4 (Latin-4) - 北欧语言
  • ISO/IEC 8859-5 (Cyrillic) - 斯拉夫语言
  • ISO/IEC 8859-6 (Arabic) - 阿拉伯语
  • ISO/IEC 8859-7 (Greek) - 希腊语
  • ISO/IEC 8859-8 (Hebrew) - 希伯来语(视觉顺序)
  • ISO 8859-8-I - 希伯来语(逻辑顺序)
  • ISO/IEC 8859-9(Latin-5 或 Turkish)- 它把Latin-1的冰岛语字母换走,加入土耳其语字母。
  • ISO/IEC 8859-10(Latin-6 或 Nordic)- 北日耳曼语支,用来代替Latin-4。
  • ISO/IEC 8859-11 (Thai) - 泰语,从泰国的 TIS620 标准字集演化而来。
  • ISO/IEC 8859-13(Latin-7 或 Baltic Rim)- 波罗的语族
  • ISO/IEC 8859-14(Latin-8 或 Celtic)- 凯尔特语族
  • ISO/IEC 8859-15 (Latin-9) - 西欧语言,加入Latin-1欠缺的芬兰语字母和大写法语重音字母,以及欧元(€)符号。
  • ISO/IEC 8859-16 (Latin-10) - 东南欧语言。主要供罗马尼亚语使用,并加入欧元符号。

该部分内容我们不用太关注,接下来

伟大的中国人终于用上了电脑

中文可不得了,文字博大精深,字符远远超过了256个。所以我们无法使用ASCII的扩展了。怎么办呢? 1981年的时候,国家派一批人来做了这个事情,他们统计出所有的中文大概有6000多个字符(后来证明这些人的水品也是有限,好多字符都没有搜出来,于是就有了多种的中文编码),用两个字节(16bit)来表示,16bit能表示的是65536个字符,太够了。我们将16bit分为前8bit和后8bit
如果前8bit小于127(英文ASCII),那么这个8bit就是表示英文
如果前8bit大于127,那么这8bit和后面的8bit合起来表示一个中文 这就是GB2312,GB2312 是对 ASCII 的中文扩展

好了,后来某些领导发现,他的名字没法编码了,这个问题出来了。6000个汉字还不足以囊括所有中文,国家在1995年又组织了一批人,继续搜罗一些生僻字,一共搜集出了21886个汉字和字符,形成了GBK编码,GBK编码向下兼容GB2312。

再后来发现了,一些满文,蒙古文啥的少数名族的语言没有编辑到GBK中,继续编辑收录,形成了GB18030编码。

从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为0。按照程序员的称呼,GB2312、GBK到GB18030都属于双字节字符集 (DBCS,Double Byte Charecter Set)。
在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处 理,必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了

自由的台湾人民

台湾是汉字是繁体字,当然不能使用大陆编辑使用的GBXX系列编码了,于是他们自己搞了一套BIG5中文编码,收录了13060个汉字和字符。但是这里要注意,BIG5的编码映射表和GBXX系列的就完全不一样了,比如同一个“中”字,在BIG5和GB2312中就是两个完全不同的字节。这里就会有乱码出现了,比如("陶喆"和"陶吉吉"),各种简体中文和繁体文的转码工具就出现了。
五种中文套装软体:文书处理,资料库,试算表,通讯,绘图。大致的意思是这套编码主要使用于这5个领域

各国人民的的UNICODE

由于每种语言都制定了自己的字符集,导致最后存在的各种字符集实在太多,在国际交流中要经常转换字符集非常不便。因此,产生了Unicode字符集,它固定使用16 bits(两个字节)来表示一个字符,共可以表示65536个字符

对于ascii里的那些“半角”字符,UNICODE 包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于"半角"英文符号只需要用到低8位,所以其高 8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。Unicode使用的通用的字符集叫做UCS。这个字符集就是一个大的字符空间,每个语种都在这个字符空间内划分一段领域。现在应用的UCS是UCS-2,意思就是不管是英文中文,统一使用两个字节(16bit)来进行字符分配。UCS-2字符集可以表示216(即65536)个字符。已经基本满足世界上所有语言了。如果不够怎么办?已经有预定方案UCS-4(用4个字节表示一个字符)。


注意

  • Unicode只是一个字符集,全纳了世界所有的符号,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
  • UTF-xx是Unicode的具体实现方式。
  • UTF-16是Unicode最基本的实现。Unicode使用16bit表示一个字符,UTF-16就是直接将字符集的映射搬过来而已。

UTF-8

本来这样就已经很美好了,但是美国人又不干了,毕竟互联网70%以上的信息仍然是英。凭什么每个英语字符要占用2个字节?凭什么占用了我们的带宽和CPU?于是一帮英语体系的外国人讨论出了UTF-8这种字符编码。UTF-8就是这样一个为了提高英文存取效率的字符集转换格式(Unicode Transformation Form 8-bit form)
UTF-8这种编码是怎么回事呢?

  • 英文字符,和ASCII码一样,占用一个字节。因此对于英语字母,UTF-8编码和ASCII码是相同的。
  • 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

    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

这里演示一个中文字“汉”,查得unicode 表式为0x6C49,二进制为0110 1100 0100 1001 根据上表,可以发现0x6C49处在第三行的范围内(0000 0800-0000 FFFF),因此"严"的UTF-8编码需要三个字节,即格式是"1110xxxx 10xxxxxx 10xxxxxx"。然后,从"严"的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,"汉"的UTF-8编码是"11100110 10110001 10001001",转换成十六进制就是0xE6B189。示例如下:

Encoding Img

图中十六进制内容是FF FE 49 6C,肿么回事?这是保存的两种方式:

  • Little endian: 第二个字节在前
  • Big endian: 第一个字节在前

如保存为Big endian方式:

Encoding Img

保存为UTF-8方式

Encoding Img

上图中的十六进制为EF BB BF E6 B1 89,肿么会多出来EF BB BF呢,参见带BOM的UTF-8
如不想带bom,使用editplus编辑器另存为无BOM

Encoding Img

总结

主要搞清两个重要概念字符集字符编码

  • 字符集:即字符的集合,规定了在这些集合里面有哪些字符,也规定了二进制表示。Ascii是一个集合,gb2312兼容Ascii。
  • 字符编码:简单讲就是规定用一个字节还是多个字节来存储一个字符。编码方式决定了实际存储的二进制。如GB2312中,字母数字一个字节存储,汉字两个字节存储。Unicode中,UTF-16是按Unicode字符集表示的二进制存储,UTF-8是按字符所在Unicode范围进行一个转换。

接下编码系列的内容可能会包括

  • URL Encode
  • web方面乱码的处理
  • java python中的编码处理
  • base64编码

码表

参拷

(完)


Comments

comments powered by Disqus