字符编码字符集那些事

此篇文章仅为网上资料的汇总,方便自己查询使用,原文如下:

参考文章1:一文读懂字符编码
参考文章2:菜鸟教程字符集
参考文章3:百度字符集
参考文章4:一个线上BUG彻底搞懂MySQL字符集,工作也快搞丢了
参考文章5:深入理解MySQL字符集

1. 字符集、字符编码定义

计算机中储存的信息都是用二进制数表示的;而我们在屏幕上看到的英文、汉字等字符是二进制数转换之后的结果。通俗的说,按照何种规则将字符存储在计算机中,如’a’用什么表示,称为"编码";反之,将存储在计算机中的二进制数解析显示出来,称为"解码",如同密码学中的加密和解密。在解码过程中,如果使用了错误的解码规则,则导致’a’解析成’b’或者乱码。

  • 字符集(Charset):是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。

  • 字符编码释义1(Character Encoding):是一套法则,使用该法则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对。即在符号集合与数字系统之间建立对应关系,它是信息处理的一项基本技术。通常人们用符号集合(一般情况下就是文字)来表达信息。而以计算机为基础的信息处理系统则是利用元件(硬件)不同状态的组合来存储和处理信息的。元件不同状态的组合能代表数字系统的数字,因此字符编码就是将符号转换为计算机可以接受的数字系统的数,称为数字代码。

  • 字符编码释义2(Character encoding)也称字集码,是把字符集中的字符,编码为指定集合中的某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储或者通信网络的传递。常见的例子是将拉丁字母表编码成摩斯电码和ASCII,比如ASCII编码是将字母、数字和其它符号进行编号,并用7比特的二进制来表示这个整数。
    字符集(Character set)是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。计算机要准确的处理各种字符集文字,就需要进行字符编码,以便计算机能够识别和存储各种文字。

2. 字符编码详解

2.1 为什么计算机需要编码

我们知道计算机的世界只有0和1,如果没有字符编码,我们看到的就是一串"110010100101100111001…",我们的沟通就好像是在对牛弹琴,我看不懂它,它看不懂我。字符编码就好比人类和机器之间的翻译程序,把我们熟知的字符文字翻译成机器能读懂的二进制,同时把二进制翻译成我们能看懂的字符。

编码(Encode)是信息从一种形式转换为另一种形式的过程,比如用预先规定的方法将字符(文字、数字、符号等)、图像、声音或其它对象转换成规定的电脉冲信号或二进制数字。
我们现在看到的一幅幅图画,听到的一首首音乐,甚至我们写的一行行代码,敲下的一个个字符,所看到的所听到的都是那么的真实,但其实在背后都是一串「01」的数字,你昨天在手机上看到的那个心动女孩,真实世界中并不存在,只是计算机用「01」数字帮你生成的“骷髅”而已。
在这里插入图片描述

2.2 二进制其实不存在

你可能认为计算机中的数据就是「01」二进制,但是实际上计算机中并没有二进制,即便我们知道所有的内容都是存储在硬盘中,但是你把它拆开可找不到里面有任何「0101」的数字,里面也只有盘片、磁道。就算我们放大了去看盘片,也只有凹凸不平的盘面,凸起的地方是被磁化过的,凹进去的地方是没有被磁化的;只是我们给凸起的地方取了个名字叫数字「1」,凹进的地方取名叫数字「0」。

同样内存里你也找不到二进制数字,内存放大了看就是一堆电容组,内存单元存储的是「0」还是「1」取决于电容是否有电荷,有电荷我们认为他是「1」,无电荷认为他是「0」。但是电容是会放电的,时间一长,代表「1」的电容会放电,代表「0」的电容会吸电,这也是我们内存不能断电的原因,需要定期对电容进行充电,保证「1」的电容电量有电。

再说显示器,这个大家感受是最直接的,你透过显示器看到的美女画皮、日月山川,其实就是一个个不同颜色的发光二极管发出强弱不一的光点,显示器就是一群发光二极管组成的矩阵,其中每一个二极管可以被称为一个像素,「1」表示亮,「0」表示灭,而我们平时能看到五彩的颜色,是把三种颜色(红绿蓝三原色)的发光二极管做到了一起。那对于一个ASCII编码「65」最后又怎么显示成「A」的呢?这就是显卡的功劳,显卡中存储了每一个字符的图形数据(也称字形码),将二维矩阵的图形数据传给显示器成像。
在这里插入图片描述

因此,所谓的0和1都是电流脉冲信号,二进制其实是我们抽象出来的数学逻辑概念,那我们为什么要用二进制表示?

因为二进制只有两种状态,使用有两个稳定状态的物理器件就可以表示二进制中的每一位,例如用高低电平或电荷的正负性、灯的亮和灭都可以很方便地用「0」和「1」来表示,这为计算机实现逻辑运算和逻辑判断提供了便利条件。

2.3 计算机编码转换过程

正因为计算机只能表示「01」的逻辑概念,无法直接表示图片以及文字,所以我们需要一定的转换过程。

这其实就是我们按照一定的规则维护了字符-数字的映射关系,比如我们把「A」抽象成计算机中的「1」,当我们看到1的时候就认为这是「A」,本质上就是一张映射表,理论上你可以随意给每个字符分配一个独一无二的编号(character code,字符编码),比如下表
在这里插入图片描述

接下来我们来看下一个文字从输入-转码存储-输出(显示/打印)的简单流程,首先我们知道计算机是美国人发明的,规则是美国人定的,键盘上的按键也都是英文字母,所以编号不是你想怎么分配就怎么分配。对于英文字母的输入,键盘和ASCII码之间是直接对应的,键盘按键「A」对应的编号「65」,存储到磁盘上也是「65」的二进制直译「01000001」,这很好理解。

但是对于汉字输入就不是这么回事了,键盘上可没有汉字对应的输入按键,我们不可能直接敲出汉字字符。于是就有了输入码、机内码、字形码的转换关系,输入码帮助我们把英文键盘按键转换成汉字字符,机内码帮助我们把汉字字符转换成二进制序列,字形码帮助我们把二进制序列输出到显示器成像。

在这里插入图片描述

2.3.1 输入码

我们模拟下汉字的输入过程,首先打开txt文本敲下「nihao」的拼音字母,然后输入栏会弹出多个符合条件的汉字词组,最后我们会选择相应的编号,就能实现汉字的输入。那这过程又是如何实现的呢?

计算机领域有一句如同摩西十诫般的神圣哲言:"计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决"。

这里我们再加一层按键字母组合和汉字的映射表,好比英汉字典,这层我们称为输入码,输入码到内码的过程就是一次查表转换操作,比如「nihao」这几个ASCII字符,大家可以随便修改映射表以及候选编号,我可以把他映射成「心流时间」。

2.3.2 机内码

机内码也称内码,是字符编码最核心的部分,是字符集在计算机中实际存储、交换、通信使用的二进制编码,通过内码我们可以达到高效率的存储、传输文本的目的。我们的外码(输入码)实现了键盘按键和字符的映射转换,但是机内码是让字符真正变成了机器能读懂的二进制语言。

2.3.3 字形码

计算机中的字符都是以内码的二进制形式表示,我们怎么把数字对应的字符在显示器上显示出来呢,比如数字「1」代表汉字「你」,怎么把「1」显示成「你」?

这就需要依赖字形码,字形码本质上是一个n*n 的像素点阵,把某些位置的像素设置为白色(用 1 表示),其它位置像素设置为黑色(用 0 表示),每一个字符的字形都是预先存放在计算机内,而这样的字形信息库我们称为字库。

比如中文「你」的点阵图,这样一个 16*16 的像素矩阵,需要 16 * 16 / 8 = 32 字节的空间来表示,右边的字模信息称为字形码。不同的字库(如宋体、黑体)对同一个字符的字形编码是不同的。

