从0开始学习字符编码

611 阅读24分钟

有一些知识你无时无刻不在接触,但是一旦让你回答,可能除了它的名字就回答不上其他什么了,字符编码就是这样,大家都知道UTF-8和GBK,可能还会知道中文字符用GBK的比较多,但是UTF-8不是也支持中文字符吗?为什么我们会用到这两种编码呢,为什么没有统一使用一种?ASCII码也是,最开始计算机基础就有学过它,但还是不知道它和其他编码的关系。本节,我们就详细的说一说,字符编码。

 

一、ASCII码

1.ASCII码的诞生

首先,计算机最终是二进制的,也就是让它表达一下数字,可以用二进制表达出来。但是如果让它表达字符,就没那么简单了,不同的字符之间毫无规律,所以只能用每个数字,代表一个独一无二的字符,像元素周期表一样,一对一。

ASCII码就是这样诞生的,在上世纪60年代,由美国制定的一套字符编码。它一共定义了128个字符,从二进制来说就是7位,从000 0000到111 1111的128种组合方式,从十进制说就是0-127。

2.ASCII码到底是几位

根据前面的分析,ASCII码应该是7位的,它不需要用到第八位就已经给所有符号用完了。英语就26个字母,老外能绞尽脑汁凑到128个字符出来已经很努力了,你再让它整128个出来,这不是拿头给你想吗。所以最开始它肯定是7位的,第八位有也是固定为0。

而现在很多说法也有说它是8位的,一方面是扩展,不同地区可能会将ASCII码剩下的128个位利用起来,扩展使用。这就导致不同国家地区的扩展后的ASDII码各不相同,当然前128位是固定死的,后面的128位就差距很大了。又有些人说计算机通常是用8位表示ASCII码,因为第8位是用来做奇偶校验的,既然有使用,所以是ASCII就是8位。

很多不同的理由,但个人感觉ASCII还是算7位的,扩充后的ASCII码自然也不是最原始的ASCII码了,更何况还有那么多种不同的扩充版本。

 

 

二、不同国家的编码

扩展ASCII码只是第一步,像欧洲一些国家,使用的语言和英文差别不大,它们就可以把原本闲置的第八位用起来,加上自己需要的一些符号进去。当然不同的国家肯定会有不同的语言符号,甚至同一个符号有不同的含义和读法,所以差异就产生了。

这些国家还好,如果是亚洲国家,那就更没法表达了。不管是128还是256都是远远无法表达如此数量庞大的汉字的,所以我们汉字也创建了属于自己的字符集:GB2312,它是中国国家标准的简体中文字符集,并且兼容ASCII码。不同的国家基本上都有自己的本土化的字符集。

 

1.字符编码

严格来说,字符集只是一种字符的集合,它和计算机是没有直接关联的。所以需要将字符进行二进制编码,将不同的字符用特定的方式存储到计算机中。

第一层编码,我们需要给每个字符通过某种规则定下一个唯一对应的数字,这是一对一的,例如ASCII码中,"A"表示数字65。

第二层编码,存储长度(字节数),是采用固定的存储长度,还是根据不同字符有不同变化的。例如ASCII码就是用一个字节存储的。

第三层编码,存储格式,大端法还是小端法,这一层通常影响不大,有时是由系统决定,例如X86的机器采用的是小端法。

2.字符集(charset)和字符编码(encoding)

通过字符编码的步骤,按理说字符集和编码方式应当是分开的,字符集纯放字符,然后编码方式制定了123层的编码规则。

但实际上并不是这样,至少通过我的查阅,发现很多字符集默认等于编码方式,严格的说,至少也是和第一层编码捆绑的。也就是在字符集发明的时候,也同时制定好了编码规则,并没有严格的区别字符集和字符编码,界限比较模糊。或许是因为在那个时间,每种字符集和它的编码方式都是一对一的,且目的都是为了计算机编码使用,所以没有必要进行区分吧。

就像现在我们的GB2312虽然是字符集,但是也同时是字符编码。

