史诗级讲解计算机字符编码的前世今生 —— ASCII、Unicode、UTF-8、GBK、GB2312

本文介绍了计算机字符编码的相关知识。首先阐述了计算机需要编码的原因及编码转换过程,接着回顾了字符编码的历史,包括电报编码、ASCII 编码等。还介绍了常见字符编码,如 ASCII、GB 系列等,重点讲解了 Unicode 编码及其实现方式 UTF - 8 和 UTF - 16。最后总结了字符集和编码格式的关系。

目录

一. 前言

二. 为什么计算机需要编码

2.1. 编码概述

2.2. 二进制其实不存在

三. 计算机编码转换过程

3.1. 概述

3.2. 输入码

3.3. 机内码

3.4. 字形码

四. 字符编码的历史

4.1. 电报编码

4.2. 编码纪元

4.3. 百花齐放

4.4. 天下一统(Unicode)

五. 字符编码模型

5.1. 传统编码模型

5.2. 现代编码模型

5.2.1. 第一层:抽象字符集 ACR

5.2.2. 第二层:编号字符集 CCS

5.2.3. 第三层:字符编码方式 CEF

5.2.4. 第四层:字符编码方案 CES

六. 常见字符编码

6.1. ASCII 编码

6.1.1. EBCDIC 编码

6.2. ISO-8859 编码系列

6.3. GB 国标编码系列

6.3.1. 概述

6.3.2. GB2312 编码

6.2.3.1. 国标码

6.2.3.2. 区位码

6.2.3.3. 机内码

6.3.3. GBK 编码

6.3.4. GB18030 编码

6.3.5. Big5 编码

七. Unicode 编码(万国码)

7.1. 背景介绍

7.2. Unicode 介绍

7.3. Unicode 与 UCS 的关系

7.4. UTF-16(Java 内部编码)

7.4.1. 代理对

7.4.2. 平面空间

7.4.3. 字节序

7.5. UTF-8

7.5.1. 概述

7.5.2. 程序算法

7.5.3. 容错性

八. 总结


一. 前言

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

    字符编码(Character Encoding)也称字集码,是把字符集中的字符,编码为指定集合中的某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储或者通信网络的传递。常见的例子是将拉丁字母表编码成摩斯电码和 ASCII,比如 ASCII 编码是将字母、数字和其它符号进行编号,并用7比特的二进制来表示这个整数。

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

    字符集(Character Set)是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。字符集种类较多,每个字符集包含的字符个数不同,常见字符集名称有:ASCII 字符集GB2312 字符集BIG5 字符集GB18030 字符集Unicode 字符集等。计算机要准确的处理各种字符集文字,就需要进行字符编码,以便计算机能够识别和存储各种文字。

二. 为什么计算机需要编码

2.1. 编码概述

    编码(Encode)是信息从一种形式转换为另一种形式的过程,比如用预先规定的方法将字符(文字、数字、符号等)、图像、声音或其它对象转换成规定的电脉冲信号或二进制数字。

    我们现在看到的一幅幅图画,听到的一首首音乐,甚至我们写的一行行代码,敲下的一个个字符,所看到的所听到的都是那么的真实,但其实在背后都是一串 0和1 的数字。你昨天在手机上看到的那个心动女孩,真实世界中并不存在,只是计算机用 0和1 数字帮你生成的“骷髅”☠而已(蛋碎了 ╥╯^╰╥)。

2.2. 二进制其实不存在

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

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

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

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

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

三. 计算机编码转换过程

3.1. 概述

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

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

比如下表这样:

字符编号
1(00000001)
2(00000010)
......

接下来我们看下一个文字从 输入-转码存储-输出(显示/打印)的简单流程:

    首先,我们知道计算机是美国人发明的,规则是美国人定的,键盘上的按键也都是英文字母,所以编号不是你想怎么分配就怎么分配。对于英文字母的输入,键盘和 ASCII 码之间是直接对应的,键盘按键 A 对应的编号是 65 ,存储到磁盘上也是 65 的二进制为 01000001 ,这很好理解。

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

