探究乱码问题的本源:GBK,UTF8,UTF16,UTF8BOM,ASN1之间的关联


前言

身为一个小菜鸟,前段时间经常被字符编码问题所折磨。从git上拉一个代码下来,中文注释显示乱码,一用vscode打开项目,文件的编码五花八门,更恐怖的是一个文件当中有的中文能正常显示有的则是乱码;可执行程序含有中文输出时,不管是输出到文件当中还是输出到终端当中有时也会出现乱码,而其他同事执行又一切正常;一次在windows编译openssl时,一直提示某个文件找不到#endif,我反复检查无数遍确定是有的,我又把他删除在给他码上,还是这个提示,经过反复折腾,一个资深老鸟叫我把编码方式改成utf8 bom,最后终于成功了,从此我对他深深的崇拜。经过这些折腾,我倦了,决定牺牲休息时间,好好把他总结一番。


一、字符集和字符编码的区别和联系

字符集: 字符集就是许多字符的一个集合,例如:ascii码就是所有英文字符和各种符号的一个集合,gbk就是中国人所有要用到字符的一个集合。
字符编码: 将字符集编码成指定集合的某一对象(如:比特,电脉冲),以便文本在计算机中存储和通过通信网络传递。对程序员来说,就是将字符编码成二进制存储或在网络传递。
区别和联系: 每一种字符集都有一张不同的码表,这张表将字符与数字一一对应,当存储字符时,只需要查找码表,找到该字符对应的数字,然后该数字就能被转换成二进制被计算机识别了。常见的字符集有ascii、gbk、unicode,这三种字符集对应有三张表,其中gbk和unicode包含ascii这张表,在英文字符的对应关系上是一致的,所以乱码问题常出现在中文字符而不是英文字符。当找到字符对应的数字时,ascii和gbk是直接将数字转换成二进制,而unicode会将数字经过不同的处理转换成二进制,狭义上说,字符编码就是将字符对应数字转换成二进制的方式。ascii、gbk字符集转换成二进制不需要特殊处理所以编码方式也叫做ascii和gbk编码,而unicode转换成二进制的方式有多种,包括utf8,utf16,utf32编码。

二、字符集编码的发展

1.单字节

计算机开始产生时只支持ascii,传入欧洲后对ascii码进行扩展,根据每个国家的地区形成了许多标准,如ISO-8859-1、ISO-8859- 2、ISO-8859-3。

2.双字节

计算机传入亚洲,亚洲国家的字符更多,一个字节最多表示256个字符,所以亚洲的国家需要两个字节表示所有的字符,后根据不同地区对ascii进行扩展形成自己的标准,比如中国的GB2312和GBK,其中GBK是对GB2312的扩展,包含了更多的汉字,GBK码表包含GB2312。ANSI编码是对两字节本地化编码的一个统称,在简体中文系统下,ANSI就代表GB2312编码,在日文系统下ANSI就代表 JIS 编码。
例子: 计算"陈a"字的GBK编码
先在GBK码表上查"陈"对应的码值为:B3C2。GBK直接存储对应的码值。所以汉字存储为2字节。
ascii码存储为1字节。

3.多字节(UNICODE字符集)

由于不同的国家有着不同的编码标准,在互联网时代对跨语言的文本信息转换不是很方便。所以unicode字符集将世界上各种语言的每个字符定义一个唯一的编码,其中ascii这个鼻祖仍然不变,但是中文字符对应的编码与GBK不同。
编码方式有三种:

(1)UTF-8

是一种变长字符编码,被定义为将码值编码为 1 至 4 个字节,具体取决于字符在UNICODE字符集所对应的码值的大小。
下表为 Unicode 值对应的 utf8 需要的字节数量。

unicode 编码(16 进制)      UTF-8 字节流(二进制)
000000 - 00007F       0xxxxxxx (ascii 码)
000080 - 0007FF      110xxxxx 10xxxxxx
000800 - 00FFFF       1110xxxx 10xxxxxx 10xxxxxx
01 0000 - 10 FFFF      11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