在这里插入图片描述

所以字符编码到显示的字形码,其实又是另一张查找表,也就是字符编码-字形码的映射关系表。

其实我们也可以认为字符编码是字形码的一种压缩方式,一个占32字节的像素点阵压缩成了2字节的机内码。
在这里插入图片描述

3 字符编码的历史

3.1 电报编码

从广义上来说,编码的历史很悠久,一直可以追溯到结绳记事的远古时期,但跟现代字符编码比较接近的还是摩尔斯电码的发明,自此开启了信息通信时代的大门。

摩尔斯电码是由美国人摩尔斯在1837年发明的,比起ASCII还要早100多年,在早期的无线电上作用非常大,它是每个无线电通讯者需必知的,它的是由点dot「.」和划dash「-」这两种符号所组成的,电报中表达为短滴和长嗒,跟二进制一样也是二元码。一个二元肯定不够表示我们的字母,那么就用多个二元来表示,比如嘀嗒「.-」代表字母「A」,嗒嘀嘀嘀「-…」代表字母「B」。

摩尔斯电码表:
在这里插入图片描述

3.2 编码纪元

计算机一开始发明出来时是用来解决数学计算问题的,后来人们发现,计算机还可以做更多的事,例如文本处理等,那个时候的机器都很大,机器之间都是隔离的,没考虑过机器的通信问题,各大厂商也各干各个的,搞自己的硬件搞自己的软件,想怎么编码就怎么编码。
后来机器间需要相互通信的时候,发现在不同计算机上显示出来的字符不一样,在IBM上「00010100」数字代表「A」,跑到微软系统上显示成了「B」,大家就傻眼了。于是美国的标准化组织就跑出来制定了ASCII编码 (American Standard Code for Information Interchange),统一了游戏规则,规定了常用符号用哪些二进制数来表示。

3.3 百花齐放

在这里插入图片描述
统一ASCII 码标准对于英语国家很开心,但是ASCII编码只考虑了英文字母,后来计算机传到欧洲地区,法国人需要加个字母符号(如:é),德国人又需要加几个字母(Ä ä、Ö ö、Ü ü、ß),幸好ASCII只用了前127个编号,于是欧洲人就将ASCII没用完的编码(128-255)为自己特有的符号编码,也能很好的一起玩耍。

但是等传到我们中国后,做为博大精深的汉语言就彻底蒙圈了,我们有几万个汉字,255个编号完全不够用啊,所以有了后来的多字节编码… 因此,各个国家都推出了本国语言的编码表,也就有了后来的 ISO 8859 系列、GB系列(GB2312、GBK、GB18030、GB13000)、Big5、EUC-KR、JIS … ,不过为了能在计算机系统中通用,这些扩展的编码均直接或间接兼容 ASCII 码。

而微软/IBM这些国际化产商为了把自己的产品卖到全世界,就需要支持各个国家的语言,要在不同的地方采用当地的编码方式,于是他们就把全世界的编码方式都集中到一起并编上号,并且起了个名字叫代码页(Codepage,又称内码表),所以我们有时候也会看到xx代码页来指代某种字符编码,比如在微软系统里 中文GBK编码对应的是936代码页,繁体中文 Big5编码对应的是950代码页。

这些既兼容ASCII又互相之间不兼容的字符编码,后来又统称为ANSI编码。看到下面这张图估计大家就很熟悉了,window下面我们基本上都用ANSI编码保存。
在这里插入图片描述
ANSI的字面意思并非指字符编码,而是美国的一个非营利组织,是美国国家标准学会(American National Standards Institute)的缩写,ANSI这个组织为字符编码做了很多标准制定工作,后来大家习惯把这类混乱的多字节编码叫ANSI编码或者标准代码页。

ANSI编码只是一个范称,一般代表系统默认的编码方式,而且并不是确定的某一种编码方式——比如在Window操作系统里,中国区ANSI编码指的是GB编码,在香港地区ANSI编码指的是Big5编码,在韩国ANSI编码指的是EUC-KR编码。

3.4 天下一统

由于各个国家各搞各的字符编码,如果有些人想装逼中文里飚两句韩文怎么办呢?不好意思,你的逼级太高,没法支持,你选择了GB2312就只能打出中文字符。同时各大国际厂商在兼容各种字符编码问题上也深受折磨,于是忍无可忍之下,决定开发一套能容纳全世界所有字符的编码,就有了后面大名鼎鼎的Unicode。

Unicode也叫万国码,包括字符集、编码方案等,Unicode是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,在这种语言环境下,不会再有语言的编码冲突,在同屏下也可以显示任何国家语言的内容,这就是Unicode的最大好处。

在Unicode编码方案里常见的有四种编码实现方案UTF-7、 UTF-8、UTF-16、UTF-32,最为知名的就是 UTF-8,不过Unicode设计之初是采用双字节定长编码的UTF-16,但是发现历史包袱太重推不动,最后出了个变长的UTF-8才被广泛接受。

4. 字符编码模型

4.1 传统编码模型

在这里插入图片描述
在传统字符编码模型中,基本上都是将字符集里的字符用十进制进行逐一的编号,然后把十进制编号直接转成对应的二进制码,可以说该字符编号就是字符的编码。

计算机在处理字符与数字的转换关系上其实就是查找映射表的过程。像ASCII编码就是给每个英文字符编一个独一无二的数字,整个编码处理过程相对还是比较简单的,计算机内部直接就映射成了二进制,十进制的编号只是方便我们看的。
在这里插入图片描述

4.2 现代编码模型

在这里插入图片描述

Unicode编码模型采用了一个全新的编码思路,将编码模型划分为4 个层次,也有说5个层次的,不过第五层是传输层的编码适配,放在编码模型里严格来说不是很恰当。

  • 第一层,抽象字符集 ACR(Abstract Character Repertoire):定义抽象字符集合,明确各个抽象字符;

  • 第二层,编号字符集 CCS(Coded Character Set):将抽象字符集进行数字编号

  • 第三层,字符编码方式 CEF(Character Encoding Form):将字符编号编码为逻辑上的码元序列

  • 第四层,字符编码方案 CES(Character Encoding Scheme):将逻辑上的码元序列编码为物理字节序列

第一层:抽象字符集 ACR
所谓抽象字符集,就是抽象字符的合集,是一个无序集合,这里强调了字符是抽象的,也就是不仅包括我们视觉上能看到的狭义字符,比如「a」这样的有形字符,也包括一些我们看不到的无形字符,比如一些控制字符「DELETE」、「NULL」等。

抽象的另一层含义是有些字形是由多个字符组合成的,比如西班牙语的 「ñ」 由「n」和「~」两个字符组成,这一点上 Unicode 和传统编码标准不同,传统编码标准多是将 ñ 视作一个独立的字符,而 Unicode 中将其视为两个字符的组合。

同时一个字符也可能会有多种视觉上的字形表示,比如一个汉字有楷、行、草、隶等多种形体,这些都视为同一个抽象字符(即字符集编码是对字符而非字形编码),如何显示是字形库的事。
在这里插入图片描述
抽象字符集有开放与封闭之分。开放的字符集指还会不断新增字符的字符集,封闭字符集是指不会新增字符的字符集。比如ASCII就是封闭式的,只有128个字符,以后也不会再加,但是Unicode是开放式的,会不断往里加新字符的,已经从最初的 7163 个增加到现在的144,697 个字符。

第二层:编号字符集 CCS

编号字符集就是对抽象字符集里的每个字符进行编号,映射到一个非负整数的集合;

编号一般用方便人类阅读的十进制、十六进制来表示,比如「A」字符编号「65」,「B」字符编号是「66」;

