编码方式与中文乱码原因分析
1 基础内容
我们常见的编码方式有utf-8,utf-16,utf-32,ansi,gbk,gb2312,iso-8859-1
其中:
1)
utf-8,utf-16,utf-32,是以Unicode码表为基础的编码方式,
Unicode码表中同一个字符,使用不同的编码方式会得到不同的字节序列。
utf-32:
使用32位,即4字节来表示每个字符。
utf-16:
使用16位,即2个字节来表示每个字符,但对于一些特殊字符,会采用更多的字节表示(日常使用可以不考虑这种情况)。
utf-16就是对Unicode码表的字面量的直接使用,因为Unicode目前普遍采用的是UCS-2,它用两个字节来编码一个字符。
所以我们通常误称的“Unicode编码,UCS-2编码”指的就是UTF-16编码。
utf-8:
是一种变长的编码方式,通常采用1~3个字节来表示常用字符。
例如:普通的ascii码就使用1个字节,而中文就使用3个字节来表示。
utf系列的编码方式,通用一个Unicode码表。
2)
ansi这种说法出现在微软的操作系统中,在中国大陆,ansi就是指gbk。
gbk只是对gb2312编码的扩充,不改变gb2312的编码方式。
早在80年代初,为了在计算机中显示中文,我国定义了针对中文的码表,又称“区位表”,
而gbk或gb2312就是基于区位表的一种编码方式。
gbk与区位表的关系,就相当于utf-*与Unicode码表的关系。
3)
ISO-8859-1
只能显示ASCII表中的字符,即英文、数字和一些符号,用1个字节表示,并且字节的最高位为0,编码范围为0x00~0x7F,共128个字符。不能显示中文、日语、韩语等其他国家的语言。
2 由编码方式的不同引起的乱码
在计算机领域中,乱码问题是一个我们始终绕不开的话题,
那乱码问题是如何产生的呢?
在这之前,我们要明确两个问题:
1)位,字节,字符的区别
位(bit),我们常常描述的计算机是由0和1构成的,这里的0和1就是指的位,由通电时的高低电平来表示。
在日常生活中,我们常说,我们办的光纤是200M,500M等等的,这里200M就是以位为概念描述的,200M 表示每秒能传输 200M个位,即200*1024*1024个0和1。
字节(byte),1个字节占8位,字节是计算机的存储单元,我们的内存是4G,硬盘是1T,这里指的就是4GB和1TB
(为了区分bit和byte,通常将byte缩写为B,bit缩写为b)。
一个数无论再小,哪怕就是一个0或1,那么在存储时也至少占一个字节。
所以,假如我们有个视频文件,大小为1G,那么使用200M的宽带下载(假如宽带设定的上传与下载额定速率相同),
需要花费的时间是(理想情况下): 1024/(200/8/2) 秒
字符(char),字母、数字、符号、各国语言等等,这些我们能够描述的文字和符号,统称字符。
我们的文字要在计算机中存储和显示,就需要将文字变成计算机能表示的0和1的序列。
这就是我们上一部分说明的各种编码方式的作用。
2)区分内存中的字符 与 文件(硬盘)中的字符
以Jvm为例进行说明。
在java中,数据类型char表示字符,为了兼容各种语言字符,使用Unicode码来表示字符(UCS-2),那么一个char就占两个字符。所以在各种环境中,字符在jvm中的表示形式固定的,且是定长的,这就极大的方便了运算。
例如,
汉字“经”,在内存中表示为“7E CF”(16进制)。
由于统一的表示方式,所以内存中不存在乱码这个说法。
但是,当我们将字符写入文件时,就会出现情况了。
当我们将内存中的数据写入文件时,并不是直接写入,而是需要有个编码的过程。
为什么不直接写入呢?
因为内存中所有字符均占2个字节,那么对于英文字母、数字、部分符号,我们明明一个字节(gbk和utf-8)就可以表示,如果统统采用2个字节,这样无形会浪费存储效率和空间(特别对于以英语为主的国家)。
所以普通文件常用的编码方式就是utf-8和gbk(因为能节省空间啊)。
那么在写入文件时,会将内存中的字符,按照一定的编码规则,生成字节序列 byte[],一定要注意这点——“字节序列”。
然后,将该序列数组写入硬盘文件中。
byte[] b = "经".getBytes("utf-8");
for(int i=0;i<b.length;i++){
System.out.print(Integer.toHexString(Byte.toUnsignedInt(b[i]))+" ");
//E7 BB 8F
}
“经”.getBytes(“gbk”) : 0xBEAD
“经”.getBytes(“utf-8”): 0xE7BB8F
于是,硬盘文件中的内容就是0xBEAD或0xE7BB8F
所以我们可以看到,jvm内存与硬盘文件对同一个汉字表示就不同了,这其中有一个“编码”的过程。
说到这里,可能有些同学及猜到了乱码的原因了,下面就是见证奇迹的时刻。
当我们使用程序读取硬盘文件内容,或接收网络传输的数据的时候,也是一个一个按字节来读取的(学过IO流,我们知道字节流是最基本的流),
假如我们得到了一个字节序列:[e6 88 91 e7 88 b1 e4 b8 ad e5 8d 8e ],
我们想看一看这个字节序列是什么内容,就需要将他们转换为内存中的字符,
我们知道将字符转换为字节序列,有个编码的过程,
那么反过来,将字节序列变为字符,就有个解码的过程,
那到底按照什么方式进行解码呢,gbk还是utf-8呢?
byte[] b = {(byte) 0xe6,(byte) 0x88,(byte) 0x91,
(byte) 0xe7,(byte) 0x88,(byte) 0xb1,
(byte) 0xe4,(byte) 0xb8,(byte) 0xad,
(byte) 0xe5,(byte) 0x8d,(byte) 0x8e};
String s1 = new String(b,"utf-8");
System.out.println(s1);//我爱中华
String s2 = new String(b,"gbk");
System.out.println(s2);//鎴戠埍涓崕
我们看到,当我们使用gbk解码这个字节数组时,就会出现乱码,而utf-8就是正常显示。
而这,就是我们出现中文乱码的根本原因:
在解码字节序列时,采用的编码方式不对,或者说,与生成该字节序列时采用的编码方式不同。
有的同学会说,既然是乱码,为什么gbk解码生成的字符串,会出现“鎴戠埍涓崕”这些内容呢?
那这就需要看gbk解码的过程了:
gbk解码时会按字节进行解码,
首先读取第一个字节,0xe6,发现这个字节的最高为是1,而不是0,就认为这是一个汉字的第一个字节,而不是一个ASCII字符。
然后,继续读取第二个字节[0x88],将两个字节合并在一起,[0xe688],然后按照gbk约定的解码逻辑,解码得到一个数字。
最后一步根据这个数字去查gbk的“区位表”,发现对应的区位表的内容是“鎴”,于是就显示出了这个汉字。
以此类推,得到“鎴戠埍涓崕”这个乱码字符串。
符: