文章目录
之前N篇都提到了汉字乱码,真是个长久困扰我们的问题。
无论开发语言,磁盘文件,数据库,网络传输都可能出现编码问题。
(一)编码
计算机本无码,它们只认0101的二进制(我们为了方便经常写作格式0xFF的16进制)。
所以要显示任何文字都需要进行编码,即使是英文字母。所以是人类创造了码。
废话尽量简短吧:
PS:内容和图来自百度和其它网站(能找到链接的都给出了)。
1.1 ASCII码
ASCII= American Standard Code for Information Interchange=美国信息交换标准码
单个字节表示一个字符,最高位为0,其它位的组合表示了各种英文字母与符号,比如:
最多: 0111 1111,7F
HEX:41 42 43 44 2C 31 32 33 34 —— ABCD,1234
在英语中,用128个符号编码便可以表示所有字母和符号,但是用来表示其他语言是不够的。
1.2 ASCII码的扩展
将最高位也使用起来,比如法语中的é的编码为130(二进制10000010)。
这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
最多: 1111 1111,FF
但是不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如:
字节(130)在法语编码中代表了é,
在希伯来语编码中却代表了字母Gimel (ג),
在俄语编码中又会代表另一个符号。
但是不管怎样,所有这些编码方式中,0–127表示的符号是一样的,不一样的只是128–255的这一段。
PS:为了知道同样的编码到底表示的具体字符,我们必须知道这段文字的字符集。
1.3 汉字(包括其它文字)的多字节的编码
由于我们有国家标准的编码(GB)又有国际标准的编码(Unicode),所以中文相对复杂一些。
- GB2312编码:1981年5月1日发布的简体中文汉字编码国家标准。GB2312对汉字采用双字节编码,收录7445个图形字符,其中包括6763个汉字。
- BIG5编码:台湾地区繁体中文标准字符集,采用双字节编码,共收录13053个中文字,1984年实施。
- GBK编码:1995年12月发布的汉字编码国家标准,是对GB2312编码的扩充,对汉字采用双字节编码。GBK字符集共收录21003个汉字,包含国家标准GB13000-1中的全部中日韩汉字,和BIG5编码中的所有汉字。
- GB18030编码:2000年3月17日发布的汉字编码国家标准,是对GBK编码的扩充,覆盖中文、日文、朝鲜语和中国少数民族文字,其中收录27484个汉字。GB18030字符集采用单字节、双字节和四字节三种方式对字符编码。兼容GBK和GB2312字符集。
- Unicode编码:国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换。Unicode采用四字节为每个字符编码。
- UTF-8和UTF-16编码:Unicode编码的转换格式,可变长编码,相对于Unicode更节省空间。UTF-16的字节序有大尾序(big-endian)和小尾序(little-endian)之别。PS:UTF-8的汉字通常是三字节。
我们的国标编码(字符集)是这样发展的:
1.4 编码实例和测试
举个例子,四个汉字(参考🔗网站)
【 我 〇 䶵 𬌗 】
- 我:常用字,各种字符集编码都有。
- 〇:早期的GB2312没有收录。
- 䶵:日文的汉字?参考链接,GBK没收录,GB18030是四字节,UTF-8是三字节
- 𬌗:牙齿咬合面,参考链接,GBK没收录,GB18030是四字节,UTF-8是四字节。
用Java测试一下,代码如下:
String aTestStr="中文我〇䶵𬌗abc";
{
System.out.print("UTF-8 :");
byte[] gb = aTestStr.getBytes(StandardCharsets.UTF_8);
for (byte b : gb)
System.out.printf("%#02x,", b);
System.out.println("\n"+new String(gb, StandardCharsets.UTF_8)+"\n");
}
{
System.out.print("GB18030:");
byte[] gb = aTestStr.getBytes("GB18030");
for (byte b : gb)
System.out.printf("%#02x,", b);
System.out.println("\n"+new String(gb, "GB18030")+"\n");
}
{
System.out.print("GBK :");
byte[] gb = aTestStr.getBytes("GBK");
for (byte b : gb)
System.out.printf("%#02x,", b);
System.out.println("\n"+new String(gb, "GBK")+"\n");
}
{
System.out.print("GB2312 :");
byte[] gb = aTestStr.getBytes("GB2312");
for (byte b : gb)
System.out.printf("%#02x,", b);
System.out.println("\n"+new String(gb, "GB2312")+"\n");
}
输出结果如下,和上面表格一致:
呃,稍微仔细点看吧,或者去掉前后无关的字。。。
UTF-8 :0xe4,0xb8,0xad,0xe6,0x96,0x87,0xe6,0x88,0x91,0xe3,0x80,0x87,0xe4,0xb6,0xb5,0xf0,0xac,0x8c,0x97,0x61,0x62,0x63,
中文我〇䶵𬌗abc
GB18030:0xd6,0xd0,0xce,0xc4,0xce,0xd2,0xa9,0x96,0x82,0x35,0x87,0x38,0x99,0x31,0xd2,0x39,0x61,0x62,0x63,
中文我〇䶵𬌗abc
GBK :0xd6,0xd0,0xce,0xc4,0xce,0xd2,0xa9,0x96,0x3f,0x3f,0x61,0x62,0x63,
中文我〇??abc
GB2312 :0xd6,0xd0,0xce,0xc4,0xce,0xd2,0x3f,0x3f,0x3f,0x61,0x62,0x63,
中文我???abc
(二)显示出现乱码的原因
2.1 超出编码范围
如上例,GBK,GB2312都有乱码,生僻的字显示成了?问号。
如果一段字符串的字节码中保存了所用字符集中没有的编码内容,
则显示时会产生看不懂的错乱符号和怪异字符,一般我们叫做乱码。
PS:之前遇到的:🔗《Python写入文本文件时‘GBK’编码器无法编码字符‘\uXXYY‘》就是编码范围的问题。
文章中没有写正确,我也懒得改了,如上测试的结果,Java并不是指定GBK就高枕无忧,得GB18030啊!
2.2 编码UTF8的BOM
在Windows下可能某些UTF8的编码,前3位是Bit Order Mark(之前写错了),但其实UTF8是不需要字节顺序标识位的,所以唯一的作用就是表示这是一个UTF8的文件。
这不是一个很通用,大家都接受的设定(请自行了解BOM),比如Linux是不认BOM的。
如果无视BOM会导致读取时前面会出现有一点点乱码。
最好办法就是不要使用BOM,中文UTF8编码(有BOM)的数据例如下:
EF BB BF 41 42 43 31 32 33 2C E4 B8 AD E6 96 87 E6 B1 89 E5 AD 97
“ABC123,中文汉字”
2.3 不支持中文
比如操作系统不支持,没有安装中文字体。
即使内容编码是正确的,但系统不知道什么是GB18030,没有能显示GB18030的字体。就只能显示成乱码。
其实这种情况不能叫乱码,码是对的,但显示不了而已(一般都是方框框?)。
2.4 用错了编码
Bingo!
比起前面几种不太常见的原因,编程时用错了编码,才是引起乱码的最主要原因。
所谓用错,就是用一种编码,去读另一种编码的字节码内容。
最常见的:用GB系列编码方式读UTF8,用UTF8读GB系列。
PS:之前遇到的:🔗《升级HBase2字符编码问题以及中文显示》属于用错了编码,
不过不是我主动写错的,而是String.getBytes没传递字符集参数,使用了系统默认字符集的问题。
Windows/Linux默认不一样,而Java在后续操作中(HBASE取出数据)对不指定的文本,都采用了UTF8处理。
下面不知道哪位大神整理的表格,遇到乱码时可以看看。
2.5 原始字节码错误
如果像上面提到的:读错后又将内容写入到了一个新的文本文件中,那么这个新的文本文件编码就错了。
文本原始的字节码已经错误了,无论你后来怎么读,显示都是错误的。
特别是【锟斤拷】这种,是不可恢复的错误。
(三)避免文件读写乱码
3.1 注意默认编码
- Java默认采用UTF8编码。
- Linux默认是UTF8编码。
- Windows默认是GB18030编码(大家都说GBK,但是GBK范围小一些,哎)
- 即使Windows下,IntelliJ IDEA的单元测试默认是UTF8编码(怎么测试和正式运行时不一样?)。
3.2 指定编码
- 打开,写入文本文件时,要指定一种编码,需要指定正确。
- 正确的编码无需转换,需转换的编码一定是前面已经错了。
3.3 不要过分依赖自动判断
两种情况:
- 内容太短,两种编码范围均在内。
- 文件太大,前面只有英文,难道需要读完10GB的文本来判断编码?
第二种情况比较好理解,
而第一种情况,内容中只有很少的汉字,如UTF8编码的 【跃跃】,【珊珊】:
跃(UTF8) = E8 B7 83
珊(UTF8) = E7 8F 8A
那么可爱的叠词就是:
跃跃(UTF8) = E8 B7 83,E8 B7 83
珊珊(UTF8) = E7 8F 8A,E7 8F 8A
如果我们2字节一组看:
跃跃(UTF8) = E8 B7 83,E8 B7 83 = E8 B7,83 E8,B7 83 = 璺冭穬(GB18030)
珊珊(UTF8) = E7 8F 8A,E7 8F 8A = E7 8F,8A E7,8F 8A = 鐝婄強(GB18030)
虽然不是常用字,但就能断定当作GB18030是错误的么?
可能跃跃和珊珊看起来太正常,
换个例子 【趃珋】 和 【瓒冪弸】 到底谁才对呢?
😄😄😏
(四)延伸讨论:Oracle的字符集
注意Oracle大概是这样的:
- Oracle服务端即使是英文字符集比如ISO8859p1也可以存储中文。
- 只需要Oracle客户端与服务端字符集设置一致。
- 有一种编码除外:服务端AL32UTF8,客户端可设置简中ZHS16GBK / 繁中ZHT16BIG5。
- 当服务端AL32UTF8时,客户端不应该设置成AL32UTF8。
原理如上,但是还要小心有坑:
- 各种客户端软件工具对字符集处理不同。
- Java8不使用Oracle客户端字符集(蛤???)。
比如试过下面几种客户端工具(不代表所有版本!):
- TOAD:不按照NLS_LANG环境变量,无法设置字符集,只支持ZHS16GBK。
- PL/SQL:按照NLS_LANG环境变量,无法设置字符集,但导入数据到AL32UTF8时实际是ZHS16GBK编码,查询时可以同时正确展示AL32UTF8和ZHS16GBK的中文。
- Navicat:按照设置的字符集导入和查询,但导入时中文字符经常出错。
Java的用阿里巴巴的德鲁伊勉强解决了,可以参考之前遇到的问题。
这个:🔗《Oracle数据库字符集为WE8ISO8859P1存储中文和Java读写展示》
以及:🔗《Oracle数据库字符集为WE8ISO8859P1存储中文和客户端程序展示中文问题》
(四)延伸讨论:FTP的字符编码
我们开发FTP也很容易遇到乱码,但是成熟的FTP工具一般不会。
那是因为别人判断得仔细,会详细询问FTP支持的指令,包括命令编码方式等。
- 简单说如果FTP服务端用UTF8,那么不需要任何转换。
- 如果服务端不是UTF-8,则我们需要把我们的GBK字节码,强行转成服务端的编码(包括8859-1一类)。
名称相关的指令,list,put,get,凡是有目录名,文件名的地方都调用一下。
为啥GBK转8859-1,不是UTF8转8859-1呢,因为如果它是UTF8就已经支持中文了啊!!!
嗯,这是个逻辑问题……
部分代码如下:
public String FromServerEncodingString(String aOriString) throws Exception {
if (ftp.getControlEncoding().equalsIgnoreCase("UTF-8")) return aOriString.trim();
else return new String(aOriString.getBytes(ftp.getControlEncoding()), "GBK").trim();
}
public String ToServerEncodingString(String aOriString) throws Exception {
if (ftp.getControlEncoding().equalsIgnoreCase("UTF-8")) return aOriString.trim();
else return new String(aOriString.getBytes("GBK"), ftp.getControlEncoding());
}
不过好在SFTP也就是SSH协议,似乎都是统一的UTF8编码。
吐槽一下FTP真是一个松散的协议啊!!!
暂时到此,以后遇到新情况再说。😴