直到Unicode的出现,大家才有意识将字符集编码区分开来,因为Unicode有了不同的编码方式。

字符集:charset,即character set 的简写

字符集编码:encoding,是 charset encoding 的简写,通常直接简称编码

 

 

三、Unicode

1.Unicode的诞生

既然不同国家有了不同的字符集和编码方式,同样一句话,我用的字符集和编码方式和你那边的截然不同,那就肯定会产生问题。例如我打了“你好”两个字,在国内自然大部分都是用相同的字符集和编码方式的,所以互相发消息没问题。但如果跨了不同的国家,它那边又没有安装你的字符集,怎么可能转换出你的消息呢。万一同样的编码在它那边的字符集和编码后对应的是“傻哔”两个字呢,那不是尴尬的一b(当然很大概率都是转成乱码)。

但是,如果让他那边也自动安装你的字符集和编码方式解决,就代表它也要安装全世界每种字符集和编码去兼容,显然是增添了太多无意义的负担。

上面的情况倒还是其次,更严重的是如果一篇文章涉及了两种语言呢?难不成一半用A编码一半用B编码?

所以统一字符集Unicode就出世了,又名万国码、统一码,它收纳了世界上的所有符号,只要计算机支持它一种字符集,就能表达任何语言。

 

2.Unicode的问题

Unicode诞生后,显然它同时定义了前面所说的第一层编码,但是却没有定义第二层编码,其实我在第五层

也就是Unicode没有定义该如何进行编码存储进计算机,不像ASCII码,直接就可以定位7位的长度然后计算机8位一个字节存储。Unicode的范围太大了,如果都以最长存储,两个字节65536都不太够用(据说已经差不多有10万个字符了),统一用三个字节又太过于浪费了。如果你是个只需要打英语的,本来一个字节就能覆盖所有英文字符了,结果Unicode硬是逼你三个字节存一个字符,那真是吃的太饱了。

如果说用一个字节表示英文(ASCII码),二到四个字节表示其他字符,那又会产生新的问题,计算机要怎样才能知道下一个字符是几个字节的呢?例如接下来的连续三个字节,是表示一个字符,还是三个单字节的字符呢?

加上由于当时互联网还没出现,Unicode也无法得到有效的推广,没有有效统一的解决方式,甚至出现了很多种不同的编码存储方式。

 

3.UTF-8

然后互联网时代到来了,UTF-8就杀出来了,它是互联网上使用最广泛的一种Unicode编码方式,和它相似的还有UTF-16和UTF-32。它们都是Unicode的不同的编码方式,可以说区别就是第二层编码,但是显然其他的编码方式没有UTF-8使用广。所以这时候同一种字符集Unicode,有了不同的编码方式,自然大家就开始将字符集和字符编码进行了区分

 

而UTF-8能从众多编码方式中脱颖而出,就在于它是可变长度的,也就是不会造成过多的资源浪费,并且它的编码规则也很简单:

  • ①对于单字节的符号,字节的第一位固定设为0,后面的7位是Unicode码,这点兼容了ASCII码。

  • ②对于大于一个字节的n字节符号,第一个字节的前n位固定设为1,第n+1位设为0,后面字节的前两位一律设为10,剩下所有其他的位组合起来就是Unicode码。

这两条规则的目的就是解决如何区分这几个字节到底是一个字符还是多个字符

如果不太理解的话,可以换个思路去理解:

站在计算机的角度,进行编码时,先读取第一个字节,查看它的首位,如果是0,说明这个字节一定是一个单字节的字符,就直接解析它后面的7位进行编码即可。如果是1,就继续读取这个字节的第二位第三位,直到碰到0为止,如果第二位就是0了,说明这个字节是前面字节的附属。如果第三第四位或以后才是0,那么这个字节开头有几个连续的1,就说明这个字符将会有几个字节

这里给个表格参照一下,左边是符号在Unicode中表示所需的位数,右边就是它的UTF-8编码方式了,可以看到目前最多可以用四个字节表示一个字符

