字符编码和字符集到底有什么区别?Unicode和UTF-8是什么关系?
前⾔
想必⼤家编写代码时肯定和我⼀样,也遇到过汉字乱码的问题。特别是,有时候和上下游对接接⼝,不能统⼀编码格式的话,⼀堆乱码问题,让⼈头⽪发⿇。
那么为什么会有这么多的乱码问题?
什么是字符编码?什么是字符集?他们之间有什么区别和联系?
什么是 Unicode ? Unicode 和我们常说的 UTF-8 ⼜有什么关系?
字符编码和解码
要想搞清楚上⾯的问题,⾸先我们要知道,在计算机中,不管是⼀段⽂字、⼀张图⽚还是⼀段视频,最终都是以⼆进制的⽅式来存储。也就是最终都会转化为0001 1011 0010 0110这样的格式。
换句话说,计算机只认识 0 和 1 这样的数字,并不能直接存储字符。所以我们需要告诉它什么样的字符对应的是什么数字。
例如,我们的业务中有记录客户端的客户⾏为⽇志,然后导出⽂件来分析,字段间会以ESC来分隔。java语言使用的字符码集是
我在编写代码的时候,就需要定义⼀下这个ESC字符应该对应什么数字,这样计算机才能识别并存储。
⽐如我把它定为0001 1011,这样计算机就把ESC这个字符存了下来。等我下次需要查看的时候,根据对应关系把它解出来就可以了。
上边的两个过程就对应字符的编码和解码过程。
字符编码就是把字符按⼀定的规则,转换成数字。字符解码是编码的逆过程,即把数字按规则转换成字符。
这样看来,貌似没有什么问题。
但是,这是我⾃⼰定义的编码规则,我同桌阿霄就不乐意了。他⾮要认为ESC应该定义为1101 1000,好家伙正好和我定义的⼆进制数字顺序相反。
那结果肯定不⽤说了,我把0001 1011这串数字给他之后,按照他的编码规则来解,肯定是&$#!这样的东西。
所以,乱码问题说到底,就是编码和解码的规则对应不上导致的。
ASCII 码
为了避免我和阿霄因为编码问题打起来,美国国家标准学会(AMERICAN NATIONAL STANDARDS INSTITUTE) ANSI 组织发话了。
停、停、停。不就是个编码问题吗,这种⼩事犯不着动⼿,我定义⼀个统⼀的规则,⼤家都按照我的规则来编码和解码不就好了嘛。
于是,ASCII 码出现了,它定义了⼀个常⽤字符集,⽤来表⽰字符和数字的对应关系,如下表。
ASCII 码全称:美国信息交换标准代码 (American Standard Code for Information Interchange)
我⼀查表,ESC字符不就对应 27 吗,对应的⼆进制就是0001 1011。我去,没想到我定义的规则竟和 ANSI 不谋⽽合。
同桌阿霄把抡在空中的拳头收了起来,默默地回去敲代码了。
ASCII 码扩展码
在使⽤英语的国家,ASCII 码就⾜够⽤了。但是,在其他欧洲发达国家⽐如法国,使⽤的语⾔是法语,有类似于这样的á符号,ASCII 码就不能表⽰了。那怎么办呢?
我们看上表就会发现,ASCII 码表的表⽰范围是⼗进制0~127,也就是⼆进制0000 0000到0111 1111。其实只是⽤了后边的 7 位,第⼀位都是 0 。
⽽计算机⼆进制中⼀个字节是 8 个位,现在只⽤了 7 位。不⾏啊太浪费了,要充分利⽤第⼀个⾼位,扩展⼀下,这样多了⼀位,能表⽰的字符范围就多了⼀倍。(2的8次⽅
=256)
这样⼀些欧洲其他国家,也能在计算机中表⽰⾃⼰的⽂字了。
后来,随着计算机的普及,中国的⽤户也多了起来。却发现,⼀个字节只能表⽰ 256 个字符,远远不能满⾜我们的要求。
于是,就出现了 GB2312 编码,它使⽤了两个字节来表⽰⼀个汉字。但是,并没有把所有的位都⽤完,前⾯⼀个字节范围 0xA1 ~ 0xF7 (即 10110001 ~ 11110111),后⾯⼀个字节范围 0xA1 ~ 0xFE (即 10110001 ~ 11111110)。这样就能表⽰简体汉字 6763 个。
GB2312 是国家标准总局发布的《信息交换⽤汉字编码字符集》,也可以说是简体中⽂的字符集。
但是,台湾和⾹港等使⽤繁体字的地区怎么办。于是,就有了⼤五码 Big5 编码来存储繁体。⾼字节(第⼀个字节)表⽰范围 0x81~0xFE,低字节(第⼆个字节)表⽰范围 0x40 ~ 0x7E,以及0xA1 ~ 0xFE 。
需要注意的是,GB2312 是简体中⽂,Big5 是繁体中⽂。如果⽤其中⼀种编码⽂字去读另外⼀种编码⽂字就会乱码。所以,就出来了 GBK 编码,把简体中⽂和繁体中⽂,以及⼀些 GB2312 不⽀持的⼈名(如历代总理有的名字⽤ GB2312 打不出来),还有⼀些我们不认识的古汉语都包含进去,共 2 万多个字符。
再然后,我们发现少数民族像藏⽂,蒙古⽂这些少数民族的语⾔,GBK 也⽀持不了,就再进⾏扩展,出现了 GB18030 。⼜多了⼏千个少数民族的⽂字。
所以,我们使⽤的 GB 国标系列⽂字都是在 ASCII 码之上扩展的,它们是依次向下兼容的。表⽰⽂字范围从⼩到⼤为 GB2312 = Big5 < GBK < GB18030 。
Unicode 字符集
我们在打开⼀个⽂档之前,就必须要知道它的编码格式,否则⽤错误的⽅式解码就会出现乱码情况。
设想,如果⼀个⽂本中,有多种类型⽂字,包括中⽂,韩语,德语,⽇语,应该⽤哪种编码⽅式?貌似怎么处理都会有乱码问题,那怎么办呢?
ISO(国际标准化组织)说:这好办啊,我把地球上,只要是⼈们使⽤的,所有语⾔和符号都囊括其中,为每个字符都指定⼀个唯⼀的字符码,这样就没有乱码问题了。于是Unicode 出现了,⼜叫统⼀码,万国码。
如上图表,汉字“⼀”对应的 unicode 码是\u4e00。我们通常在字符码前加个\u代表这是 unicode 码。4
e00 是⼗六进制表⽰。
也有很多在线转码⼯具供我们使⽤,如:
Unicode 编码⽅案
⾸先强调⼀下以下⼏个概念的区别:
0. 字符:就是我们看到的⼀个字母或⼀个汉字、⼀个标点符号都叫字符。如上边的汉字“⼀”就是⼀个字符。
1. 字符码:在指定的字符集中,⼀个字符对应唯⼀⼀个数字,这个数字就叫字符码。如上边的字符“⼀
”,在 Unicode 字符集中,对应的字符码为\u4e00。
2. 字符集:规定了字符和字符码之间的对应关系。
3. 字符编码:规定了⼀个字符码在计算机中如何存储。
需要注意的是,Unicode 只是⼀个字符集,它规定了每个字符对应的唯⼀字符码,却没有规定这个字符码在计算机中怎样存储(也就是它的字符编码格式)。
例如,上边的汉字“⼀”,它的 Unicode 字符码为\u4e00,转换成⼆进制就是100 1110 0000 0000。可以看到,它有 15 位⼆进制数,⾄少需要两个字节来存储。
这只是简单的汉字,如果其他复杂的字符有可能会需要三、四个字节或者更多字节来存储。
那么到底应该⽤⼏个字节来存储呢?
于是 UTF-32 编码制定了标准,⼀个字符就⽤四个字节来表⽰。这样编码和解码都⽅便,固定取 32 位⼆进制就⾏了。
但是这样⼜引来⼀个问题。⽐如 A 字符其实只需要⼀个字节就可以存储了。如果必须要⽤四个字节来存储,那么前边三个字节都要补 0 ,这样势必会造成空间的浪费。
于是 UTF-16 编码(⼀个字符⽤两个字节或者四个字节)和我们熟悉的 UTF-8 编码格式就出现了。
这⾥我们重点介绍 UTF-8 。它使⽤⼀种变长的编码⽅式,可以使⽤ 1~4 个字节来表⽰⼀个字符。根据不同的字符变换长度。
变长听起来很美好,但是它的不固定性,就让计算机懵逼了。⽐如,计算机怎么知道这四个字节代表的是⼀个字符,还是四个字符,亦或是两个字符呢?
于是,UTF-8 规定了以下编码规则,来避免以上问题。
对于单字节的符号,第⼀位设为0,后边 7 位对应这个字符的ASCII码值。因此,像“A"这样的英⽂字母,UTF-8 编码和 ASCII 编码是相同的。
对于⼤于⼀个字节的符号,假设为 n 字节,那么第⼀个字节的前 n 位都设为 1,这样有⼏个 1 就说明有⼏个字节。然后,第 n+1 位设为0 。后边的字节,前两位都设为10,剩余的其他⼆进制位都⽤这个字符的 Unicode 码填充(从后向前填充,不够补0)。
字节个数Unicode符号范围(16进制)UTF-8 编码格式(⼆进制)
1(单字节)0000 0000 ~ 0000 007F0xxxxxx
20000 0080 ~ 0000 07FF110xxxxx 10xxxxxx
30000 0800 ~ 0000 FFFF1110xxxx 10xxxxxx 10xxxxxx
40001 0000 ~ 0010 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
刚开始看上表,可能⽐较懵逼。其实,Unicode 符号表⽰的范围最⼤为四个字节,因此⼆进制为 4*8=32 位。我们知道,⼆进制转换⼗六进制时,以四位为⼀个单位转换,因此,对应的⼗六进制为 32/4=8 位。
上表中的 Unicode 符号范围是以 16 进制表⽰,可以看到就是 8 位的。
我们还是以汉字 “⼀” 为例,16进制表⽰为4e00,补全所有位,其实就是0000 4E00(不区分⼤⼩写)。因此,查上表发现,它处在三个字节的 Unicode 范围内(0000 0800 < 0000 4e00 < 0000 FFFF)。
所以,它⽤ UTF-8 来编码,就是三个字节的,即格式是这样的1110xxxx 10xxxxxx 10xxxxxx。
把4e00转换为⼆进制为100 1110 0000 0000,⼆进制位从后向前依次填充到上述格式中的x位置(也是从后向前填充)。
于是,就得出汉字 “⼀” 的 UTF-8 编码后的⼆进制表⽰为:1110 0100 1011 1000 1000 0000。
其实,可以发现,汉字的⼆进制为 15 位,前边补零⼀位即为 16 位0100 1110 0000 0000。⽽三个字节的 UTF-8 编码格式中的 x 个数也为 3*8 - (4+2+2) = 16 位,正好⼀⼀对应。
那么,我们这⼀通推算,是否正确呢。可以在程序中打印这个字符的⼆进制格式,以及UTF-8编码后的⼆进制。程序如下,
public class Test {
public static void main(String[] args) throws UnsupportedEncodingException {
System.out.println("字符'⼀'的⼆进制为:" + BinaryString('⼀'));
System.out.println("========");
String str = "⼀";
System.out.println("转换为UTF-8编码格式的⼆进制为:"+ toBinary(str,"utf-8"));
}
public static String toBinary(String str, String encode) throws UnsupportedEncodingException {
StringBuilder sb = new StringBuilder();
byte[] bytes = Bytes(encode);
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
sb.BinaryString(b & 0xFF));
}
String();
}
}
打印结果为:
字符'⼀'的⼆进制为:100111000000000
========
转换为UTF-8编码格式的⼆进制为:111001001011100010000000
PS:通常的,我们发现常⽤的汉字以 16 进制表⽰都在0000 0800 ~ 0000 FFFF范围内。因此,汉字在 UTF-8 编码下通常占⽤三个字节。
细⼼的同学可能发现了,我上边转换的汉字可以⽤ char 类型来存储,这是为什么呢?
这是因为,在 Java 中,默认使⽤的字符集就是 Unicode,可以容纳 100 多万个字符,其中就包括汉字。
我们使⽤的绝对⼤多数汉字,都在0000 0800 ~ 0000 FFFF这个范围内,可以看出来前边的四位⼗六进制都⽤不到(都是0000),因此,只需要后边的四位⼗六进制位,转换为⼆进制就是 4*4=16 位,只占⽤了两个字节(16/8=2)。⽽ char 在 Java 中占⽤两个字节,完全可以⽤来存储汉字。
总结
最后,来解答下⽂章开头的问题。
乱码的问题,究其根本原因,其实是编码和解码时的规则不⼀样导致的。
字符编码和字符集是两个不同的概念。⼀句话表⽰:字符集定义了字符到数字的映射关系,字符编码定义了这个数字如何在计算机中表达(存储)。
对于 ASCII 和 GB 系列,他们既是字符集也是字符编码。GB 兼容 ASCII 码。
⽽对于 Unicode 来说,字符集是 Unicode,⽽字符编码可以是 UTF-8,UTF-16 和 UTF-32 。所以,我们平时常⽤的 UTF-8 编码其实只是 Unicode 的⼀种编码实现⽅式⽽已。
本期内容你学会了吗,把学会打在评论区。。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。