例子: 计算"陈"字的utf8编码
先在unicode码表上查"陈"对应的码值为:9648,在表格的第三行。转换成二进制10010110 01001000,按照第三行格式进行编码11101001 10011001 10001000,转换成十六进制为:E99988。
所以一个常用的汉字需要3个字节存储。ascii存储需1字节。通过读取前缀就能够知道几个字节代表一个字符。

(2)UTF-16

utf-16使用2个字节或者4个字节来存储。

  1. 对于 Unicode 编号范围在 0~FFFF 之间的字符,UTF-16 使用两个字节存储,并且
    直接存储 Unicode 编号,不用进行编码转换,这跟 UTF-32 非常类似。
  2. 对于 Unicode 编号范围在 10000~10FFFF 之间的字符,UTF-16 使用四个字节存储。
unicode 编码(16 进制)      UTF-16 编码(二进制)
0000 0000~0000 FFFF       xxxxxxxx xxxxxxxx
0001 0000~0010 FFFFF      110110yy yyyyyyyy 110111xx xxxxxxxx

(3)UTF-32

UTF-32 是固定长度的编码,始终占用 4 个字节,足以容纳所有的 Unicode 字符,所
以直接存储 Unicode 编号即可,不需要任何编码转换。浪费了空间,提高了效率。

(4)UTF BOM

UTF-16,UTF-32都是多个字节表示一个字符,就存在大小端的问题。BOM(Byte Order Mark)字节序(字节顺序的标识),其实就是用大端(BE)还是小端(LE)。UTF16,UTF32又可以继续细分成大端存储和小端存储。
UTF-16BE,其后缀是 BE 即 big-endian,是UTF16的大端存储。UTF-16LE,其后缀是 LE 即 little-endian,是UTF8的小端端存储。UTF32同理。当没有后面的标识即UTF-16,就只能看存储文件的BOM来辨别。
UTF 在文件中的存储。UTF 格式在文件中总有固定文件头:

UTF 编码      Byte Order Mark
UTF-8        EF BB BF
UTF-16LE      FF FE
UTF-16BE       FE FF
UTF-32LE      FF FE 00 00
UTF-32BE      00 00 FE FF

例子: 计算"陈"字的utf16 utf32编码
先在unicode码表上查"陈"对应的码值为:9648。UTF16对码值直接存储。
UTF16BE编码:FEFF9648
UTF16LE编码:FFFE4896
UTF32BE编码:0000FEFF00009648
UTF32LE编码:FFFE000048960000
由于UTF8是多字节编码,单字节时就没有大小端的说法,所以UTF8统一成大端法。由于UTF16、UTF32都带有BOM,能够从BOM中分辨UTF16和UTF32编码,所以UTF8的BOM可以省略,但是有的编辑器或者编译器需要BOM来识别UTF8。

三、不同编码方式的对比分析

存储空间对比: GBK编码:ASCII码占用1字节,汉字占用2字节。UNCODE字符集中的UTF8编码:ASCII码占用1字节,汉字占用3字节。UTF16:汉字和ascii码都占用2字节。UTF32:汉字和ASCII码都占用4字节。所以不管汉字和ascii码的比例多少,gbk编码最节省空间。当汉字较多UTF16比UTF8节省空间。当ascii码过多则UTF8更节省空间。UTF32不管怎样都是最浪费空间的。
GBK和UNICODE对比: GBK是中国人自己的标准,在编程中不能够使用其他国家的字符,需要单独的GBK码表才能解码,当访问GBK编码的网页时,需要有单独的中文语言包支持。UNICODE则是一种国际化的方式,全世界使用一张码表,你能够使用其他国家的字符,也能解码其他国家的字符,全世界人民都能直接访问UNICODE编码的网页。
UTF-8和UTF-16对比: UTF-8没有字节序的概念,不用考虑大小端问题,特别适合用于字符串的网络传输。UTF-8属于变字节存储,对ascii码来说没有区别,对汉字来说一个汉字要占3个char,所以不能在通过字符数组的下表来操作汉字。UTF-16由于ascii码也是2个字节存储,可以将其与汉字进行统一,用wchar_t来进行存储,一个wchar能过存储一个汉字或字符,能够方便的通过下标来进行字符操作。但是wchar_t在windows和linux没有进行统一,wchar_t在windows中占用2字节,wchar_t在linux中占用4字节,所以跨平台会有一定问题。

