写在前边的
相信大家经常遇见乱码,我用java就经常遇到,尤其是servlet接收参数时,当然python、js、mysql都有这问题,乱码这个问题说解决也挺简单,百度一下也许就解决了,但是下次出现仍然不知道哪里出现的问题,这个时候就该想想应该彻底把它搞懂了!
问题
本文仍按照以往风格,以问题为导向,解答疑惑
- 程序为什么会出现乱码?
- unicode、gbk、iso8859-1、ascii、utf-8、utf-16、utf-32,分别代表什么?它们之间有什么关系?
其实,只要弄懂了字符集、编码,就懂了乱码的全部。
字符集&编码
字符集就是字符的集合,相当于一本字典,我们都知道字典有英语字典、汉语字典,如果去英语字典里找汉字肯定找不到,所以就出现了乱码。经常见的字符集有ascii、iso8859-1、gb2312、gbk、big5、unicode。
字符集只规定了字符对应的数值(专业说法叫code point代码点,代码点就是个数值),但没规定存储格式,比如对一个字符可以用1个字节表示也可以用2个字节表示,这就是编码方案,编码就是用将字符变为数值,注意编码方案是对应某种字符集而言的,当然如果使用了某种编码方案默认也就是用了其对应的字符集。
ascii、iso8859-1、iso8859-n
ascii(American Standard Code for Information Interchange,美国信息交换标准代码)是最早出现的字符集,它仅含有常用的英文字母、数字及英文标点符号,共计128个字符,1个字节最多表示256个数值,所以1个字节足以表示所有ascii字符,所以每个字符占用1个字节。
iso8859-1是西欧字符的集合,它兼容ascii,另外多了一些西欧的字符。既然有iso8859-1,那有没有iso8859-2、-3…?答案是有,iso8859-2、、iso8859-3一直到.iso8859-16,用来表示中欧、南欧、东欧等地的字符,注意iso8859-n之间是不兼容的,或者说部分兼容,因为iso8859-1的总数量也没超过256,所以仍然用1个字节表示所有的iso8859-1字符,也就意味着每个字符占用1个字节,注意这里要理解iso8859-1兼容ascii这句话,兼容的意思就是iso8859-1囊括了ascii的所有字符,并且对同一个字符的数值是一样的,比如字母a,在ascii中的数值是97,在iso8859-1中数值也是97。
综上,iso8859-1既是字符集也是编码方案,所有字符都占用1个字节。
gbk、big5
既然欧美都出版了本国的字符集,那我国也不能落后。
gb2312是中文的字符集,它兼容ascii,收录了大约6000个字符,主要是简体字及常见字,繁体字、古文书上的生僻字没包括在内,为了解决这个问题,陆续出来了gbk、gb18030,gbk的k是扩展的扩的拼音首字母,收录了大约2万个字符和图形,gb18030收录的字符就更多了,包括蒙古语、朝鲜语等少数民族语言,gb18030、gbk、gb2312依次向前兼容。gbk兼容ascii,对英文字字母和数字使用单字节表示,对汉字采用双字节表示。
big5包含中文的繁体字,主要在港澳台等地流行;
unicode
可以看到每个国家和地区都有自己的字符集,在日常交流中,字符集之间需要转换,比较麻烦,而且如果其他国家再造一套本国的就更麻烦了。所以国际组织就创造了unicode。
unicode又称万国码,顾名思义就是包括世界上所有国家的字符,英文、中文、日文、韩文、英文、拉丁文、希伯来人、斯拉夫文等等。unicode也是兼容ascii的,不过它比较特殊有多套编码方案,utf-8、utf-16、utf-32。
utf-8、utf-16、utf-32
utf-8是变长编码方案,它以8-bit为编码单元,英语、数字占用1个字节,绝大多数汉语占3个字节。
utf-16也是变长编码方案,它以16-bit为编码单元,最开始计划用16-bit(也就是2个字节)表示unicode中的所有字符(基本多语言平台中的字符),但是后来unicode扩容了(扩容的部分称为辅助平面),16-bit不够用的,对于辅助平面中的字符,就用2个16-bit表示,也就是4个字节。
utf-32是定长编码方案,所有字符都占用32-bit,也就是4个字节。
可以看到utf-8是比较省空间的编码方案,所以现在的网络传输、文本保存基本用utf-8。但是它也有缺点,就是找一段文本中的第n个字符需要从头找,因为字符占用的字节数是变长的。
小总结
综上所述,
unicode是字符集,对应的编码方案是utf-8、utf-16、utf-32。
gbk,既是字符集也是编码方案,英文字母、数字占用1个字节,汉字占用2个字节。
iso8859-1是字符集也是编码方案,已有的字符集基本都兼容它。
回答问题
现在再回头看最开始的问题,
- 程序为什么会出现乱码?
- 乱码产生的根本原因是编码方案与解码方案不一致导致;比如字符a用utf-8编码的,如果用utf-16解码,虽然用的同一个字符集unicode,但肯定会出现乱码,更不用说用utf-8编码,而用gbk去解码了。
- unicode、gbk、iso8859-1、ascii、utf-8、utf-16、utf-32,这几个名词,代表什么?它们之间有什么关系?
- unicode是字符集,utf-8、utf-16、utf-32是unicode的编码方案。gbk既是字符集也是编码方案、ascii、iso8859-1是英文字母、数字的字符集和编码,unicode、iso8859-1都兼容ascii
我们来实战一下,
String s = "万";
//将字符编码,相当于客户端发的数据
byte[] bytes = s.getBytes("utf-8");
System.out.println(Arrays.toString(bytes));
//服务器端收到字节流了,将字节流解码为字符
String decodedUTF8 = new String(bytes, "utf-8");
String decodedGBK = new String(bytes, "gbk");
System.out.println(decodedUTF8);
System.out.println(decodedGBK);
输出
[-28, -72, -121] //绝大多数汉字在utf-8中占3个字节,所以这里是3个数值
万 //utf-8编码的,再用utf-8解码,正常
涓� //utf-8编码的,用其他编码方案解码,乱码!
还有一种常见的错误如下
String s = "万";
//将字符编码
byte[] bytes = s.getBytes("utf-8");
System.out.println(Arrays.toString(bytes));
String decodedISO = new String(bytes, "iso8859-1"); //应该在这里修正,改为utf-8
System.out.println(decodedISO); //乱码了
String fixStr = new String(decodedISO.getBytes("iso8859-1"), "utf-8"); //再转为utf-8修正,这是不对的!应该在第一次字节流转字符处,改正
System.out.println(fixStr);
输出
[-28, -72, -121]
万 //乱码了
万
后记
编码、解码的世界里还有 byte order mark,简称BOM,为什么有字节序?它跟大端序、小端序有什么关系?为什么utf-8可以没有bom,而utf-16、utf-32却必须有bom?大端序、小端序又指的是什么?下篇再讲吧。
参考
asciiiso8859-1gb2312gbkgb18030unicodeunicode FAQ