大家需要清楚对于有些字符编码的编号就是存储的二进制序列,如ASCII编码;有些字符编码的编号跟存储的二进制序列并不一样,比如GB2312、Unicode等。

另外,编号字符集合是有范围限制的,比如ASCII字符集范围是0127,ISO-8859-1范围是0256,而GB2312是用一个94*94的二维矩阵空间来表示,Unicode是用Plane平面空间的概念来表示,这称为字符集的编号空间。

编号空间中的一个位置称为码点( Code Point 代码点 )。一个字符占用的码点所在的坐标(非负整数值对)或所代表的非负整数值,就是该字符的码值(码点编号)。

ASCII码点编号:
## 4.3

第三层:字符编码方式 CEF

抽象字符集和编号字符集是站在方便我们理解的角度来看的,所以最后我们需要翻译成计算机能懂的语言,将十进制的编号转换成二进制的形式。

因此字符编码方式就是将字符集的码点编号,转换成二进制码元序列( Code Unit Sequence )的过程。

码元:字符编码的最小处理单元,比如ASCII一个字符等于一个字节,属于单字节码元;UTF-16一个字符等于两个字节,处理过程是按字「word」来处理,所以是双字节码元;UTF-8是多字节编码,有单字节字符,也有多字节字符,每次处理是按单个单个字节解析处理,所以处理最小单位是字节,也属于单字节码元

这里大家可能会有疑问,十进制直接转二进制不就好了吗,为什么要单独抽出这么一层?

早期的字符编码确实也是这么处理的,十进制和二进制之间是直接转换过去的,比如ASCII码,字符「A」的十进制是「65」,那对应的二进制就是「1000001」,同时存储到硬盘里的也是这个二进制,所以那时候的编码比较简单。

随着后来多字节字符编码(Muilti-Bytes Character Set,MBCS多字节字符集)的出现,字符编号和二进制之间不是直接转换过去的,比如GB2312编码,「万」字的区位编号是「45,82」,对应的二进制机内码却是「1100 1101 1111 0010」(其十进制是「205,242」)。

如果这里不转换直接映射成二进制码会出什么问题呢?「万」字的字符编号「45,82」,45在ASCII里是「-」,82是「U」,那到底是显示两个字符「-U」还是显示一个字符「万」字,为了避免这种冲突 所以增加了前缀处理,详细的过程会在下文具体来讲解。

第四层:字符编码方案 CES

字符编码方案也称作“序列化格式“( Serialization Format ),指的是将字符编号进行编码之后的码元序列映射为字节序列(即字节流)的形式,以便经过编码后的字符能在计算机中进行处理、存储和传输。
字符编码方式CEF有点像我们数据库结构设计里的逻辑设计,而这一层编码方案CES就像是物理设计了,将码元序列映射为跟特定的计算机系统平台相关的物理意义上的二进制过程。
这里大家可能又会有疑问,为什么二进制的码元序列和实际存储的二进制又会不一样呢?这主要是计算机的大小端序造成的,具体端序内容会在UTF-16编码部分详细介绍。

大小端序名词出自Jonathan Swift的《格列夫游记》一书 :
所有人都认为,吃鸡蛋前,原始的方法是打破鸡蛋较大的一端。可是当今皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋时碰巧将一个手指弄破了,因此他的父亲,当时的皇帝,就下了一道敕令,命令全体臣民吃鸡蛋时打破鸡蛋较小的一端,违令者重罚。
老百姓们对这项命令极为反感。历史告诉我们,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位…关于这一争端,曾出版过几百本大部著作,不过大端派的书一直是受禁的,法律也规定该派的任何人不得做官。

5. 常用字符集和字符编码

常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等。计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。

5.1 ASCII字符集&编码

很久以前,计算机制造商都是按各自的方式来将字符渲染到屏幕上,当时的计算机动不动可就是一套房子的大小,这家伙可不是谁都能玩的起的,那时人们并不关心计算机如何交流。随着上世纪七八十年代微处理器的出现,计算机变得越来越小,个人计算机开始进入大众的视线,随后出现了井喷式的发展,但是之前厂商都是各自为政,没考虑过自家的产品要兼容别人家的东西,导致在不同计算机体系间的数据转换变得十分蛋疼,因此美国的标准协会在1967年制定出了ASCII编码,到目前为止共定义了128个字符。

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语,而其扩展版本EASCII则可以勉强显示其他西欧语言。它是现今最通用的单字节编码系统(但是有被Unicode追上的迹象),并等同于国际标准ISO/IEC 646。

ASCII字符集:主要包括控制字符(回车键、退格、换行键等);可显示字符(英文大小写字符、阿拉伯数字和西文符号)。

ASCII编码:将ASCII字符集转换为计算机可以接受的数字系统的数的规则。使用7位(bits)表示一个字符,共128字符;但是7位编码的字符集只能支持128个字符,为了表示更多的欧洲常用字符对ASCII进行了扩展,ASCII扩展字符集使用8位(bits)表示一个字符,共256字符。ASCII字符集映射到数字编码规则如下图所示(ASCII编码表):

在这里插入图片描述

在这里插入图片描述
其中前 32 个(031)是不可见的控制字符,32126 是可见字符,127 是 DELETE 命令(键盘上的 DEL 键)。
其实早在ASCII之前,IBM在1963年也推出过一套字符编码系统EBCDIC,跟ASCII码一样囊括了控制字符、数字、常用标点、大小写英文字母。
EBCDIC 编码:
在这里插入图片描述
但是他的字符编号并不是连续的,这给后续程序处理带来了麻烦,后来ASCII 编码吸取了 EBCDIC 的经验教训,给英文单词分配了连续的编码,方便程序处理,因此被后来广泛接受。

ASCII 和 EBCDIC 编码相比,除了字符连续排列之外,最大的优点是ASCII 只用了一个字节的低 7 位,最高位永远是 0。可别小看了这个最高位的 0,看似无足轻重,但这是ASCII设计最成功的地方,后面介绍各编码原理的时候你会发现,正是因为这个高位0,其它编码规范才能对 ASCII 码无缝兼容,使得 ASCII 被广泛接受。

ASCII的最大缺点是只能显示26个基本拉丁字母、阿拉伯数目字和英式标点符号,因此只能用于显示现代美国英语(而且在处理英语当中的外来词如naïve、café、élite等等时,所有重音符号都不得不去掉,即使这样做会违反拼写规则)。而EASCII虽然解决了部份西欧语言的显示问题,但对更多其他语言依然无能为力。因此现在的苹果电脑已经抛弃ASCII而转用Unicode。

5.2 ISO-8859系列

美国市场虽然统一了字符编码,但是计算机制造商在进入欧洲市场的时候又遇到了麻烦,欧洲的主流语言虽然也是用拉丁字母,但却存在很多扩展体,比如法语的「é」,挪威语中的「Å」,都无法用 ASCII 表示。但是大家发现ASCII后面的128个还没有被使用可以利用起来,这对于欧洲主流语言就足够了。

于是就有了大家所熟知的这个ISO-8859-1(Latin-1),它只是扩展了ASCII后128个字符,还是属于单字节编码;同时为了兼容原先的 ASCII码,当最高位是0的时候仍然表示原先的 ASCII 字符不变,当最高位是1的时候表示扩展的欧洲字符。

在这里插入图片描述

但是到这里还没有完,刚说了这只是欧洲主流的语言,但主流语言里没有法语使用的 œ、Œ、Ÿ 三个字母,也没有芬兰语使用的 Š、š、Ž、ž ,而单字节编码里的256个码点都被用完了,于是就出现了更多的变种 ISO-8859-2/3/…/16 系列,他们都兼容 ASCII,但彼此间又不完全兼容。
ISO-8859-n系列字符集如下:

ISO8859-1 字符集,也就是 Latin-1,是西欧常用字符,包括德法两国的字母。
ISO8859-2 字符集,也称为 Latin-2,收集了东欧字符。
ISO8859-3 字符集,也称为 Latin-3,收集了南欧字符。
ISO8859-4 字符集,也称为 Latin-4,收集了北欧字符。
ISO8859-5 字符集,也称为 Cyrillic,收集了斯拉夫语系字符。
ISO8859-6 字符集,也称为 Arabic,收集了阿拉伯语系字符。
ISO8859-7 字符集,也称为 Greek,收集了希腊字符。

5.3 GBXXXX字符集&编码

计算机发明之处及后面很长一段时间,只用应用于美国及西方一些发达国家,ASCII能够很好满足用户的需求。但是当天朝也有了计算机之后,为了显示中文,必须设计一套编码规则用于将汉字转换为计算机可以接受的数字系统的数。

当计算机进入东亚国家的时候,厂商们更傻眼了,美国和欧洲国家语言基本都是表音字符,一个字节就足够用了,但亚洲国家有不少是表意字符,字符个数动辄几万十几万的,一个字节完全不够用,所以我们国家有关部门按照ISO规范设计了GB2312双字节编码,但是GB2312是一个封闭字符集,只收录了常用字符总共也就7000多个字符,因此为了扩充更多的字符包括一些生僻字,才有了之后的GBK、GB18030、GB13000(“GB” 为 “国标” 的汉语拼音首字母缩写)。

按照 GB 系列编码方案,在一段文本中,如果一个字节是 0~127,那么这个字节的含义与 ASCII 编码相同,否则,这个字节和下一个字节共同组成汉字(或是 GB 编码定义的其他字符),所以GB系列都是兼容ASCII编码的。
在这里插入图片描述

5.3.1 GB2312

天朝专家把那些127号之后的奇异符号们(即EASCII)取消掉,规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。

上述编码规则就是GB2312。GB2312或GB2312-80是中国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,又称GB0,由中国国家标准总局发布,1981年5月1日实施。GB2312编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB2312。GB2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。对于人名、古汉语等方面出现的罕用字,GB2312不能处理,这导致了后来GBK及GB 18030汉字字符集的出现。下图是GB2312编码的开始部分(由于其非常庞大,只列举开始部分,具体可查看GB2312简体中文编码表):

在这里插入图片描述

GB2312是使用两个字节来表示汉字的编码标准,共收入汉字6763个和非汉字图形字符682个,为了避免与 ASCII 字符编码(0~127)相冲突,规定表示一个汉字的编码字节其值必须大于127(即字节的最高位为 1 ),并且必须是两个大于 127 的字节连在一起来共同表示一个汉字( GB2312 为双字节编码),所以GB2312 属于变长编码,当是英文字符的时候占一个字节,中文字符的时候占两个字节,可以认为 GB2312是对 ASCII 的中文扩展。
GB2312字符集编号空间是一个94*94的二维表,行表示区(高位字节),列表示位(低位字节),每区有94个位,每个区位对应一个字符,称为区位码。区位码上加2020H,就得到国标码,国标码上加8080H,就得到常用的计算机机内码。这里引入了区位码、国标码、机内码概念,下面我们说下三者的关系

国标码

国标码是我国汉字信息交换的标准编码,规定由4位16进制数组成,用两个低7位字节表示,为了避开 ASCII 字符中的前32个控制指令字符,所以每个字节都是从第33个编号开始,如下图所示:
在这里插入图片描述

区位码
由于上述国标码的16进制可编码区不够直观不方便我们使用,所以我们把他映射成了十进制的94*94二维表编号空间,我们称之为区位码,同时区位码也可以当成一种外码使用,输入法可以直接切换成区位码进行汉字输入,不过这种输入法无规则可言 人们很难记住区位编号,用的人也不多了。

下图是区位码的二维表,比如「万」字是45 区 82 位,所以「万」 字的区位码是「45,82」。
在这里插入图片描述

其中:

  • 01~09区(682个):特殊符号、数字、英文字符、制表符等,包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母等在内- 的682个全角字符;
  • 10~15区:空区,留待扩展;
  • 16~55区(3755个):常用汉字(也称一级汉字),按拼音排序;
  • 56~87区(3008个):非常用汉字(也称二级汉字),按部首/笔画排序;
  • 88~94区:空区,留待扩展。

机内码
GB2312国标码规范是覆盖掉ASCII中可见部分的符号和英文字母,使用两个7位码将其中的英文字母和符号重新编入,但是这样产生一个弊端,早期用ASCII码编码的英文文章无法打开,一打开就是乱码,也就是说应该要兼容早期ASCII码而不是覆盖它,后来微软为了解决这个问题,将字节的最高位设为1,因为ASCII中使用7位,最高位为0,转换后的编码称为机内码(内码),这种方式本质上是修改了GB2312的编码标准,最后被大家接受沿用。

总结下三者转换关系:区位码 —> 区码和位码分别 + 32(即 + 20H )得到国标码 —> 再分别 + 128(即 + 80H)得到机内码(与 ACSII 码不再冲突)
在这里插入图片描述

5.3.2 GBK

GBK即“国标扩展”的意思,因为GB2312双字节的最高位都要求大于1,上限也不会超过1万个字符,所以对此进行了扩展,对GB2312的字符不重新编码直接沿用,因此完全兼容GB2312。GBK虽然也是双字节编码,但是只要求第一个字节大于 127 就固定表示这是一个汉字的开始,正因为如此,GBK的编码空间比GB2312大很多。

GBK 整体编码范围为 8140-FEFE,首字节在 81-FE 之间,尾字节在 40-FE 之间,剔除 xx7F 一条线,总计 23940 个码位,共收入 21886 个汉字和图形符号;其中 GBK/1 收录除 GB 2312 字符外的其他增补字符,GBK/2 收录 GB2312 字符,GBK/3 收录 CJK 字符,GBK/4 收录 CJK 字符和增补字符,GBK/5 为非中文字符,UDC 为用户自定义字符。
详细如下如所示:
在这里插入图片描述
→ 这里大家可能会有两个疑问,为什么尾字节要从40开始,而不是00开始;为什么要排除 FF、xx7F这两条线的编号?

GBK的尾字节编码高位没有强制要求是1,当高位是0时跟ASCII码是冲突的,ASCII码里00-40之间大部分都是控制字符,所以排除控制字符主要是为了防止丢失高字节导致出现系统性严重后果;

排除FF是为了兼容GB2312,GB2312这个位是保留不使用的;而7F表示DEL字符就是向后删除一个字符,如果传输过程中丢失首字节那么就会出现严重的后果,所以需要将xx7F也排除,这是所有编码方案都需要注意的地方。

5.3.3 GB18030

随着计算机的发展,GBK的2万多个字符也还是扛不住,于是2000年我国又制定了新标准 GB18030,用来替代 GBK 标准。GB18030是强制性标准,现在在中国大陆销售的软件都支持 GB18030。

GB18030其实是对齐Unicode标准的,里面包括了所有Unicode字符集,也算是Unicode的一种实现(UTF)。

那既然有了UTF我们为什么还要搞一套Unicode实现?

主要是UTF-8/UCS-2他们是不兼容GB2312的,如果直接升级那么就全乱码了,所以GB18030是为了兼容GB系列,是GBK、GB2312的超集,当我们原先的GB2312(GBK)软件考虑升级到国际化Unicode时,可以直接使用GB18030进行升级。

GB18030虽然也是GB2312的扩展,但它和GBK的扩展方式不一样,GBK主要是充分利用了GB2312的一些没定义的编码空间,而GB18030采用的是字节变长编码,单字节区兼容ASCII、双字节区兼容GBK、四字节区对齐所有Unicode 码位。