四、乱码出现的原因分析及避免

根据目前的知识水平暂时将乱码分成以下几种:

1.用一种编辑器打开某个文件出现乱码。

原因: 保存文件的字符编码和打开文件的字符编码不兼容,例如,保存文件的编码以GBK进行保存,而打开文件用UTF8打开就会出现乱码。因为GBK和UNICODE需要查找不同的表,如果文件编辑器按照UTF8的方式解码查找UNICODE表就会出现乱码。以UTF8保存的文件以UTF16打开可能也会出现乱码,因为虽然都是查找一张表但是对原文件的读取按照UTF16的格式进行解码。
解决办法: linux下用file -i查看保存文件的编码方式,编辑器用同样的编码方式进行打开;也可以用命令iconv将文件转换成能够正确打开的编码方式。windows下直接用nodepade进行打开选择能够解码的方式。如果文件为二进制文件(不能被某一编码所识别)则不论以何种方式打开都会出现乱码,可以用base64编码将二进制文件可视化成字符串进行打开。
总结: 猜想一个文本编辑器打开一个文件的过程大概是,读取二进制数据,通过二进制数据的特殊格式如BOM,能够知道该二进制数据的编码方式,然后按照特定的解码方式通过查找特定的码表来获取字符,然后显示到文件编辑器当中。所以一个成熟的编辑器打开一个文件时,它应该能够自己以特定的编码方式进行打开。注意:当打开文件是一个乱码文件时,如果你以该编码将这个乱码文件保存了,那么这个乱码文件将会永久的乱码,不论哪种编码都不能使其恢复正常。

2.可执行程序将字符输出到终端或者文件当中出现乱码。

原因: 可执行文件的字符编码方式与输出终端或文件打开的编码方式不一致。
解决办法:
(1)控制输出程序中字符的编码方式
在linux系统和windows中用gcc进行编译实验得:直接对源文件进行编译时,编译的可执行程序的字符编码格式与源文件的字符编码格式一致。例如源文件保存编码格式为GBK,编译的可执行程序格式也是GBK,源文件保存编码格式UTF8,编译可执行程序字符格式也是UTF8,往终端输出时也是UTF8格式。源文件只能是GBK和UTF8两种形式才能编译通过。
通过编译选项控制可执行程序字符的编码格式:

-finput-charset=utf-8  //源文件的编码格式
-fexec-charset=gbk   //可执行程序中字符的编码格式
-fwide-exec-charset=utf-16LE  //源文件中宽字符的输出格式

当指定-finput-charset而不指定输出格式时,默认输出可执行程序为UTF8,当指定-fexec-charset 时,就能输出指定可执行程序的格式。输入输出的格式都只能指定成GBK或UTF8的一种。当使用了wchar_t存储宽字符时,可以用-fwide-exec-charset 选项指定宽字符的输出编码格式,不指定编译器也能进行编译。
(2)控制终端输出的编码方式
linux终端的输出方式默认为utf8格式,如果可执行程序为GBK就会出现乱码。
查看ubuntu下的语言环境:
在这里插入图片描述
LC_ALL和LANG优先级的关系:LC_ALL > LC_* >LANG 。LC_*中每一条都代表具体某一方面系统输出字符串的编码方式,LANG是默认输出方式,LC_ALL能够控制所有的输出方式。
可以export LANG=XXX.XX 方式临时改变。也可以修改/etc/envirnment永久修改。
运行export LC_ALL=zh_CN.GBK。
在这里插入图片描述此时系统往终端输出的字符串都是gbk编码。接下来还需要将终端修改成显示GBK编码。这样能够保证终端运行Linux命令出现中文时不会出现乱码。
直接通过以下操作将输出终端改成GBK,但是是临时的。
在这里插入图片描述
在这里插入图片描述
windows的cmd默认是GBK,如果可执行程序输出的是UTF8就会出现乱码。
查看cmd默认的编码方式:
在这里插入图片描述

   序号      编码方式