3.2. 输入码

    我们模拟下汉字的输入过程:打开 txt 文本敲下 nihao 的拼音字母,然后输入栏会弹出多个符合条件的汉字词组,最后我们会选择相应的编号,就能实现汉字的输入。

    那这个过程又是如何实现的呢?计算机领域有一句如同摩西十诫般的神圣哲言:“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。

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

3.3. 机内码

    机内码也称内码,是字符编码最核心的部分。

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

3.4. 字形码

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

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

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

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

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

四. 字符编码的历史

4.1. 电报编码

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

摩尔斯电码是由美国人摩尔斯在1837年发明的,比起ASCII还要早100多年,在早期的无线电上作用非常大,
它是每个无线电通讯者需必知的,它的是由点dot “.” 和划dash “-” 这两种符号所组成的,
电报中表达为短滴和长嗒,跟二进制一样也是二元码。

    一个二元码肯定不够表示我们的字母,那么就用多个二元来表示,比如嘀嗒 .和- 代表字母 A ,嗒嘀嘀嘀 -... 代表字母 B。

摩尔斯电码表:

4.2. 编码纪元

    计算机一开始发明出来时是用来解决数学计算问题的,后来人们发现,计算机还可以做更多的事,例如文本处理等。那个时候的机器都很大,机器之间都是隔离的,没考虑过机器间的通信问题,各大厂商也各干各个的,搞自己的硬件搞自己的软件,想怎么编码就怎么编码。

    后来机器间需要相互通信的时候,发现在不同计算机上显示出来的字符不一样,在 IBM 上00010100 数字代表 A,跑到微软系统上显示成了 B,大家就傻眼了。于是美国的标准化组织就跑出来制定了 ASCII 编码(American Standard Code for Information Interchange),统一了游戏规则,规定了常用符号用哪些二进制数来表示。

4.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 编码。看到下面这张图估计大家就很熟悉了,Windows 下面我们基本上都用 ANSI 编码保存。

    ANSI 的字面意思并非指字符编码,而是美国的一个非营利组织,是美国国家标准学会(American National Standards Institute)的缩写,ANSI 这个组织为字符编码做了很多标准制定工作,后来大家习惯把这类混乱的多字节编码叫 ANSI 编码或者标准代码页。

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

4.4. 天下一统(Unicode)

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

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

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

五. 字符编码模型

5.1. 传统编码模型

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

    计算机在处理字符与数字的转换关系上其实就是查找映射表的过程。

    像 ASCII 编码就是给每个英文字符编一个独一无二的数字,整个编码处理过程相对还是比较简单的,计算机内部直接就映射成了二进制,十进制的编号只是方便我们看的。

字符十进制二进制
A6501000001
B6601000010
C6701000011
.........

5.2. 现代编码模型

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

这 4 个层次分别是:

  1. 第一层,抽象字符集 ACR(Abstract Character Repertoire):定义抽象字符集合,明确各个抽象字符;
  2. 第二层,编号字符集 CCS(Coded Character Set):将抽象字符集进行数字编号;
  3. 第三层,字符编码方式 CEF(Character Encoding Form):将字符编号编码为逻辑上的码元序列;
  4. 第四层,字符编码方案 CES(Character Encoding Scheme):将逻辑上的码元序列编码为物理字节序列。

下面将分别来详细讲讲各层。

5.2.1. 第一层:抽象字符集 ACR

    所谓抽象字符集,就是抽象字符的合集。

    它是一个无序集合,这里强调了字符是抽象的,也就是不仅包括我们视觉上能看到的狭义字符,比如 a 这样的有形字符,也包括一些我们看不到的无形字符,比如一些控制字符 Ctrl、Shift、Delete、NULL 等。

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

    同时一个字符也可能会有多种视觉上的字形表示,比如一个汉字有楷、行、草、隶等多种形体,这些都视为同一个抽象字符(即字符集编码是对字符而非字形编码),如何显示是字形库的事。