Unicode符号位数       |  UTF-8编码方式
(二进制)              | (二进制)
----------------------+---------------------------------------------
1到7位               | 0xxxxxxx
8到11位              | 110xxxxx 10xxxxxx
12到16位             | 1110xxxx 10xxxxxx 10xxxxxx
17到21位             | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

举例说明

例如中文字符【码】,它的Unicode码是111 1000 0000 0001,共15位,对照上图就是第三行,需要三个字节去表示,那么它的UTF-8码就是11100111 10100000 10000001,理解了吗,将红字提出来组合在一起就是Unicode码,所以尽管是同一个字符,它的Unicode码和UTF-8码是不同的,注意这点哦。

有些人可能不知道怎么获取一个字符的Unicode码和UTF-8码的二进制,我这里简单写了个java测试类,供参考:

    public static void main(String[] args) throws UnsupportedEncodingException {
        char a = '码';
        //输出2进制的Unicode码
        System.out.println("二进制的Unicode码:"+Integer.toBinaryString(a));
        //输出16进制的Unicode码
        System.out.println("十六进制的Unicode码:"+Integer.toHexString(a));


        //获取到了字符UTF-8的byte数组,byte也是十进制数字的存储格式,所以要转换输出
        byte[] aBytes = String.valueOf(a).getBytes("UTF-8");
        String aBinary;
        System.out.println("二进制的UTF-8码:");
        for (byte aByte : aBytes) {
            //直接输出aByte的话,会得到三个-127~127之间的整数,所以要作转换处理
            //由于是补码,所以先处理为原码,再利用Integer的方法输出二进制
            aBinary = Integer.toBinaryString(aByte & 0xFF);
            System.out.print(aBinary+" ");
        }


        //附送一个16进制的UTF8码
        String a16= URLEncoder.encode(String.valueOf(a), "UTF-8");
        //由于是百分号分隔,有点丑,我们换个符号
        a16 = a16.replace("%", "-");
        System.out.println("\n十六进制的UTF-8码:"+a16);
    }


另外,其实网上大多数人讨论这些码时,大多是用16进制表示的,例如什么4EA2。但我在看的时候看的有点晕,本身编码这回事就有点乱,还要在二进制、十六进制、十进制之间转换来转换去,实在太绕了,所以我这里都是直接用二进制的。所以如果大家看到十六进制的也无需担心,转换为二进制后其实都一样。

关于UTF-8的最大字节数

其实在早期,一开始UTF-8设计的最大字节数是6个字节,也就是它最多可能会用6个字节表示一个字符。但是后面重新规范后,只能使用Unicode定义的区域 U+0000到U+10FFFF,所以通常我们说UTF-8最大是4个字节。

所以如果有人说UTF-8是6个字节,也并非空穴来风。只是现如今我们世界所有的符号加起来的数量,都没有达到能使用5个字节的UTF-8的程度,并且大概率以后也会一直是最大4个字节。

 

4.为什么还需要GBK?

按理说编码统一了,那就都用Unicode(UTF-8)不就行了,但是我们会发现日常使用中,还是有很多地方是GBK的,新建一个txt,默认的文本存储格式也是GBK的,这是为什么?

虽然UTF-8是可变长的,但并不代表它没有浪费资源(只是浪费的少了),到了四个字节,差不多11个位是固定写死的,都用来识别了,显然这就是一种浪费,也是可变长度需要付出的一定代价。在UTF-8中汉字通常是三个字节,这样我们一篇文章就至少得用三个字节去存储一个文字。

而GBK是专门用于中国的,基本上我们常用的任何字符,甚至附近几个亚洲国家的字符(日文、俄文、韩文)都囊括在内,最重要的是它表示一个汉字只需要两个字节存储,显然比UTF-8要节省很多空间,文本少的时候可能没有感觉到,但一旦文本多了,差异是十分明显的。

 