65001        utf-8
20936      GB2312
936       GBK

输入chcp 65001 修改成utf-8编码方式

总结: 为了避免出现乱码要注意:1.源文件保存的编码格式。2.可执行程序中字符的编码格式,和源文件的编码格式有关联。3.输出终端或者文件的编码格式。这几种编码格式最好保持统一。

3.编译时出现奇奇怪怪的字符报错。

1.在windows下编译openssl时,提示某个文件结尾的宏#endif找不到,然而文件里面确实是有的。将UTF8格式转换成UTF8BOM格式后成功编译。可能原因是,编译器要通过BOM来识别该文件为UTF8格式,但是有的文件也为UTF8就没得事情。
2.如果直接编译UTF16保存的文件也会出现各种奇怪报错。
3.将windows脚本拷贝到Linux系统下,运行脚本也会出现奇怪的报错。这个和编码没有关系。windows中的为DOS格式,而linux下为unix格式,dos格式文件传输到linux系统时,会在每行的结尾多一个^M,当然也有可能看不到。

vim 打开文件
set fileformat //查看文件格式
set fileformat=unix  //改变文件格式

4.开源组件出现字符乱码。

(1)nginx出现中文乱码
解决:conf 文件需要加入 charset utf-8;
(2)-redis 命令行时出现中文乱码
解决:单纯以 redis-cli 启动命令行,在 set 中文 value 是再 get 出现乱码。附加–raw 参数启动即可,即是 redis-cli --raw 启动命令行。

总结

当我们程序有中文字符时就要注意源文件的编码方式和可执行程序字符的编码方式,一个大工程里面尽量保证所有的文件编码格式一致,要不然就会陷入各种编码问题的苦海中。目前水平有限就总结这些,以后遇到其他情况在添加。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ASN1Encodable 对象转换为字符串时出现码的原因可能是由于编码方式不正确或者缺少字符集的处理。你可以尝试使用不同的字符集来解决这个问题。 下面是一个示例代码,使用 UTF-8 字符集将 ASN1Encodable 对象转换为字符串: ```java import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.util.encoders.Hex; import java.nio.charset.StandardCharsets; public class ASN1Encoder { public static String encodeToString(ASN1Encodable asn1Encodable) { try { byte[] encoded = asn1Encodable.toASN1Primitive().getEncoded(); return new String(Hex.encode(encoded), StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); } return null; } public static void main(String[] args) { // 假设 asn1Encodable 是要转换的 ASN1Encodable 对象 ASN1Encodable asn1Encodable = null; String encodedString = encodeToString(asn1Encodable); System.out.println("Encoded String: " + encodedString); } } ``` 在这个示例中,我们将字节数组使用 UTF-8 字符集进行解码,以确保正确处理编码和字符集。你可以根据实际情况选择适合的字符集,例如 UTF-8、UTF-16、ISO-8859-1 等。 如果仍然遇到问题,可能是因为 ASN1Encodable 对象的内容无法直接转换为字符串,或者需要根据特定的编码规则进行解析。在这种情况下,你可能需要了解 ASN.1 数据结构的具体格式和编码规则,并根据实际情况进行解析和处理。 请注意,具体的转换方式可能因不同的编程语言、库或工具而有所不同。上述示例代码仅提供了一个基本的参考,并使用了 Bouncy Castle 库作为示例。在实际应用中,你可以根据具体的需求和使用的工具来进行相应的转换和处理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值