汉字“人”的不同形态:

    抽象字符集有开放与封闭之分:开放的字符集指还会不断新增字符的字符集,封闭字符集是指不会新增字符的字符集。比如 ASCII 就是封闭式的,只有 128 个字符,以后也不会再加。但是Unicode 是开放式的,会不断往里加新字符的,已经从最初的 7163 个增加到现在的144,697 个字符。

5.2.2. 第二层:编号字符集 CCS

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

    编号一般用方便人类阅读的十进制、十六进制来表示,比如 A 字符编号 65,B 字符编号是 66。大家需要清楚对于有些字符编码的编号就是存储的二进制序列,如 ASCII 编码;有些字符编码的编号跟存储的二进制序列并不一样,比如 GB2312、Unicode 等。

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

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

ASCII 码点编号:

5.2.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”还是显示一个字符“万”字,为了避免这种冲突 所以增加了前缀处理,详细的过程会在下面具体来讲解。

5.2.4. 第四层:字符编码方案 CES

    字符编码方案也称作序列化格式(Serialization Format),指的是将字符编号进行编码之后的码元序列映射为字节序列(即字节流)的形式,以便经过编码后的字符能在计算机中进行处理、存储和传输。

    字符编码方式 CEF 有点像我们数据库结构设计里的逻辑设计,而这一层编码方案 CES 就像是物理设计了,将码元序列映射为跟特定的计算机系统平台相关的物理意义上的二进制过程。

这里大家可能又会有疑问:为什么二进制的码元序列和实际存储的二进制又会不一样呢?

    这主要是计算机的大小端序造成的,具体端序内容会在 UTF-16 编码部分详细介绍。大小端序名词出自 Jonathan Swift 的《格列夫游记》一书 :

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

六. 常见字符编码