我们本地也可以简单的证明这点:

  • 在任意位置,鼠标右键=》新建=》文本文档

  • 随便打一万个汉字,点另存为,编码选择ANSI,在国内ANSI编码就是GBK编码的意思,原因后面会说明

    保存为ANSI编码

  • 继续另存为,这次编码选择UTF-8再保存

保存为UTF-8编码

下图可以看到,两个文件的大小,GBK只需要2万个字节存储,而UTF-8则需要3万个字节,整整多了一半的存储资源,而文本却是一毛一样的!

image-20210114224650667

不光是中国,其他国家恐怕也会这样,特别是亚洲的一些国家。由于主要语言不是英文字母,可能都会有自己的字符集和编码方式。如果平时的使用确定不会涉及国际化,一般也会选用自己国家的定义的编码方式,方便,也节省资源。

当然最主要的还是windows默认为不同的地区选择了默认的编码方式。不过如果当初windows铁了心无论什么地区都一律使用UTF-8,说不定现在整个计算机网络环境会很少出现乱码问题。

但是这些决断现在也不用打马后炮了,因为已成定局了。

 

 

四、中国的编码历史

前面我们简单的提到了一下GB2312和GBK,相信大家都看出来这两个都是中国的编码标准了,当然现在基本上都是用GBK了。这里我就简单的概况一下我们国家自己的编码标准的发展史,虽然取名差异很大,但是都是GB打头的,是“国标”两个字的简写,所以能从这判断出是中国的编码。

实际上这段发展史远比我这几段字复杂严谨的多,有兴趣的同学可以自己去查阅一下,我这只能通过网上的一些资料,简单且不严谨的总结那么一小下。

1.GB2312字符集

最开始便是GB2312,是一个简体中文字符集的中国国家标准,没有区分编码。前面127个字符仍然保留了ASCII码,后面就是汉字了,存储是一个汉字用两个字节。

它一共收录了6763个常用汉字,也收录了包括拉丁字母、希腊字母、日文平片假名、俄语西里尔字母在内的682个全角字符。两个字节长的就是全角字符,而原来在127号以下的那些就叫半角字符了。

附加思考:全角和半角标点符号

相信从使用输入法接触计算机以来,我们就知道全角字符和半角字符的存在了,可能还知道半角字符就是英文的符号,全角是中文的符号。并且我们在打代码时,标点符号都得用半角的。例如逗号,代码语法里用的都是半角逗号:【,】,如果不小心哪里用了全角的逗号:【,】,就会编译异常。

这说明半角和全角的标点符号是完全不一样的字符,其实全角的标点就是中文编码里新定义的字符,我们可以理解为它已经是一种特殊的汉字了,而半角的符号就是ASCII码里前128个符号。

既然是不同的符号,不同的字符,也就有不同的字符编号,当然都是不兼容的。所以中文逗号和英文逗号注定是没有关联的,最多只能抽象的说它们的意思是一样的。

 

2.GB13000

GB13000的出现主要是为了便于多种文字同时处理,它包含了20902个汉字,显然比GB2312扩充了很多,并定义了一套全新的编码体系。但是吧,好像没火起来,说实话我网上都搜不到太多它的资料,差点就以为GB2312后面直接就是GBK了。估计可以说它虽然制定好了,但是实现起来出现了困难。

 

3.GBK

网上有种说法是GBK最初是微软推出对GB2312的扩展,在windows95简中版使用。随着它的流行和广泛使用,国家就正式将其确定为官方的了,不过这种说法也倒是没有找到具体的证明。唯一能确定的是,GBK并不是国家标准,只是作为技术规范指导性文件。现在不管它的起源是如何,它现在也是国内windows系统最常用的一种编码方式了。

GBK扩充于GB2312,支持GB13000。从原来GB2312扩充至21003个汉字(包括繁体)和883个符号,远大于原来的6000多个,并且编码方式也改变了。虽然它收录了GB13000的所有汉字,但是它的编码方式和GB13000制定的不同,所以只能算是从GB2312到GB13000的一种过渡方案。(当然,到现在似乎也没能过渡成功,反而都用GBK了)

 