实现原理上主要是采用第二字节未使用到的0x30~0x39编码空间来判断是否四字节。
在这里插入图片描述

  • 单字节,其值从0到0x7F。
  • 双字节,第一个字节的值从0x81到0xFE,第二个字节的值从0x40到0xFE(不包括0x7F)。
  • 四字节,第一个字节的值从0x81到0xFE,第二个字节的值从0x30到0x39,第三个字节的值从0x81到0xFE,第四个字节的值从0x30到0x39。

由于GB 2312-80只收录6763个汉字,有不少汉字,如部分在GB 2312-80推出以后才简化的汉字(如"啰"),部分人名用字(如中国前总理朱镕基的"镕"字),台湾及香港使用的繁体字,日语及朝鲜语汉字等,并未有收录在内。于是厂商微软利用GB 2312-80未使用的编码空间,收录GB 13000.1-93全部字符制定了GBK编码。根据微软资料,GBK是对GB2312-80的扩展,也就是CP936字码表 (Code Page 936)的扩展(之前CP936和GB 2312-80一模一样),最早实现于Windows 95简体中文版。虽然GBK收录GB 13000.1-93的全部字符,但编码方式并不相同。GBK自身并非国家标准,只是曾由国家技术监督局标准化司、电子工业部科技与质量监督司公布为"技术规范指导性文件"。原始GB13000一直未被业界采用,后续国家标准GB18030技术上兼容GBK而非GB13000。

GB 18030,全称:国家标准GB 18030-2005《信息技术 中文编码字符集》,是中华人民共和国现时最新的内码字集,是GB 18030-2000《信息技术 信息交换用汉字编码字符集 基本集的扩充》的修订版。与GB 2312-1980完全兼容,与GBK基本兼容,支持GB 13000及Unicode的全部统一汉字,共收录汉字70244个。GB 18030主要有以下特点:

  • 与UTF-8相同,采用多字节编码,每个字可以由1个、2个或4个字节组成。
  • 编码空间庞大,最多可定义161万个字符。
  • 支持中国国内少数民族的文字,不需要动用造字区。
  • 汉字收录范围包含繁体汉字以及日韩汉字。

GB18030编码总体结构图:
在这里插入图片描述
本规格的初版使中华人民共和国信息产业部电子工业标准化研究所起草,由国家质量技术监督局于2000年3月17日发布。现行版本为国家质量监督检验总局和中国国家标准化管理委员会于2005年11月8日发布,2006年5月1日实施。此规格为在中国境内所有软件产品支持的强制规格。

5.4 BIG5字符集&编码

Big5,又称为大五码或五大码,是使用繁体中文(正体中文)社区中最常用的电脑汉字字符集标准,共收录13,060个汉字。中文码分为内码及交换码两类,Big5属中文内码,知名的中文交换码有CCCII、CNS11643。Big5虽普及于台湾、香港与澳门等繁体中文通行区,但长期以来并非当地的国家标准,而只是业界标准。倚天中文系统、Windows等主要系统的字符集都是以Big5为基准,但厂商又各自增加不同的造字与造字区,派生成多种不同版本。2003年,Big5被收录到CNS11643中文标准交换码的附录当中,取得了较正式的地位。这个最新版本被称为Big5-2003。

Big5码是一套双字节字符集,使用了双八码存储方法,以两个字节来安放一个字。第一个字节称为"高位字节",第二个字节称为"低位字节"。"高位字节"使用了0x81-0xFE,"低位字节"使用了0x40-0x7E,及0xA1-0xFE。在Big5的分区中:
在这里插入图片描述

5.5 Unicode字符集&UTF编码

伟大的创想Unicode——不得不单独说Unicode

像天朝一样,当计算机传到世界各个国家时,为了适合当地语言和字符,设计和实现类似GB232/GBK/GB18030/BIG5的编码方案。这样各搞一套,在本地使用没有问题,一旦出现在网络中,由于不兼容,互相访问就出现了乱码现象。

为了解决这个问题,一个伟大的创想产生了——Unicode。Unicode编码系统为表达任意语言的任意字符而设计。它使用4字节的数字来表达每个字母、符号,或者表意文字(ideograph)。每个数字代表唯一的至少在某种语言中使用的符号。(并不是所有的数字都用上了,但是总数已经超过了65535,所以2个字节的数字是不够用的。)被几种语言共用的字符通常使用相同的数字来编码,除非存在一个在理的语源学(etymological)理由使不这样做。不考虑这种情况的话,每个字符对应一个数字,每个数字对应一个字符。即不存在二义性。不再需要记录"模式"了。U+0041总是代表’A’,即使这种语言没有’A’这个字符。

在计算机科学领域中,Unicode(统一码、万国码、单一码、标准万国码)是业界的一种标准,它可以使电脑得以体现世界上数十种文字的系统。Unicode 是基于通用字符集(Universal Character Set)的标准来发展,并且同时也以书本的形式[1]对外发表。Unicode 还不断在扩增, 每个新版本插入更多新的字符。直至目前为止的第六版,Unicode 就已经包含了超过十万个字符(在2005年,Unicode 的第十万个字符被采纳且认可成为标准之一)、一组可用以作为视觉参考的代码图表、一套编码方法与一组标准字符编码、一套包含了上标字、下标字等字符特性的枚举等。Unicode 组织(The Unicode Consortium)是由一个非营利性的机构所运作,并主导 Unicode 的后续发展,其目标在于:将既有的字符编码方案以Unicode 编码方案来加以取代,特别是既有的方案在多语环境下,皆仅有有限的空间以及不兼容的问题。

(可以这样理解:Unicode是字符集,UTF-32/ UTF-16/ UTF-8是三种字符编码方案。所以说一个字节占多少字节不是由字符集决定的,而是由字符编码决定的)

5.5.1 UCS & UNICODE

通用字符集(Universal Character Set,UCS)是由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的标准字符集。历史上存在两个独立的尝试创立单一字符集的组织,即国际标准化组织(ISO)和多语言软件制造商组成的统一码联盟。前者开发的 ISO/IEC 10646 项目,后者开发的统一码项目。因此最初制定了不同的标准。

1991年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码;ISO也承诺,ISO 10646将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致。两个项目仍都存在,并独立地公布各自的标准。但统一码联盟和ISO/IEC JTC1/SC2都同意保持两者标准的码表兼容,并紧密地共同调整任何未来的扩展。在发布的时候,Unicode一般都会采用有关字码最常见的字型,但ISO 10646一般都尽可能采用Century字型。

5.5.2 UTF-32

上述使用4字节的数字来表达每个字母、符号,或者表意文字(ideograph),每个数字代表唯一的至少在某种语言中使用的符号的编码方案,称为UTF-32。UTF-32又称UCS-4是一种将Unicode字符编码的协定,对每个字符都使用4字节。就空间而言,是非常没有效率的。

这种方法有其优点,最重要的一点就是可以在常数时间内定位字符串里的第N个字符,因为第N个字符从第4×Nth个字节开始。虽然每一个码位使用固定长定的字节看似方便,它并不如其它Unicode编码使用得广泛。

5.5.3 UTF-16