6.1. ASCII 编码

    我们知道,计算机内部所有信息最终都是一个二进制值。每一个二进制位(bit)有 0和1 两种状态,因此八个二进制位就可以组合出 256 种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从00000000到11111111。

    上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。

    ASCII 码一共规定了128个字符的编码,比如空格 SPACE 是32(二进制为 00100000),大写的字母 A 是65(二进制为 01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0。

    前 32 个(0~31)是不可见的控制字符,32~126 是可见字符,127 是 DELETE 命令(键盘上的 DEL 键)。

    但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用 256 个符号的编码方式,代表的字母却不一样。比如,130 在法语编码中代表了 é,在希伯来语编码中却代表了字母 Gimel(ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0-127 表示的符号是一样的,不一样的只是 128-255 的这一段。

    至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示 256 种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。

6.1.1. EBCDIC 编码

    早在 ASCII 之前,IBM 在1963年也推出过一套字符编码系统 EBCDIC,跟 ASCII 码一样囊括了控制字符、数字、常用标点、大小写英文字母。

    但是,他的字符编号并不是连续的,这给后续程序处理带来了麻烦,后来 ASCII 编码吸取了 EBCDIC 的经验教训,给英文单词分配了连续的编码,方便程序处理,因此被后来广泛接受。

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

6.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系列字符集如下:

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

6.3. GB 国标编码系列

6.3.1. 概述

    当计算机进入东亚国家的时候,厂商们更傻眼了,美国和欧洲国家语言基本都是表音字符,一个字节就足够用了,但亚洲国家有不少是表意字符,字符个数动辄几万十几万的,一个字节完全不够用。

    所以我们国家有关部门按照 ISO 规范设计了 GB2312 双字节编码。但是 GB2312 是一个封闭字符集,只收录了常用字符总共也就7000多个字符,因此为了扩充更多的字符包括一些生僻字,才有了之后的 GBK、GB18030、GB13000(“GB” 为 “国标” 的汉语拼音首字母缩写)。

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

6.3.2. GB2312 编码

    GB2312 是使用两个字节来表示汉字的编码标准,共收入汉字 6763 个和非汉字图形字符 682个。

    为了避免与 ASCII 字符编码(0~127)相冲突,规定表示一个汉字的编码字节其值必须大于 127(即字节的最高位为 1),并且必须是两个大于 127 的字节连在一起来共同表示一个汉字( GB2312 为双字节编码),所以GB2312 属于变长编码,当是英文字符的时候占一个字节,中文字符的时候占两个字节,可以认为 GB2312 是对 ASCII 的中文扩展。

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

GB2312编码表的开始部分

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

    GB2312 字符集编号空间是一个 94*94 的二维表,行表示区(高位字节),列表示位(低位字节),每区有94个位,每个区位对应一个字符,称为区位码。区位码上加 2020H,就得到国标码,国标码上加 8080H,就得到常用的计算机机内码。

    这里引入了区位码、国标码、机内码概念,下面我们说下三者的关系。

6.2.3.1. 国标码

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

6.2.3.2. 区位码

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

    不过这种输入法无规则可言,人们很难记住区位编号,用的人也不多了。下图是区位码的二维表,比如“万”字是45 区 82 位,所以“万” 字的区位码是“45,82”。

其中:

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

    GB2312 国标码规范是覆盖掉 ASCII 中可见部分的符号和英文字母,使用两个7位码将其中的英文字母和符号重新编入。但是这样会产生一个弊端:早期用 ASCII 码编码的英文文章无法打开,一打开就是乱码,也就是说应该要兼容早期 ASCII 码而不是覆盖它。

    后来微软为了解决这个问题:将字节的最高位设为 1,因为 ASCII 中使用7位,最高位为 0,转换后的编码称为机内码(内码),这种方式本质上是修改了 GB2312 的编码标准,最后被大家接受沿用。

    总结下三者转换关系:区位码 ---> 区码和位码分别 + 32(即 + 20H )得到国标码 ---> 再分别 + 128(即 + 80H)得到机内码(与 ACSII 码不再冲突)。

6.3.3. GBK 编码

    GBK 即国标扩展的意思,因为 GB2312 双字节的最高位都要求大于 1,上限也不会超过1万个字符,所以对此进行了扩展,对 GB2312 的字符不重新编码直接沿用,因此完全兼容 GB2312。

    GBK 虽然也是双字节编码,但是只要求第一个字节大于 127 就固定表示这是一个汉字的开始,正因为如此,GBK 的编码空间比 GB2312 大很多。

    GBK 整体编码范围为 8140-FEFE,首字节在 81-FE 之间,尾字节在 40-FE 之间,剔除 xx7F 一条线,总计 23940 个码位,共收入 21886 个汉字和图形符号。其中 GBK/1 收录除 GB2312 字符外的其他增补字符,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 也排除,这是所有编码方案都需要注意的地方。

6.3.4. GB18030 编码

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

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

GB18030 主要有以下特点:

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

    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 编码空间来判断是否四字节。

具体就是:

  1. 单字节,其值从 0 到 0x7F。
  2. 双字节,第一个字节的值从 0x81 到 0xFE,第二个字节的值从 0x40 到 0xFE(不包括0x7F)。
  3. 四字节,第一个字节的值从 0x81 到 0xFE,第二个字节的值从 0x30 到 0x39,第三个字节的值从 0x81 到 0xFE,第四个字节的值从 0x30 到 0x39。
6.3.5. Big5 编码

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

    Big5 码是一套双字节字符集,使用了双八码存储方法,以两个字节来安放一个字。第一个字节称为高位字节,第二个字节称为低位字节。高位字节使用了 0x81-0xFE,低位字节使用了 0x40-0x7E,及 0xA1-0xFE。

七. Unicode 编码(万国码)

    Unicode 编码实在是太重要了且涵盖内容也较多,所以从常用字符编码章节里独立出来,单独成为一节。

7.1. 背景介绍

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

1. 目前世界上有多少个字符,需要几个字节存储?

    工作组统计了当时全世界的报纸等刊物,结论是两个字节足以囊括全世界有实用意义的字符(当然这只统计了当时使用的字符,不包括古代语言或者废弃语言)。

2. 采用固定长度编码还是变长编码?

    一种采用变长编码形式,对于 ASCII 字符使用一个字节,其他字符使用两个字节,类似 GBK。另一种采用定长编码形式,不管是不是 ASCII 字符统一使用两个字节。

    方案选择上主要从计算机处理过程中的时间和空间两个维度,也就是编解码的执行效率和存储大小两方面。

    最后结论是采用双字节定长编码,因为定长带来的空间变大在整体传输、存储成本上其实影响并不大,而定长编码处理效率会明显高于变长编码,所以早期 Unicode 采用了定长编码形式。

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

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

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

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

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

例如下面这句话如果不做字源分离,会是什么情况呢?

原句:户有三种写法,分别是“戶”、“户”、“戸”。
改写后:户有三种写法,分别是“户”、“户”、“户”。

7.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 章节详细介绍。

7.3. Unicode 与 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 编码,而 Windows 操作系统采用的是 UCS-2,他们都是同一个 Unicode 标准。

    为什么这里使用的是2字节编码,而不是4字节呢?请继续往下看。

7.4. 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字节)。

7.4.1. 代理对

    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 编号值之间的计算公式。

换算码元序列(CH 高半区/CL 低半区):

换算字符编号(CH 高半区/CL 低半区):

7.4.2. 平面空间

    UTF-16 把编码空间 0000 ~ 10FFFF 切成了17个平面,其实就是划分成17个区块,每个平面空间码点数都是 65536 个,第一个平面称为基本多语言平面(Basic Multilingual Plane,简称BMP),这个平面涵盖了当今世界上最常用的字符,固定使用定长两个字节,除此之外的字符都放到增补平面里,都是使用两个码元的定长4个字节。

下表是各个平面的用途:

平面Unicode 范围中文名称英文名称
0 号平面U+0000 - U+FFFF基本多文种平面Basic Multilingual Plane,简称 BMP
1 号平面U+10000 - U+1FFFF多文种补充平面Supplementary Multilingual Plane,简称 SMP
2 号平面U+20000 - U+2FFFF表意文字补充平面Supplementary Ideographic Plane,简称 SIP
3 号平面U+30000 - U+3FFFF表意文字第三平面Tertiary Ideographic Plane,简称 TIP
4 号平面至13 号平面U+40000 - U+DFFFF尚未使用
14 号平面U+E0000 - U+EFFFF特别用途补充平面Supplementary Special-perpose Plane,简称 SSP
15 号平面U+F0000 - U+FFFFF保留作为私人使用区(A区)Private Use Area-A,简称 PUA-A
16 号平面U+100000 - U+10FFFF保留作为私人使用区(B区)Private Use Area-B,简称 PUA-B

    增补平面的编号是采用双码元4个字节来表示的,去除代理对之后有效位数是20位,然后将这20位的编号再划成16个平面区域,其中高半区的数字位里取出4位表示平面,剩下的16位表示每个平面可以表示的字符数也就是2的16次方 65536 个(两个字节大小)。

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

7.4.3. 字节序

    字节序顾名思义是指字节的顺序,对于单字节编码来说,一个字符对应一个字节,也就不存在字节序问题。但是对于 UTF-16 这种定长多字节编码,就有字节顺序问题了。

    字节序其实跟操作系统和底层硬件有关,不仅只是 UTF-16 这种多字节编码存在字节序,只要是多字节类型的数据都存在字节顺序问题,比如 short、int、long。

    为了方便说明,我们这里举个例子:比如存一个整数值“305419896”对应16进制是0x12345678,有人习惯从左到右按顺序去存,也有人说高位当然要放到高位地址而低位放到低位地址,要从右往左存。

于是就有了下面两种存取方式:

    其实这两种方式没有孰优孰劣,只是我们认知习惯有所不同导致最终的设计不同,说来这都是阿拉伯人的锅啊,为什么数字高位非要在左边,这也引起了著名的大小端之争。

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

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

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

 

7.5. UTF-8

7.5.1. 概述

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

    由于互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一。

    UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用 1~4 个字节表示一个符号,根据不同的符号而变化字节长度。

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

  1. 如果首字节以 0 开头,则是单字节编码(即单个单字节码元);
  2. 如果首字节以 110 开头,则是双字节编码(即由两个单字节码元所组成的双码元序列);
  3. 如果首字节以 1110 开头,则是三字节编码(即由三个单字节码元所组成的三码元序列),以此类推。

下表总结了编码规则,字母 x 表示可用编码的位:

    根据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。

    理论上 UTF-8 变长可以超过4个字节,只是 Unicode 联盟规范上限是 10FFFF,所以 UTF-8 规则设计上也限制了大小。

下面,还是以汉字“严”为例,演示如何实现 UTF-8 编码:

严 的 Unicode 是 4E25(100111000100101),根据上表,可以发现 4E25 处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的 x,多出的位补0。这样就得到了,严的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5。

7.5.2. 程序算法

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

typedefstruct
{
  intcmask; // 前缀码掩码
  intcval; // 前缀码
  intshift; // 移动位数
  longlmask; // Unicode值掩码
  longlval; // Unicode下限值
} Tab;

staticTab 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 解析的字节长度
 */
intmbtowc(wchar_t*p, char*s, size_tn)
{
  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;
}
7.5.3. 容错性

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

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

    我们再来看 GB2312 这种多字节编码,如果丢失了 b2 字节那么整个文本都乱套了,这是最糟糕的,大部分多字节编码都有类似问题,一旦出现错误可能导致整个文件都需要重传。

    接下来我们看看 UTF-8 是如何避免这种“一颗老鼠屎坏了一锅粥”的情况:UTF-8 的码元序列的第一个字节指明了后面所跟字节的个数,比如首字节高位是0 就表示单字节,110 表示总共两个字节,1110 表示三个字节依次类推,除首字节之外后续字节都是10开头。所以 UTF-8 的前缀码具有很强的鲁棒性,即使丢失、增加、改变个别字节也不会导致后续字符全部错乱这样的传递性、连锁性的错误问题。

八. 总结

    简单来说,Unicode、GBK 和 Big5码 等就是编码的值(也就是术语“字符集”),而 UTF-8、UTF-16、UTF32 之类就是这个值的表现形式(即术语“编码格式”)。

    另外,Unicode、GBK 和 Big5码 等字符集是不兼容的,同一个汉字在这三个字符集里的码值是完全不一样的。如“汉”的 Unicode 值与 GBK 就是不一样的,假设 Unicode 为 a040,GBK 为b030。以 UTF-8 为例,UTF-8 码完全只针对 Unicode 来组织的,如果 GBK 要转 UTF-8 必须先转Unicode 码,再转 UTF-8 才行。即 GBK、GB2312 等与 UTF8 之间都必须通过 Unicode 编码才能相互转换

    看起来好像谁都懂的字符编码知识,深入了解之后发现也有这么浓重的发展历程,试想一下,如果计算机还是跟之前大型机一样,个人计算机没有井喷式发展起来就没有这些字符编码的事了,如果 ASCII 当初就设计成多字节编码,也没有后面 Unicode 什么事了。

    计算机字符编码发展历程其实就是一个很典型的架构设计问题。到底好的架构是设计出来的,还是演化出来的?

    有人说靠演化出来的:没有设计的产品架构是没有灵魂的,发展的路上死的很快。

    有人说靠设计出来的:这是一种完美主义者,你超前设计个50年、100年等你设计出来了,说不定公司都已经倒闭了,有很多叫好不叫座的产品、架构也比比皆是。

    其实,一个好的架构是既要靠设计又要靠演化,老话说得好三分靠设计七分靠演化,我们既要学会务实,也要懂得前瞻,至少我们首先需要活下来。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流华追梦

你的鼓励将是我创作最大的动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值