4.GB18030

GB18030又是对GBK的一次扩充,所以它兼容GBK和GB2312,这次它收录了国内少数民族的字符,说明少数民族村里也开始通网了,同时增补了一些韩文字符,和一些杂七杂八的。总共收录了27484个汉字,覆盖了东亚地区的大部分内容。并且它也整了个可变长的编码方式,有单字节、双字节、四字节三种。

目前它是我们的国家标准,意思就是强制性的。只是在windows系统中还没有推广开来,网上搜了一些资料,看来是任重而道远啊。。。但是这些东西就交给专业的人忙活吧。

目前windows中还是常用GBK的,但是别忘记,我国的编码国家标准还是GB18030,GBK只能说算是一个行业规范。并且和GB13000不同,GB18030在系统中是有这种具体的编码方式的。例如我最常用的IDEA,里面是可以切换文件为GB18030的编码的。

 

5.GB编码总结

可以看到,基本上中文编码,现在就是GB2312、GBK、GB18030这三种。其实除了我说的这些还有很多GB打头的字符集,只是我对此了解也不是很多,毕竟也就是个小小码农,不需要在这种地方太钻牛角尖,只是没想到GBK的发展有这么多故事。说不准以后哪天标配就是GB18030了,所以还是应该有所了解的。

 

 

*五、其他的几个编码集(选读)

感觉说都说了好几种了,其他的也粗略的说一两句吧,不然好像差了点什么。。但是这些编码既不是全球统一的,也不是国内通用的,所以简单看看就行了,跳过不看也没啥影响。

1.ISO-8859

ISO8859不是一个标准,是一系列的标准,所以我们会看到ISO-8859-1、ISO-8859-2、ISO-8859-3这些不同的字符集。它下面这些不同的字符集使用了不同国家地区的字符,也就是给不同国家使用的,相互之间相差甚远。

并且都是单字节的字符集,其实就是利用了ASCII没有定义的第8位,添加一些字符,128之后留了32个码位给扩充定义的32个控制码,所以它新增的字符的范围只有0xA1---0xFF(161---255),欧洲那边用的多,或者说自己国家字符少的国家可以用(中国肯定用不了)。

码位:

有时候我们会看到码位、编号、区位,其实都差不多,都是对字符集中某个字符的位置的形容,也就是一个座位号。并且通常是用十六进制表示,如0xA1之类的。

 

2.ISO-2022

显然ISO8859只能在给那些拉丁语系或字符少的国家使用,对于中日韩,肯定是没办法的。ISO2022就是这样出来的,它和8859一样代表的是一系列的标准,像ISO-2022-CN、ISO-2022-JP、ISO-2022-KR代表的就是中日韩的标准。

它也是可变长的编码方式,且支持GB2312,不过这里就不细说原理了,反正基本没啥人会用。

 

附加知识:ISO是什么组织

ISO是国际标准化组织International Organization for Standardization,它并不只为字符集服务,它是专门用来制定一些全球化标准的组织,涉及目前的绝大多数领域。所以Unicode字符集这种全球统一的字符集,也有它的一份力。

 

3.BIG5

大五码是弯弯(中国台湾省)那边用的编码,共收录13,053个字,基本上就都是繁体字,局限性很大。不说不常见的字了,就连一些常见的字符都没收录全。

不过上世纪90年代,香港也开始用了,毕竟它们都是用繁体的。然后香港发现有很多他们需要的字都没收录后,自然也一直在为缺少的字忙活​​,还出了个香港增补字符集,用来解决这个问题。

 

4.UTF-16和UTF-32

UTF-8前面说完了,这里再简单说说它两个没什么人气的兄弟:UTF-16和UTF-32。

①UTF-32

先说说UTF-32,和最开始的ASCII码一样,是固定长度的,32位即4字节。基本上没有4字节表达不了的符号了,但是同时也是个浪费空间的败家子,你想想你打个hello,用UTF-8也就5个字节,UTF-32要给你用掉20个字节,刺不刺激?

