【锟斤拷】的故事:谈谈汉字编码和常用字符集


之前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-8UTF-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-80xe4,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客户端字符集(蛤???)。

比如试过下面几种客户端工具(不代表所有版本!):

  1. TOAD:不按照NLS_LANG环境变量,无法设置字符集,只支持ZHS16GBK。
  2. PL/SQL:按照NLS_LANG环境变量,无法设置字符集,但导入数据到AL32UTF8时实际是ZHS16GBK编码,查询时可以同时正确展示AL32UTF8和ZHS16GBK的中文。
  3. 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真是一个松散的协议啊!!!


暂时到此,以后遇到新情况再说。😴

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值