前言
近日发现请假系统查询页面导出的TSV
文件里部分中文字符不存在(例如“玥”),于是我和学长开启了新一轮的Debug
之旅。
解决
原始关键代码逻辑如图所示:
其中,//IGNORE
声明忽略编码时候的错误。
经过反复调试输出文件测试得知:
Excel
默认不会以Utf-8
编码解析中文字符,换句话说就是用它打开Utf-8
编码的表格会默认乱码。
然而部分中文字符不显示的原因则在于GB2312支持的中文数太少了,作为最早一版的中文编码,每个字占据2bytes
。由于要和ASCII
兼容,那这2bytes
最高位不可以为0了(否则和ASCII
会有冲突)。在GB2312
中收录了6763
个汉字以及682
个特殊符号,仅囊括了生活中最常用的所有汉字(存名字肯定是不够的)。
因此我们需要用更大的编码集,具体关系见下图:
随后我们看了详细区别后就直接用GB18030//IGNORE
进行了测试,导出结果与原字符以及Excel
的兼容性完美,解决问题!
知识
本部分节选自:程序员必备:彻底弄懂常见的7种中文字符编码
ASCII编码
ASCII
编码每个字母或符号占1byte
(8bits
),并且8bits
的最高位是0,因此ASCII能编码的字母和符号只有128个。有一些编码把8bits
最高位为1的后128个值也编码上,使得1byte
可以表示256个值,但是这属于扩展的ASCII
,并非标准ASCII
。通常所说的标准ASCII
只有前128个值!
ASCII
编码几乎被世界上所有编码所兼容(UTF16
和UTF32
是个例外),因此如果一个文本文档里面的内容全都由ASCII
里面的字母或符号构成,那么不管你如何展示该文档的内容,都不可能出现乱码的情况。
GB2312
、GBK
、GB18030
编码
GB
全称GuoBiao
国标,GBK
全称GuoBiaoKuozhan
国标扩展。GB18030
编码兼容GBK
,GBK
兼容GB2312
,其实这三种编码有着非常深厚的渊源,我们放在一起进行比较。
【GB2312
】
最早一版的中文编码,**每个字占据2bytes**
。由于要和ASCII
兼容,那这2bytes
最高位不可以为0了(否则和ASCII
会有冲突)。在GB2312
中收录了6763
个汉字以及682
个特殊符号,已经囊括了生活中最常用的所有汉字。(GB2312
编码全表:链接)
GB2312
编码表有个值得注意的点,这个表中也有一些数字和字母,与ASCII里面的字母非常像。例如A3B2
对应的是数字2(如下图),但是ASCII
里面50(十进制)对应的也是数字2。他们的区别就是输入法中所说的“半角”和“全角”。全角的数字2占两个字节。
通常,我们在打字或编程中都使用半角,即ASCII来编写数字或英文字母。特别是编程中,如果写全角的数字或字母,编译器很有可能不认识……
【GBK
】
由于GB2312
只有6763个汉字,我汉语博大精深,只有6763个字怎么够?于是GBK
中在保证不和GB2312
、ASCII
冲突(即兼容GB2312
和ASCII
)的前提下,也用每个字占据2bytes
的方式又编码了许多汉字。经过GBK
编码后,可以表示的汉字达到了20902个,另有984个汉语标点符号、部首等。值得注意的是这20902个汉字还包含了繁体字,但是该繁体字与台湾Big5
编码不兼容,因为同一个繁体字很可能在GBK
和Big5
中数字编码是不一样的。(GBK
编码全表:链接)
【GB18030
】
然而,GBK
的两万多字也已经无法满足我们的需求了,还有更多可能你自己从来没见过的汉字需要编码。
这时候显然只用2bytes
表示一个字已经不够用了(2bytes
最多只有65536
种组合,然而为了和ASCII
兼容,最高位不能为0就已经直接淘汰了一半的组合,只剩下3万多种组合无法满足全部汉字要求)。因此GB18030
多出来的汉字使用4bytes
编码。当然,为了兼容GBK
,这个四字节的前两位显然不能与GBK
冲突(实操中发现后两位也并没有和GBK
冲突)。
我国在2000年和2005年分别颁布的两次GB18030
编码,其中2005年的是在2000年基础上进一步补充。至此,GB18030
编码的中文文件已经有七万多个汉字了,甚至包含了少数民族文字。有兴趣的可以到国家标准委官网了解详情,链接
GB2312
,GBK
,GB18030
都是采取了固定长度的办法来解决字符分隔(即前文所提的第2件事情)问题。GBK
和GB2312
比ASCII多出来的字都是2bytes
,GB18030
比GBK
多出来的字都是4bytes
。至于他们具体是如何做到兼容的,可以参考下图:
这图中展示了前文所述的几种编码在编码完成后,前2个byte
的值域(用16进制表示)。每个byte
可以表示00到FF(即0至255)。ASCII
编码由于是单字节,所以没有第2位。因为GBK
兼容GB2312
,所以理论上上图中GB2312
的领土面积也可以算在GBK
的范围内,GB18030
也同理。
上图只是展示出了比之前编码“多”出来的面积。GB18030
由于是4bytes
编码,上图只是展示了前2bytes
的值域,虽然面积最小,但是如果后2bytes
也算上,GB18030
新编码的字数实际上远远多于GBK
。
可以看出为了做到兼容性,以上所有编码的前2bytes
做到了相互值域不冲突,这样就可以允许几种不同编码中的文字同时出现在同一个文本文件中。只要全都按照GB18030
编码的规则去解析并展示文件,就不会有乱码出现。实际业务中GB18030
很少提到,通常GBK
见得比较多,这是因为如果你去看一下GB18030
里面所编码的文字,你会发现自己一个字也不认识……
UTF8
编码(Unicode Transformation Format)
99%的前端写网页时都会加上<meta charset="utf-8">
,99%的后端工程师新建数据库表时都会加上DEFAULT CHARSET=utf8
(剩下的1%应该是忘了写)。
之所以我们想让UTF8
一统天下,就是因为UTF8
可以表示出世界上所有的文字!UTF8
与前面说的GB
系列编码不兼容,所以如果一个文件中即有UTF8
编码的文字,又有GB18030
编码的文字,那绝对会有乱码。
Unicode
赋予了全世界所有文字和符号一个独一无二的数字编号,UTF8
所做的事情就是把这个数字编号表示出来(即解决前文提到的第2件事情)。UTF8
解决字符间分隔的方式是数二进制中最高位连续1的个数来决定这个字是几字节编码。0开头的属于单字节,和ASCII
码重合,做到了兼容。
以三字节为例,开头第一个字节的”1110
”,有连续三个1,说明包括本字节在内,接下来三个字节一起构成了一个文字。凡是不属于文字首字节的byte
都以“10”开头,上表中标注X的位置才是真正用来表示Unicode
数值的。
这种巧妙设计,把Unicode
的数值和每个字的字节数融合在一起,最坏情况是6个字节表示一个字,已经足够表示世界上所有语言的所有文字了。不过从这种表示方式也可以很显然地看出来,UTF8
和GBK
没有任何关系,除了都兼容ASCII以外。
举例说明,中文“鹅”字,Unicode十进制值为40517(16进制为9E45
,2进制为1001 1110 0100 0101
)。这个2进制值长度为12位,查询上面表格发现,二字节不够表示,四字节太长,三字节刚好,因此可以表示为 11101001 10111001 10000101
,换算为16进制即E9B985
,这就是“鹅”字的UTF8
编码,占3字节。另外,经查询,“鹅”的GBK
编码为B6EC
,和UTF8
的值完全不相干。
对于中文汉字来说,所有常用汉字的Unicode值都可以用3字节的UTF8
表示出来,而GBK
编码的汉字基本是2字节(GB18030
虽4字节但是日常没人会写那些字)。这也就导致了,如果把GBK
编码的中文文本另存为UTF8
编码,体积会大50%左右。这也是UTF8
的一点小瑕疵,存储同样的汉字,体积比GBK
要大50%。
不过在“可表示世界上所有文字”这一巨大优势面前,UTF8
的这点小瑕疵可以忽略了,所以日常开发中最常使用UTF8
。
其他经常遇到的编码
【ANSI
编码】
准确说,并不存在哪种具体的编码方式叫做ANSI
,它只是一个Windows
操作系统上的别称而已。在中文简体Windows
操作系统上,ANSI
就是GBK
;在泰语操作系统上,ANSI
就是TIS-620
(一种泰语编码);在韩语操作系统上,ANSI
就是EUC-KR
(一种韩语编码)。并且所谓的ANSI
只存在于Windows
操作系统上。
【Latin1
编码(又名ISO-8859-1
编码)】
相信99%的人第一次听到Latin1
都是在使用Mysql
数据库的时候接触到的。Latin1
是Mysql
数据库表的默认编码方式。Latin1
也是单字节编码方式,也就是说最多只能表示256个字母或符号,并且前128个和ASCII
完全吻合。
Latin1
在ASCII基础上又充分利用了后面那128个值,赋予他们一些泰语、希腊语等字母或符号,将1个字节的256个值全部占满了。因为项目中用不到,我对这种编码的细节没兴趣了解,唯一感兴趣的是为什么Mysql
选它做默认编码(为什么默认编码不是UTF8
)?以及如果忘了设置Mysql
表的编码方式时,用Latin1
存储中文会不会出问题?
为什么默认编码是Latin1
而不是UTF8
?原因之一是Mysql
最开始是某瑞典公司搞的项目,故默认collate都是latin1_swedish_ci
。swedish
可以理解为其私心,不过latin1
不管是否出于私心目的,单字节编码作为默认值肯定是比多字节做默认值更不容易在插入数据时报错。
既然Latin1
为单字节编码,并且将1个字节的所有256个值全部占满,因此理论上把任何编码的值塞到Latin1
字段都是可以存的(无非就是显示乱码而已)。
假设默认为UTF8
这一多字节编码,在用户误把一个不使用UTF8
编码的字符串存进去时,很有可能因为该字符串不符合UTF8
的编码要求导致Mysql
根本没法处理。这也是单字节编码的一大好处:显示可以乱码,但是里面的数据值永远正确。
用Latin1
存储中文有没有问题?答案是没有问题,但是并不建议。例如你把UTF8
编码的“讯”字(UTF8
编码为0xE8AEAF
,占三个字节)存入了Latin1
编码的Mysql
表,那么在Mysql
眼里,你存入的并不是一个“讯”字,而是三个Latin1
的字母(0xE8
,0xAE
,0xAF
)。本质上,你存的数据值依然是0xE8AEAF
,这种“欺骗”Mysql
的行为并没有导致数据丢失,只不过你需要注意读取出来该值的时候,自己要以UTF8
编码的方式显示出来,要不然就是乱码。
因此,用Latin1
存任何文字技术上都可以,但是经常会导致数据显示乱码。通常的解决方案,就是让UTF8
一统天下,建表的时候就声明charset
为utf8
。
文章最后,遗留一个问题。既然有这么多编码形式,如果给定一个文本文件,不告诉你是什么编码,如何用程序进行检测?比较有代表性的编码检测库为python
的chardet
,其具体的原理,且待下回分解~
参考资料:
https://zhuanlan.zhihu.com/p/46216008
https://zhidao.baidu.com/question/470888931.html
https://blog.csdn.net/snow_love_xia/article/details/80001878
版权属于:soarli
本文链接:https://blog.soarli.top/archives/638.html
转载时须注明出处及本声明。