所以这也侧面说明了UTF-8这种可变长的编码的意义。

另外由于它是固定长度的,并且又不是单字节,处理单元是四个字节。所以会衍生出一种问题,就是大小端存储,不同的系统可能是不一样的,它怎么知道顺序是从大到小还是从小到大呢?

所以编码时就会分UTF-32 BE(Big Endian)和UTF-32 LE(Little Endian),直接从这就可以区分了。

什么是大端小端:

直接用例子说明比较简单,假设现在有一个字符,它是用4个字节表示的:0x12345678(这是16位表示,两个数字是一个字节),那么

大端法

低地址 -----------------> 高地址 0x12 | 0x34 | 0x56 | 0x78

小端法

低地址 ------------------> 高地址 0x78 | 0x56 | 0x34 | 0x12

就是一种顺序的不同

 

②UTF-16

UTF-16是变长的,只不过它只有两种变体:2字节和4字节。并且大部分都属于2字节,据说一开始就是固定2字节存储的,16位=2字节嘛,但是显然码位不够,就想办法延长♂到4字节了。

所以对于编号在 U+0000 到 U+FFFF 的字符(常用字符集),直接用两个字节表示。 而编号在 U+10000 到 U+10FFFF 之间的字符,则需要用四个字节表示。

另外它也会有大小端的问题,所以会看到UTF-16 BE 和UTF-16 LE两种

 

 

六、其他一些问题

列一些和字符有关的其他问题,可能会有人需要一个答案,后续可能会更新补充这一块。

 

1.什么是ANSI?

记事本保存时,默认就是ANSI的编码方式,当然现在我们知道了它是GBK,那么ANSI=GBK吗?

其实不是,ANSI编码的意思可以理解为“本地编码”,也就是在中国它代表的是GBK,在台湾省代表的是Big5,在日本就代表的是JIS,好一个墙头草。所以无需理解的太复杂,在中国的windows,你就当它是GBK就行了。

所以注意,ANSI和ASCII是没啥关系的,除了长的是有那么一点点像。

 

2.什么是UCS,它和Unicode有什么联系?

①什么是UCS:

有时我们会看到所谓的UCS字符集,实际上它的全名是Information technology -- Universal Coded Character Set (UCS) 【翻译:信息技术通用编码字符集(UCS)】,用编号来表示就是ISO 10646标准,其实它才是ISO组织出来为了统一全球字符编码的。注意它刚出来的时候和Unicode是不同的,Unicode是一个名为Unicode联盟的学术学会的机构创建的,也就是当时UCS和Unicode都是怀揣着同一目的诞生的:统一全球的编码。

②它和Unicode的联系

后来,显然世界上不需要两种不同的统一编码,所以他们进行了合作,从Unicode2.0开始,两家基本上就都差不多了,而UCS-2和UCS-4就是UCS的具体编码方式,和UTF-8对于Unicode一样。

并且,UCS-2可以看作是早期UTF-16的父集,都是固定用2个字节表示的,只是后面UTF-16引入了辅助平面字符,可以用4个字节表示字符了,就无法相提并论了。

UCS-4则和UTF-32基本没什么区别,至少,目前是差不多的。

 

 

 

参考资料:

1.百度百科

2.字符编码笔记:ASCII,Unicode 和 UTF-8:【推荐阅读,虽然是07年的文章,但是仍然值得一看】

www.ruanyifeng.com/blog/2007/1…

3.字符集与编码(一)——charset vs encoding:【这是个系列,一共有九篇,可以都阅读一下】

my.oschina.net/goldenshaw/…

4.编码方式的比较,以及UTF-8,gb2312的选择:

www.cnblogs.com/shanwater/p…

5.GB2312、GB 13000、GBK、GB18030 介绍和说明文档:

wenku.baidu.com/view/057033…

6.ANSI是什么编码?:

www.cnblogs.com/malecrab/p/…