尽管有Unicode字符非常多,但是实际上大多数人不会用到超过前65535个以外的字符。因此,就有了另外一种Unicode编码方式,叫做UTF-16(因为16位 = 2字节)。UTF-16将0–65535范围内的字符编码成2个字节,如果真的需要表达那些很少使用的"星芒层(astral plane)"内超过这65535范围的Unicode字符,则需要使用一些诡异的技巧来实现。UTF-16编码最明显的优点是它在空间效率上比UTF-32高两倍,因为每个字符只需要2个字节来存储(除去65535范围以外的),而不是UTF-32中的4个字节。并且,如果我们假设某个字符串不包含任何星芒层中的字符,那么我们依然可以在常数时间内找到其中的第N个字符,直到它不成立为止这总是一个不错的推断。其编码方法是:

  • 如果字符编码U小于0x10000,也就是十进制的0到65535之内,则直接使用两字节表示;
  • 如果字符编码U大于0x10000,由于UNICODE编码范围最大为0x10FFFF,从0x10000到0x10FFFF之间 共有0xFFFFF个编码,也就是需要20个bit就可以标示这些编码。用U’表示从0-0xFFFFF之间的值,将其前 10 bit作为高位和16 bit的数值0xD800进行 逻辑or 操作,将后10 bit作为低位和0xDC00做 逻辑or 操作,这样组成的 4个byte就构成了U的编码。

对于UTF-32和UTF-16编码方式还有一些其他不明显的缺点。不同的计算机系统会以不同的顺序保存字节。这意味着字符U+4E2D在UTF-16编码方式下可能被保存为4E 2D或者2D 4E,这取决于该系统使用的是大尾端(big-endian)还是小尾端(little-endian)。只要文档没有离开你的计算机,它还是安全的——同一台电脑上的不同程序使用相同的字节顺序(byte order)。但是当我们需要在系统之间传输这个文档的时候,也许在万维网中,我们就需要一种方法来指示当前我们的字节是怎样存储的。不然的话,接收文档的计算机就无法知道这两个字节4E 2D表达的到底是U+4E2D还是U+2D4E。

为了解决这个问题,多字节的Unicode编码方式定义了一个"字节顺序标记(Byte Order Mark)",它是一个特殊的非打印字符,你可以把它包含在文档的开头来指示你所使用的字节顺序。对于UTF-16,字节顺序标记是U+FEFF。如果收到一个以字节FF FE开头的UTF-16编码的文档,你就能确定它的字节顺序是单向的(one way)的了;如果它以FE FF开头,则可以确定字节顺序反向了。

5.5.4 UTF-8

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码(定长码),也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部份修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。

UTF-8使用一至四个字节为每个字符编码:

  • 128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。
  • 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要二个字节编码(Unicode- 范围由U+0080至U+07FF)。
  • 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字)使用三个字节编码。
  • 其他极少使用的Unicode辅助平面的字符使用四字节编码。

在处理经常会用到的ASCII字符方面非常有效。在处理扩展的拉丁字符集方面也不比UTF-16差。对于中文字符来说,比UTF-32要好。同时,(在这一条上你得相信我,因为我不打算给你展示它的数学原理。)由位操作的天性使然,使用UTF-8不再存在字节顺序的问题了。一份以utf-8编码的文档在不同的计算机之间是一样的比特流。

总体来说,在Unicode字符串中不可能由码点数量决定显示它所需要的长度,或者显示字符串之后在文本缓冲区中光标应该放置的位置;组合字符、变宽字体、不可打印字符和从右至左的文字都是其归因。所以尽管在UTF-8字符串中字符数量与码点数量的关系比UTF-32更为复杂,在实际中很少会遇到有不同的情形。

优点

  • UTF-8是ASCII的一个超集。因为一个纯ASCII字符串也是一个合法的UTF-8字符串,所以现存的ASCII文本不需要转换。为传统的扩展ASCII字符集设计的软件通常可以不经修改或很少修改就能与UTF-8一起使用。
  • 使用标准的面向字节的排序例程对UTF-8排序将产生与基于Unicode代码点排序相同的结果。(尽管这只有有限的有用性,因为在任何特定语言或文化下都不太可能有仍可接受的文字排列顺序。)
  • UTF-8和UTF-16都是可扩展标记语言文档的标准编码。所有其它编码都必须通过显式或文本声明来指定。
  • 任何面向字节的字符串搜索算法都可以用于UTF-8的数据(只要输入仅由完整的UTF-8字符组成)。但是,对于包含字符记数的正则表达式或其它结构必须小心。
  • UTF-8字符串可以由一个简单的算法可靠地识别出来。就是,一个字符串在任何其它编码中表现为合法的UTF-8的可能性很低,并随字符串长度增长而减小。举例说,字符值C0,C1,F5至FF从来没有出现。为了更好的可靠性,可以使用正则表达式来统计非法过长和替代值(可以查看W3 FAQ: Multilingual Forms上的验证UTF-8字符串的正则表达式)。

缺点

因为每个字符使用不同数量的字节编码,所以寻找串中第N个字符是一个O(N)复杂度的操作 — 即,串越长,则需要更多的时间来定位特定的字符。同时,还需要位变换来把字符编码成字节,把字节解码成字符。

6. Unicode字符集&UTF编码详解

6.1 UNICODE

6.1.1 背景介绍

在统一码之前,各国创造了大量的节编码标准,有单字节的、双字节的(如 GB 2312、Shift JIS、Big5 、ISO8859等),各自又相互不兼容。在1987 年,苹果、Sun、微软等公司开始讨论囊括全世界所有字符的统一编码标准,组成了 Unicode 联盟,这个期间做了很多研讨工作,讨论核心要点如下:

  • 目前世界上有多少个字符,需要几个字节存储?
    工作组统计了当时全世界的报纸等刊物,结论是两个字节足以囊括全世界有实用意义的字符(当然这只统计了当前使用的字符,不包括古代语言或者废弃语言)。
  • 采用固定长度编码还是变长编码?
    一种采用变长编码形式,对于 ASCII 字符使用一个字节,其他字符使用两个字节,类似 GBK;
    另一种采用定长编码形式,不管是不是 ASCII 字符统一使用两个字节。

方案选择上主要从计算机处理过程中的时间和空间两个维度,也就是编解码的执行效率和存储大小两方面,最后结论是采用双字节定长编码,因为定长带来的空间变大在整体传输、存储成本上其实影响并不大,而定长编码处理效率会明显高于变长编码,所以早期 Unicode 采用了定长编码形式。

中、日、韩中有很多相近的表意文字是否可以统一?

由于汉字表意文字字符量较大,如果可以统一那么能大幅减少收录汉字的数量,

所以最初收录汉字遵循两个基本原则:表意文字认同原则和字源分离原则。

所谓表意文字认同原则,即“只对字,不对形”编码,将同一字的不同字形(即异体字)合并。例如“房”字的第一笔,在中日韩的写法都不同,但它本身是同一个字,只给一个编码,而写法的不同交由字体进行区分。

字源分离原则,是指一个字源中同时收录了同一个字的不同字形,则给予两个字形分别编码。例如:之前GBK中就收录了“戶”、“户”、“戸”三个字,那么Unicode也需要保留三个字,如果直接合并会造成使用上的困扰。

例如下面这句话如果不做字源分离,会是什么情况呢?
原句 :户有三种写法,分别是“戶”、“户”、“戸”,
改写后:户有三种写法,分别是“户”、“户”、“户”

6.1.2 Unicode介绍

Unicode 称为统一码(也叫万国码),是按现代编码模型进行设计的一套字符编码体系,涵盖抽象字符集、编号、逻辑编码、编码实现。Unicode是为了解决传统的字符编码方案的局限而产生的,在这种语言环境下,不会再有语言的编码冲突,可以在同屏下显示任何国家的语言。

UTF-n编码(Unicode Transformation Format Unicode字符集转换格式,n表示码元位数)是Unicode这套编码体系里的编码实现CES部分,像UTF-8、UTF-16、UTF-32都是将数字转换到实际的二进制编码实现,Unicode的编码实现除了UTF系列之外,还有UCS-2/4,GB18030等。但是现在很多人误把Unicode当成只是一个字符编号,这其实是不对的。

