Unicode、UTF、ASCII —— 字符集编码那些事儿
来自:http://www.cnblogs.com/del/archive/2008/11/19/1336444.html
区位码:
在 GB2312 时提出的, GB2312 是一个 94*94 的二维表, 行就是 "区"、列就是 "位", 譬如 "万" 字在 45 区 82 位, 所以 "万" 字的区位码是: 45 82.
00-09 区(682个): 是符号、数字、英文字符...制表符等;
10-15 区: 空白, 留待扩展;
16-55 区(3755个): 常用汉字(也有叫一级汉字), 按拼音排序;
56-87 区(3008个): 非常用汉字(也有叫二级汉字), 这是按部首排序的;
88-94 区: 空白, 留待扩展.
国际码:
咱们的 GB2312 用一个二维表表示了咱们需要的字符,其他文字可能也是如此。为了区别, 所以有国际组织规定把咱们的 "区" 和 "位" 分别加上32(十六进制表示: $20; 二进制表示: 00100000) 作为国际码(那其他文字应该加另外一个不同的数字)。
这样我们可以算出(45+32, 82+32):
"万" 字的国际码是 77 114($4D72)
77 = 0111 0111
114 = 0111 0010
77和114这两个字节还是以0开头,所以还是不能用于ANSI编码里,否则很难分清你到底是要一个“万”字,还是要M和r这两个英文字符。
内码:
不过这还不能在计算机上使用,因为这样会和早已通用的 ASCII 码混淆(导致乱码)。譬如: 77 是 ASCII 的 "M", 114 是 ASCII 的 "r"。所以又有规定把每个字节的最高位都从 0 换成 1(这之前它们都是 0),或者说把每个字节(区和位)都再加上 128(十六进制的: $80;), 从而得到 "机内码", 也就是前面所说的 "内码"。
77 + 128 = 205 = CD
114+ 128 = 242 = F2
打开记事本输入 "万" 字,保存(编码选择 ANSI); 然后用二进制编辑器(譬如: UltraEdit) 打开, 会看到: CD F2, 这就是 "万" 字的内码! (当然,已经在计算机里处理了,以最实用的标准为准;而之前两种编码都只是理论上的标准)
总结一下: 从区位码(国家标准定义) ---> 区和位分别 +32 得到国际码(不再国际混淆)---> 再分别 +128 得到内码(与ACSII也不再混淆); 区位码的区和位分别 +160 即可得到内码。用十六进制表示: 区位码 + $A0A0 = 内码。
内码输入法状态下:可以同时用区位码和内码输出,但不能用国标码输出!
{查汉字区位码}
function Str2GB(const s: AnsiString): string;
const
G = 160;
begin
Result := Format('%d%d', [Ord(s[1])-G, Ord(s[2])-G]);
end;
{通过区位码查汉字}
function GB2Str(const n: Word): string;
const
G = 160;
begin
Result := string(AnsiChar(n div 100 + G) + AnsiChar(n mod 100 + G));
end;
{测试}
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowMessage(GB2Str(4582)); {万}
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
ShowMessage(Str2GB('万')); {4582}
end;
end.
获取区位码表: 准备个 Memo 接收(注意使用了上面的函数)
var
i,j: Byte;
s: string;
begin
s := '';
for i := 1 to 94 do
begin
for j := 1 to 94 do
s := Format('%s %s', [s, GB2Str(i*100 + j)]);
Memo1.Lines.Add(s);
s := '';
end;
end;
1 引言
在计算机编程和文本处理等相关领域,乱码、字节匹配不当或字符乱序是编辑调试过程中经常出现的问题。这类问题的产生,绝大部分都与编码理论有重要关联。如果对编码理论只知其然但不知其所以然,甚至是一窍不通,那么碰上这类问题将无从下手,无法从源头上解决。
展开具体分析前,我不妨先罗列几个与编码理论相关的术语作为引子:ASCII,Unicode,UTF-8,UTF-16,UTF-32,UCS,BOM,BMP,GB2312,GBK,GB18030……任何接触过计算机编程的人,都必定看过这些字母与数字的组合。然而,若要求详细诠释其中某一术语的含义,或只需做简单解释,可能就少有人能够做到。从本质上去理解、分析字符编码理论,从而找出问题根源所在,对症下药,才是“质”的解决方案。了解计算机字符编码基本理论的重要性就在于此。
2 字符集与字符编码
在真正弄清楚上述这堆编码术语之前,必须先区分两个概念:字符集与字符编码。
所谓“字符集”,顾名思义,即数量庞大的字符集合,而这个集合必定囊括了某一或某些具有共同特征的语言文字。但此处所指“语言文字”的表现形式,并非人们日常书写,或在报刊杂志和浏览器看到的自然语言文字,也不是机器最底层只包含0和1的二进制比特流,而是一种中间表现形式。这种表现形式在编码理论中称之为“码点”。
举个例子,在Unicode字符集的BMP平面中(下文会解释什么是BMP平面):
英文字母“A”的码点为“U+ 0041”;
中文“汉”字的码点为“U+ 6C49”。
一句话总结:当把每个自然语言文字,包括少部分不可打印的控制符(例如回车符)与码点建立起一一对应的映射关系后,这些码点的集合,就称之为字符集。(见图-1)
图-1 语言文字与字符集的映射关系
对于字符编码,则是另一个与字符集密切相关、但却有本质区别的概念。为了更加形象直观地说明这二者的区别,我以接口的定义与实现为例,请看下图:
图-2 JAVA中的接口与实现
显然,“Fly”是一个接口,该接口只定义了“飞翔”这个行为,但并没有规定由谁来如何执行。因此,必须有一个“Bird”类去实现这个接口。
为何举这个例子?因为此类关系正是“字符集”与“字符编码”这两个概念的本质联系(不妨也可以说是本质区别)。对字符集而言,它仅仅只规定了每个字符的码点是什么,但并没有规定这个码点实际的表现(实现)形式。换句话说,同一个码点,在不同的编码方案下得出的码字是不一样的。不仅有“位(Bit)”上的不同,也有“字节(Byte)”上的不同。这其中最典型的代表,就是Unicode字符集与其对应的三种编码转换方案UTF-8、UTF-16和UTF-32。同一个字符集中的码点,在这三种编码转换方式下,其实际码字并不相同。
可能有人会产生疑问:为何同一个字符集却要搞出那么多套不同的编码方案?这岂不是大大增加了其复杂性,显得晦涩难懂?这其中的原因,其实就是“一对一”和“一对多”的辩证关系。如图-3所示,对于ASCII和GB标准系,这二者就是严谨的“一对一”关系,即“一个字符集只对应一种编码方案”,没有多余的第二种了。包括早期的Unicode,从某种意义上讲,也可看成是仅与UTF-16(或UCS-2)一一对应的关系。但随着Unicode的不断发展与完善,人们逐渐发现:仅由一套编码方案来处理数量极为庞大的Unicode字符集显得有些捉襟见肘,不够灵活和高效。因此才衍生出了UTF-8、UTF-32等编码方案进行补充与扩展。
一句话再总结之:字符编码是为了实现字符集而存在,一个字符集可以有多种编码方案(实现方式),但一种编码方案只能对应一个字符集。
图-3 字符集与字符编码的联系
3 Unicode
3.1 为何要制定Unicode
据前德意志**出版的《语言学与语言交际手段指南》记载,当今世界范围内,已知语言共有5651种,公认的独立语言有4200种。其中100万以上人口使用的语言有19种。使用人数最多的语种是汉语,使用国家最多的语种是英语,有30多个国家。
在Unicode产生前,从未有任何一个字符集能够囊括所有语种中的所有字符(或者说绝大部分)。这就给不同国家、不同语言之间在文字编码交流方面造成了诸多困难。你有你的一套编码标准,我自然也有我的一套,当二者想进行交流通讯时,必定会产生冲突、乱码,甚至系统崩溃。
Unicode正是为解决这一历史局限问题而产生的。Unicode最大的生存价值和目标,是为每个语种中的每个字符,统一制定唯一的一个编码,这个编码就是我们前面提到的“码点”。因此, Unicode又被称为万国码、统一码或国际码。显然,当把全世界所有语言的文字字符都规定好唯一的码点之后,上述提到的问题将全部迎刃而解,并且解决得非常彻底。这是一项耗时、工程浩大、但又一劳永逸的精细活。
3.2 BMP平面
Unicode字符集将全部码点空间均分为17个大小相等的区域,每个区域有65536(即2的16次幂)个码点。这里所划分的“区域”称之为“平面(plane)”,编号从0开始,第一个平面就是BMP平面,全称为“Basic Multilingual Plane”,即“基本多语言平面”。我们日常所接触的绝大部分字符都落在这个BMP平面内。
而后续的16个平面称为“SP(Supplementary Planes),辅助平面”。目前这些平面很多仍然是空的,留作扩展和私用。
3.3 鸡蛋的两端:big endian 与 little endian
这是Unicode一个比较有意思的地方。所谓big endian和little endian,即表示字符储存顺序的“大端法”和“小端法”。以“汉”字的Unicode码点“6C 49”为例,我们将其拆分为高字节“6C”和低字节“49”。这里的“高低”,也就是对应我们平常从左往右的书写习惯,高位在左边先写,低位在右边后写,这就是大端法。显然大端法符合人们对语言文字“看”和“写”习惯的表示方法。
那么与之对应的“小端法”,无非就是把大端法颠倒过来,即把原本“6C 49”的书写顺序颠倒为“49 6C”。到这里必定又有读者产生疑问:为何又无端搞出一个与人们书写习惯相反的小端法?
解释这个问题,得从计算机内部的储存机制分析。
我们知道,一个双字节字符在系统内存的储存形式是:先被拆分为两个字节,然后紧邻着内存空间进行储存。但这里就有一个问题了:我们假定分配两个连续的内存地址空间0x00与0x01,用来储存“汉”字的两个字节。那么,按照上文提到的大端法,它在内存中是这个样子:
图-4 大端法和小端法的高低字节与内存高低地址关系示例
显然,原本的高字节“6C”被放在了内存的低地址区,而低字节“49”却被放在了高地址区。看似符合人们书写习惯的大端法,在系统内部储存结构中反而被完全颠倒了顺序。但由上图可明显看到,小端法却恰恰适合计算机内存的读取顺序:高字节储存在高地址区,低字节储存在低地址区,即内存地址和字符字节都是从高位到低位一一对应。
那么,操作系统从何得知一个文本文件是使用了大端法还是小端法?显然这很重要,假如对一个用大端法储存的文件,却用小端法去读取,那必定造成严重的乱码。前文提到的BOM,正是用来解决这个问题的一种标识。
在Unicode编码中,通常会在文件的首部加入一个BOM文件头。BOM全称为Byte Order Mark(字节顺序标识)。例如在UTF-16中,大端法的BOM为“FE FF”,小端法的BOM为“FF FE”,而十六进制“FF”刚好比“FE”大1。因此,可用这两个标识来区分大端和小端。有了BOM,操作系统就知道文件是使用大端法还是小端法了。
至于系统默认是使用哪一种表示法,这与制造厂商有关。例如Intel平台就是默认用小端法。另外,这两种方法也是只针对多字节而言,对于单字节,没有什么高低字节可言。
3.4 定长与不定长的取舍
何为“定长”与“不定长”?对于某种编码方案,如果无论什么字符都采用固定的字节数去表示,那么就称这种编码方案是定长的。反之,若允许根据不同字符设置不同的字节数来表示,即字节数可变,那么这种编码方案就是变长的。
对定长方案而言,问题的关键是:这个长度应该定多少?全世界有成千上万个字符,那些相对简单的,用一个字节来表示就已经绰绰有余了(例如使用最广泛的英文字母);但是对于相对复杂的字符(例如汉字),用一个字节远远不够,必须要有两个、三个甚至四个字节才能表示。如果定得太短,根本不够表示这些相对复杂的字符;但如果定得太长,对于频繁使用的英文字符,明明一个字节就可以表示,却还得加多几个字节,这对储存空间无疑是一种巨大的浪费,也将严重影响存取和检索效率。
这也正是为什么Unicode字符集无法只用一种编码方案来实现的原因。企图仅用一种能够同时兼顾储存容量和读取效率的方案去统一管理所有字符,对于庞大的Unicode字符集而言,显得有些不现实。因此才会衍生出多种定长与变长的编码方案。
3.5 Unicode转换格式 — UTF
前文提到,Unicode字符集有三种不同的编码方案UTF-x。UTF全称为Unicode Transformation Format,是对字符集的一种编码转换方式。而后面的“x”可以取值8,16和32。这里涉及到一个“代码单元”的概念:数字x表示这种编码方案以x位为一个代码单元,而一种编码方案只能包含整数个代码单元。例如,UTF-8表示以8位(一个字节)为一个代码单元,而UTF-8编码可以包含1到4个字节;UTF-16表示以16位(两个字节)为一个代码单元,而UTF-16编码可包含2或4个字节。这两种编码方案都是“变长”的,即编码所包括的字节数可变;但对于UTF-32,其自身以32位为一个代码单元,单单一个字节已可以容纳所有字符了。因此UTF-32是一种定长的编码方案。
图-5 UTF三种编码方案比较
3.6 UTF-8
在UTF三种编码方案中,最常用、最灵活的正是UTF-8。其灵活性体现在:对于上文提到的复杂字符,UTF-8用多个字节进行编码;而对于那些简单字符,UTF-8只用一个字节编码就可以了。从某种意义上可以说:在UTF-8模式下,一个字符的编码字节个数与这个字符的复杂程度成正比。UTF-8正是通过这种灵活可变的调节方式,在容量与效率之间作出了较好的折衷。
UTF-8的编码规则非常简单,总结起来只有两条:
1、对于单字节字符,其字节最高位置0,剩下7位为这个字符的二进制Unicode码点;
2、对于多字节字符,设其字节数为n(n取值为2,3,4),编码规则如下:对第一个字节,前n位设为1,第n+1位设为0;对第一个字节后面剩下的字节,前两位一律设为10。剩下的没有提及的码位,全部用来存放该字符的二进制Unicode码点。
具体规则图示如下表,最后一栏的“空位数”为表中下划线个数,即除去标志位后剩下用于填充码点的预留位位数。
图-6 UTF-8编码模板
下图是“汉”字的Unicode码点到UTF-8的转换过程:
图-7 UTF-8编码实例
从上表的转换过程,我们可以很直观地看到,一个汉字的Unicode码点映射到UTF-8编码时,并不是整段映射,而是被分段后填充到相应的空位。另外不难发现,UTF-8三字节表示法的第一个字节,均是固定以“1110”开头。“1110”表示为十六进制为字母“E”。因此,若看到一连串的十六进制编码具有如下形式:
“E _ _ _ _ _ E _ _ _ _ _ ”……
即每三个字节中,首字节都是以字母“E”开头,那么这很有可能就是一串汉字的UTF-8编码。
由此可见,在UTF-8编码方案下,无论是单字节还是多字节,其二进制编码都是由规则2中提到的那些标志位与二进制Unicode码点的组合。而且,编码的字节数不同,可用来填充Unicode码点的位数也不同。我们不妨做个简单计算:设b和B分别表示在n某个取值下,除去标志位后剩下的可编码空间位数,并设e表示这种方案的可利用编码空间效率。不难得出,n与b的函数关系如下:
一旦计算出b值,B与e的值也随之确定了 。具体关系如下表所示:
图-8 UTF-8编码效率
由上表可看出,无论是单字节还是多字节,UTF-8的编码空间利用率并不是100%,而是随着字节数的增加逐渐递减。
3.7 UTF-16 / UTF-32
另外两种方案UTF-16和UTF-32,其编码机制与UTF-8大体相同。比较显著的区别是:对BMP平面内的字符,UTF-16采用双字节编码;而对平面外的字符则采用四字节编码。UTF-16较UTF-8的一个好处是,绝大多数经常使用的字符都采用固定长度的双字节编码,显然这具有“定长”的优点。不过,UTF-16无法兼容ASCII编码。这是UTF-16一个明显的不足。
至于UTF-32,则是一个典型的、在前文所涉及“容量”和“效率”这二者的权衡之间,完全倾向于“容量”的一种编码方案。无论对简单或复杂字符,UTF-32一律使用四字节进行编码和储存。即便是只需一个字节就能够表示的简单字符,也必须在前面补0补足到四个字节。作为UTF三种编码方案中唯一的一种定长编码方案,UTF-32在空间浪费方面的缺点,可能已经掩盖了其在定长方面的优点。
3.8 与UCS-2、UCS-4的联系
UCS全称为Universal Character Set(通用字符集)。在概念层面,UCS与Unicode字符集是等价的,都是代表一个字符集,而不是一种字符编码方案。UCS的实现形式有两种:UCS-2和USC-4,分别用双字节和四字节进行编码。
在早期,UCS-2与UTF-16这两个概念其实是同一个意思,都是表示BMP平面内的双字节字符。后来,当出现增补平面后,UCS-2就特指BMP平面内的字符了,而UTF-16在此基础上还包括了BMP平面外超过双字节的字符。从这个层面上讲,UTF-16可以看成是UCS-2的父集。如果不做严格意义上的区分,那么UTF-16就是UCS-2,而UTF-32就是UCS-4。
4 ASCII
ASCII全称是American Standard Code for Information Interchange(美国信息交换标准代码)。所有ASCII码仅在一个字节内进行编码。一个八位字节共有28=256个码位,其中,前128位(即0到127位)称为标准ASCII码,后128位(即128到255位)称为扩展ASCII码。显然,标准ACSII码的最高位均为0,而扩展ASCII码最高位均为1。我们平常所涉及、使用最频繁的默认是指标准ASCII码。
标准ASCII码所定义的128个字符中,前33个为不可打印的控制字符,后95个为可打印的字母、数字、符号等字符。可以说,这128个字符已经能满足西方人日常基本的语言文字交流,其使用频率和效率非常高。
在频繁使用英文的环境下,ASCII码之所以能成为一种最节省空间、同时效率也最高的编码方案,是因为在英文中,无论多复杂的文本或篇幅,拼来凑去说到底不过26个大小写字母和10个数字。比起中文动辄上千级别的字符集,根本无法相提并论。因此,这也是为何在早期,西方人不愿意使用和推广Unicode的最大原因。原本用一个字节就可以表示的字符,现在非得增加到两个字节(甚至更多),而且高字节全部是毫无意义的补零位。对文本文件的储存硬生生比原来多出了至少一倍的空间,这当然难以接受。由此可见,Unicode的初衷虽然是统一全世界的字符集,但所谓众口难调,尤其是面对使用频率最高的英文,在进行统一整合的同时,兼顾好人们使用的方便性与效率,无疑是一个需要重视和解决的问题。
5 GB标准系
所谓“GB”,“G=guo”,“B=biao”,合起来就是“guobiao国标”,即国家标准。我国颁布的汉字编码规范中,最主要、最常见的是GB2312,GBK和GB18030。GB2312是国家最早颁布的一个标准,我们先从它讲起。
GB2312字符集实际上与Unicode字符集类似,只是没有其数量之庞大罢了。GB2312字符集平面图是一个规规矩矩的正方形,横竖均为94,编号从1开始。因此理论上共有94*94=8836个编码空间。GB2312使用定长的双字节编码方案。在这个正方形字符集区域中,竖的叫“区”,用来表示高字节;横的叫“位”,用来表示低字节。因此,一个字符所对应的码点就称之为“区位码”。 GB2312共收录了6763个汉字,另外还有其它一些标点、数字序号、拼音、外文等符号。GB2312字符集平面各区所收录字符分类如下图所示:
5.2 GB2312
图-9 GB2312字符集
然而,区位码只是字符集所规定的码点,实际储存和表示时并不是用区位码。这其中最大一部分原因是为了与ASCII码兼容。前面提到,ASCII码前33个字符为不可打印的控制字符,即序号为0到32。但GB2312字符集编号是从1开始,因此前32个字符有可能与ASCII产生歧义,即计算机不知道到底是代表ASCII码,还是GB2312编码。为了避免与这32个控制符产生冲突,第一步解决方案是:实际使用时,在区位码的高字节和低字节分别加上32(十六进制0x20),以跳过这些控制符。所得的新码点就称之为“国标码”。
但是国标码并没有完全解决与ASCII码潜在的冲突问题。我们知道标准ASCII码是7位编码,最高位均为0。而在GB2312中,如果也存在最高位为0的编码,就仍然有可能产生冲突。因此,最彻底的解决方案是:在国标码的高低字节分别再加上128(十六进制0x80)。相信读者应该也明白这种措施的目的了:128二进制形式为10000000,加上后刚好把国标码的最高位都置为1。这样就与ASCII码完全区分开了。设计机内码的目的就在于此。综上所述,可得出以下特征:
西文字符机内码为单字节,最高位为0;
中文字符机内码为双字节,最高位为1。
图-10 区位码、国标码、机内码相互转换关系
仍旧以“汉”字为例:在GB2312字符集中,“汉”字的区位码为“26 26”,加上32为国标码“58 58”(十六进制“3A 3A”),然后再加上128为机内码“186 186”(十六进制“BA BA”)。因此,“汉”字的机内码就是“BA BA”。
5.2 GBK / GB18030
上文提到“GB”代表“国标”,那么“GBK”就是“国标扩展”,全称为“汉字内码扩展规范”。GBK向下兼容GB2312,向上支持UCS,实际上是一个过渡性的标准方案。GBK共有23940个码位,其中已收录了21003个汉字和883个图形符号。
至于GB18030,则是一个编码空间高达160多万的字符编码方案,其中收录了7万多的汉字(包括各类少数民族文字)。GB18030向下兼容GB2312和GBK,是一种变长编码方案,支持单字节、双字节和四字节。这一点与UTF-8有些类似。
5.3 与Unicode、ASCII的比较
就国际通用性而言,GB标准系肯定不如Unicode。毕竟GB标准系是以中文为主,而不是西文。但如果从对汉字的编码效率而言,GBK不仅兼容ASCII,并统一采用双字节编码;而UTF-8对常用汉字都是采用三字节编码。因此,无论是全中文还是中英混合的情形,GB标准系都要优于Unicode。
6 结束语
狭义至计算机软件工程相关领域,广义至IT界乃至整个互联网,字符集编码理论都占据着相当重要的地位。编码理论是一套相互关联、但同时又非常零碎的体系。本文将其中最重要的主干理论进行归纳和整理,并选取具有代表性的UTF-8和GB2312进行详细分析,有所侧重地介绍计算机字符编码的原理机制。
当然,这篇博文所涉及的内容对于整套编码理论而言,不过是沧海一粟。但理解并掌握这部分基础理论,其必要性还是不容小觑的。尤其对相关领域人员而言,不仅有助于对字符集编码理论形成一个宏观基本的认知,对实际编程工作也将产生一定的辅助作用。