Unicode可以容纳世界上所有国家的文字和符号,其编号范围是0-0x10FFFF,有1,114,112个码位,为了方便管理划分成17个平面,现已定义的码位有238,605个,分布在平面0、平面1、平面2、平面14、平面15、平面16。其中平面0又称为基本多语言平面(Basic Multilingual Plane,简称BMP),这个平面基本涵盖了当今世界上正在使用中的常用字符。我们平常用到的字符,一般都是位于 BMP 平面上的,其范围拥有 65,536 个码点,其他平面统称增补平面,关于平面的概念会在UTF-16章节详细介绍。

6.1.3 与UCS的关系

在这里插入图片描述
说起Unicode我们不得不提UCS(全称Universal Multiple-Octet Coded Character Set 通用多八位编码字符集),国际标准编号ISO/IEC 10646,是由 ISO 和 IEC 两家国际标准组织联合成立的工作组设计的一套新的统一字符集项目,目的与Unicode 联盟一样致力于开发一款全世界通用的编码集。

早在1984 年ISO 和 IEC 两家组织就成立了一个联合工作组来设计一套新的统一字符集标准,但是这两个组织都不知道对方的存在,直到Unicode联盟1988年发布了Unicode草案(UCS草案1989年发布),才发现大家在做同一件事,没有必要搞两套标准 所以后面又考虑合并,

由于UCS 最初设计的是 31 位编码空间(UCS-4编码实现),可以容纳 2^31 约 21 亿个字符,而Unicode是16位空间(UTF-16编码实现),所以最开始Unicode 打算作为 UCS 的真子集,即 Unicode 中的每个字符都存在于 UCS 中,而且两者的码点相同,但 UCS 中的字符(编号超过65,536的)则不一定存在于 Unicode 中。

不过由于双方利益关系并没有说谁解散谁,最后双方作出一些妥协保持一致共同发展,两个标准中相同字符的编码(码点)必须是一样的,这是一个屁股决定脑袋的决策,如果最初Unicode知道UCS的存在,就不会再出现Unicode了。当然合并工作不是一蹴而就的而是经过多轮迭代, ISO/IEC 和 Unicode在 1993 年发布了第一版相互兼容版本,到了 1996年Unicode 2.0标准发布时,Unicode 字符集和 UCS 字符集(即 ISO/IEC 10646-1 )基本保持了一致,同时Unicode为了跟UCS的四字节保持一致推出了UTF-32编码实现,UCS为了跟Unicode的两字节保持一致推出了UCS-2编码实现。

所以现在我们可以认为UCS和Unicode是同一个东西,比如我们常见的java内部运行就采用的是UTF-16编码,而window操作系统采用的是UCS-2,他们都是同一个Unicode标准。

→ 为什么这里使用的是2字节编码,而不是4字节呢?先留个悬念,后续会详细讲解

6.2 UTF-16(Java内部编码)

UTF是Unicode Transfer Format的缩写,即把Unicode转做某种格式的意思,所以UTF-16是Unicode编码里的其中一种实现方式,16代表的是字节位数,占两个字节(UTF-32则表示4个字节)。

Unicode 设计之初是采用UTF-16这种双字节定长编码的,其字符编号就是对应的二进制编号,也就是说第二层的CCS和第三层的CEF是一致的。比如汉字「万」的 Unicode 码点是 「U+4E07」,其二进制序列就是直译的「0100 1110 0000 0111 」,这种编码方式的优点是高效,不需要检查标志位,但缺点是不兼容ASCII,ASCII编码的文本都会显示乱码。

不过后来Unicode联盟发现 16 位编码空间根本不够用,与此同时 ISO/IEC组织也觉得 UCS的 32 位编码空间太多了,实际中根本没有几十亿字符,也挺浪费空间的,所以最终 Unicode 联盟和 ISO/IEC 工作组达成一致:两者使用统一的编码空间「 0000 ~ 10FFFF」(即 UCS 保证永远不分配大于 10FFFF 的字符码点),而且双方在字符编码上保持同步,即一方标准中增加了字符,也要通知另一方同步。

于是Unicode在UTF-16基础上拓展编码空间到 21 位,UCS则搞了一个双字节的UCS-2编码实现。

→ UTF-16 编码是双字节的,上限也只有6w多个码点,怎么让他支持到10FFFF(100w+)个码点呢?

本质就是多加几个字节来表示更多的字符,只是UTF-16不像UCS那样采用定长4字节,而是使用变长的形式,但是这个跟UTF-8变长方式又不太一样,他是采用代理对的方式实现,大部分常用字符用一个码元表示(定长2个字节),其他扩展的特殊字符用两个码元表示(定长4字节)。

代理对

UTF-16跟UTF-8、GB系列等都算是变长字节,但是设计初衷却不一样,像GBK是为了兼容ASCII,但是UTF-16一开始就没考虑要兼容ASCII,所以他的变长是为了节约存储空间而采用的自然增长方案,当空间不够的时候增长到4个字节。

那问题来了,我怎么知道存储的4个字节是表示一个字符,还是两个字符呢?比如当程序遇到字节序列01001110 00101101 01010110 11111101时,到底是判断成一个字符还是两个字符?

这就需要一个前导识别,比如GB2312识别第一个字节高位是不是1来判断是单字节还是双字节,但是UTF-16的高位1已经被用来编码了,当然这也难不倒我们,第一位被用了那么就用前几位的组合形式。

UTF-16采用了代理对来解决,也就是高半区编码(前两个字节)范围D800-DBFF(称为代理码点),低半区编码(后两个字节)范围DC00-DFFF,组成一个四个字节表示的字符。在这里插入图片描述
上述前导6位组合也是有讲究的,ISO组织要求编号范围是0~10FFFF(),也就是说用20位就可以表示10FFFF个字符,对于双码元就是每个码元各自负责10位,一个码元是16位,数字位占去10位后,剩下的6位做为前导位。

当UTF-16使用一个码元表示的时候,Unicode字符编号跟码元序列是等值映射的,但是当采用双码元后,字符编号跟码元序列就需要转换了,下面是码元和Unicode编号值之间的计算公式:
在这里插入图片描述

平面空间

UTF-16把编码空间0000 ~ 10FFFF切成了17个平面,其实就是划分成17个区块,每个平面空间码点数都是=65536个,第一个平面称为基本多语言平面(Basic Multilingual Plane,简称BMP),这个平面涵盖了当今世界上最常用的字符,固定使用定长两个字节,除此之外的字符都放到增补平面里,都是使用两个码元的定长4个字节,下面是各个平面的用途:
在这里插入图片描述
增补平面的编号是采用双码元4个字节来表示的,去除代理对之后有效位数是20位,然后将这20位的编号再划成16个平面区域,其中高半区的数字位里取出4位表示平面,剩下的16位表示每个平面可以表示的字符数也就是2的16次方65536个(两个字节大小)

在这里插入图片描述
UTF-16可看成是UCS-2的父集。在没有辅助平面前,UTF-16与UCS-2所指的是同一的意思。但当引入辅助平面字符后,就称为UTF-16了。

字节序

字节序顾名思义是指字节的顺序,对于单字节编码来说,一个字符对应一个字节,也就不存在字节序问题;但是对于UTF-16这种定长多字节编码,就有字节顺序问题了。字节序其实跟操作系统和底层硬件有关,不仅只是UTF-16这种多字节编码存在字节序,只要是多字节类型的数据都存在字节顺序问题,比如short、int、long。

为了方便说明,我们这里举个例子,比如存一个整数值「305419896」对应16进制是0x12345678,有人习惯从左到右按顺序去存,也有人说高位当然要放到高位地址而低位放到低位地址,要从右往左存。于是就有了下面两种存取方式。
在这里插入图片描述
其实这两种方式没有孰优孰劣,只是我们认知习惯有所不同 最终的设计不同,说来这都是阿拉伯人的锅啊,为什么数字高位非要在左边,这也引起了著名的大小端之争。

因此字节序也就有了大端和小端的概念,也形成了各自的阵营,比如Windows、FreeBSD、Linux 是小端序,Mac是大端序。其实大小端序并没有技术上的好坏之分。

小端序( Little-Endian)就是低位字节(即小端字节、尾端字节)存放在内存的低地址,而高位字节(即大端字节、头端字节)存放在内存的高地址。

大端序(Big-Endian )就是高位字节(即大端字节、头端字节)存放在内存的低地址,低位字节(即小端字节、尾端字节)存放在内存的高地址。

6.3 UTF-8

6.3.1 简介&规则

Unicode还是UCS最初都是采用多字节定长编码,由于没有兼容现有的 ASCII 标准的文件和软件,新标准很难被推广,于是兼容ASCII版本的UTF-8就诞生了。

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,是现代字符编码模型中的第三层 CEF 。它可以用一至四个字节对 Unicode 字符集中的所有有效编码点进行编码,属于Unicode标准的一部分,UTF-8 就是为了解决向后兼容 ASCII 码而设计,Unicode 中前 128 个字符(与 ASCII 码一一对应),使用与 ASCII 码相同的二进制值的单个字节进行编码,这使得原来处理 ASCII 字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字优先采用的编码方式。

UTF-8需要兼容ASCII,所以也需要有前缀码来控制,前缀规则如下:

  • 如果首字节以 0 开头,则是单字节编码(即单个单字节码元);
  • 如果首字节以 110 开头,则是双字节编码(即由两个单字节码元所组成的双码元序列);
  • 如果首字节以 1110 开头,则是三字节编码(即由三个单字节码元所组成的三码元序列),以此类推。
    在这里插入图片描述
    理论上UTF-8变长可以超过4个字节,只是Unicode联盟规范上限是10FFFF,所以UTF-8规则设计上也限制了大小。

6.3.2 程序算法

用文字不太好描述算法结构,我们就直接来欣赏一下UTF-8鼻祖写的这段解析代码,这是Ken 和 Rob 用一个晚上写出来的编解码算法,代码非常简短精炼,为了方便阅读我加了注释解读。

typedef
struct
{
  int   cmask; //前缀码掩码
  int   cval;  //前缀码
  int   shift; //移动位数
  long  lmask; //Unicode值掩码
  long  lval;  //Unicode下限值
} Tab;

static
Tab  tab[] =
{
  0x80, 0x00, 0*6, 0x7F,       0,         /* 1 byte sequence */
  0xE0, 0xC0, 1*6, 0x7FF,      0x80,      /* 2 byte sequence */
  0xF0, 0xE0, 2*6, 0xFFFF,     0x800,     /* 3 byte sequence */
  0xF8, 0xF0, 3*6, 0x1FFFFF,   0x10000,   /* 4 byte sequence */
  0xFC, 0xF8, 4*6, 0x3FFFFFF,  0x200000,  /* 5 byte sequence */
  0xFE, 0xFC, 5*6, 0x7FFFFFFF, 0x4000000, /* 6 byte sequence */
  0, /* end of table */
};
/**
* 把一个多字节序列转换为一个宽字符
* 
* @param p 存放计算后的unicode值
* @param s 需要解析的UTF-8字节序列
* @param n 字节长度
* @return 解析的字节长度
*/
int mbtowc(wchar_t *p, char *s, size_t n)
{
  long l;  int c0, c, nc;  Tab *t;
  if(s == 0) return 0;
  nc = 0;
  //异常校验(可不用关注)
  if(n <= nc) return -1;
  //c0 此处备份一下首字节,后续需要用到前缀码
  c0 = *s & 0xff;
  //l 保存 Unicode 结果
  l = c0;
  /* 遍历tab,从单字节结构->2字节结构->..依次检查找到对应tab */
  for(t=tab; t->cmask; t++) {
    //字节数+1,字节数和tab结构是对应的,也就是当nc=1时 tab结构是单字节,nc=2是tab是两字节
    nc++;
    /* 判断前缀码跟当前的tab是否一致, 如果一致计算最终unicode值并返回*/
    if((c0 & t->cmask) == t->cval) {
      //通过 & Unicode有效值掩码,移除高位前缀码,得到最终unicode值
      l &= t->lmask;
      //异常校验
      if(l < t->lval) return -1;
      //保存结果并反回
      *p = l;
      return nc;
    }
    //异常校验
    if(n <= nc) return -1;
    //读取下个字节;如果上面判断前缀码不一致,说明需要再读取下个字节
    s++;
    //计算有效位的值,目的是去除UTF-8 编码从第二个字节开始的高两位10
    // 例如 s=10101111、0x80=10000000 计算结果是00101111,这样就去除了高位前缀10
    c = (*s ^ 0x80) & 0xFF;
    //异常校验
    if(c & 0xC0) return -1;
    //重新计算unicode值,根据UTF-8规则c只有低 6 位有效,所以通过移位把c填入到l的低6位
    l = (l<<6) | c;
  }
  //返回异常
  return -1;
}

6.3.3 容错性

通过上面的程序我们知道解析过程是一个字节一个字节往下处理的,我们在传输过程中如果发生局部的字节错误、丢失,或者中间有一个字节规则对不上,会不会影响整个文本的解析?

我们先来看下其他编码的容错情况,从对于单字节的ASCII码来说,丢失一个字节就丢失一个字符,并不影响后续文本的内容,比如Hello world,丢失b2字节后内容是Hllo world少个e而已

我们再来看GB2312这种多字节编码,如果丢失了b2字节那么整个文本都乱套了,这是最糟糕的,大部分多字节编码都有类似问题,一旦出现错误可能导致整个文件都需要重传。
在这里插入图片描述
接下来我们看看UTF-8是如何避免这种“一颗老鼠屎坏了一锅粥”的情况,UTF-8 的码元序列的第一个字节指明了后面所跟字节的个数,比如首字节高位是0就表示单字节,110表示总共两个字节,1110表示三个字节依次类推,除首字节之外后续字节都是10开头。所以UTF-8的前缀码具有很强的鲁棒性,即使丢失、增加、改变个别字节也不会导致后续字符全部错乱这样的传递性、连锁性的错误问题。
在这里插入图片描述

7. Accept-Charset/Accept-Encoding/Accept-Language/Content-Type/Content-Encoding/Content-Language

在HTTP中,与字符集和字符编码相关的消息头是Accept-Charset/Content-Type,另外主区区分Accept-Charset/Accept-Encoding/Accept-Language/Content-Type/Content-Encoding/Content-Language:

  • Accept-Charset:浏览器申明自己接收的字符集,这就是本文前面介绍的各种字符集和字符编码,如gb2312,utf-8(通常我们说Charset包括了相应的字符编码方案);

  • Accept-Encoding:浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate),(注意:这不是只字符编码);

  • Accept-Language:浏览器申明自己接收的语言。语言跟字符集的区别:中文是语言,中文有多种字符集,比如big5,gb2312,gbk等等;

  • Content-Type:WEB服务器告诉浏览器自己响应的对象的类型和字符集。例如:Content-Type: text/html; charset=‘gb2312’

  • Content-Encoding:WEB服务器表明自己使用了什么压缩方法(gzip,deflate)压缩响应中的对象。例如:Content-Encoding:gzip

  • Content-Language:WEB服务器告诉浏览器自己响应的对象的语言。

8. mysql字符集

一个线上BUG彻底搞懂MySQL字符集,工作也快搞丢了
深入理解MySQL字符集

  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值