字符编码(二:简体汉字编码与 ANSI 编码 )

3,910 阅读46分钟

全文链接:

1.3 简体汉字编码演变

发展概述

英文字母再加一些其他标点字符之类的也不会超过 256 个,用一个字节来表示一个字符就足够了(2^8 = 256)。但其他一些文字不止这么多字符,比如中文中的汉字就多达 10 多万个,一个字节只能表示 256 个字符,肯定是不够的,因此只能使用多个字节来表示一个字符。

于是当计算机被引入到中国后,相关部门设计了 GB 系列编码( “GB” 为 “国标” 的汉语拼音首字母缩写,即中华人民共和国国家标准)。

ps:GB 系列只有 GB 2312 码是遵从 ISO 2022 规范的。

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

因此,GB 系列编码方案向下完全直接兼容 ASCII 编码方案。也就是说,如果一段用 GB 编码方案编码的文本里的所有字符都在 ASCII 编码方案中有定义(即该文本全部由 ASCII 字符组成),那么这段 GB 编码实际上和 ASCII 编码完全一样。

最早的 GB 编码方案是 GB 2312,其收录的汉字不足一万个,基本能满足日常使用需求,但不包含一些生僻字,因此后来又在 GB 2312 基础上进行了扩展。

在 GB 2312 基础上扩展的编码方案称之为 GBK(K 为“扩展”的拼音首字母),后来又在GBK的基础上进一步扩展,称之为 GB 18030,加入了一些国内少数民族的文字,一些生僻字被编到了 4 个字节。

包括 GB 2312、GBK、GB 18030 在内的 GB 系列编码方案(不包括 GB 13000,下同,不再赘述;GB13000 编码方案下文有详解介绍),每扩展一次都完全保留之前版本的编码,所以每个新版本都向下兼容。

这里要指出的是,虽然都用多个字节表示一个字符,但是 GB 类的汉字编码与后文的 Unicode 编码方案的 UTF-8、UTF-16、UTF-32 等字符编码方式 CEF 是毫无关系的(其中 UTF-8 对于 ASCII 字符仍用一个字节编码,而非 ASCII 字符则为多字节编码)。

不过,也正因为不得不使用多个字节来表示一个字符,相较于只使用单个字节的 ASCII 编码方案,GB 系列编码方案与后面要介绍的 Unicode 编码方案一样,无疑导致了更高的复杂度(包括时间复杂度、空间复杂度等)。

比如,当多字节字符与原先的 ASCII 字符混用时:

  • 要么将原先的 ASCII 字符重新编码为多个字节表示,以便与其他多字节字符统一起来( UTF-16、UTF-32 等采用的就是这种方法);
  • 要么保持 ASCII 字符为单个字节编码不变,但将其他多字节字符编码中的各个字节的最高位(即首位)设为 1,以避免与字节最高位为 0 的 ASCII 编码相冲突( GB、UTF-8 等采用的就是这种方法)。

前者具有更高的空间复杂度,因为原先只需要单个字节表示的 ASCII 字符,现在也必须用多个字节来表示,显然更为耗费存储空间;后者则具有更高的时间复杂度,因为为了避免冲突以及其他种种考虑(比如扩展性、容错性等),使用了更为复杂的编码算法(Encoding Algorithm),无疑更为耗费计算时间。

而且,无论是前者还是后者,若多字节编码中采用的又是**多字节码元(Code Unit)的话(如 UTF-16、UTF-32 编码采用的就是多字节码元,而 UTF-8 中的非 ASCII 字符虽然也是多字节编码,但采用的却是单字节码元;注意,GB 系列编码虽然除 ASCII 字符外的其余字符为多字节编码,但采用的仍然是单字节码元),由于历史的原因,又进一步引发了更为麻烦的字节序(Byte-Order)**问题。(编码算法、码元、字节序的相关介绍,详见后文解释)

GB 2312 码

GB 2312 编码方案,即《信息交换用汉字编码字符集——基本集》,是由中国国家标准总局于 1980 年发布、1981 年 5 月 1 日开始实施的一套国家标准,标准号为 GB 2312-1980,维基上也称:GB/T 2312GB/T 2312–80GB/T 2312–1980

GB 2312 编码适用于汉字处理、汉字通信等系统之间的信息交换,通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持 GB 2312(不包括港澳台地区,他们使用另外几套繁体字编码体系:CCCIICNS 11643BIG5 大五码。原文不做详细解释,自行了解即可)。

GB 2312 编码为了避免与 ASCII 字符编码(0~127)相冲突,规定表示一个汉字的编码(即汉字内码)的字节其值必须大于127(即字节的最高位为 1 ),并且必须是两个大于 127 的字节连在一起来共同表示一个汉字( GB2312 为双字节编码),前一字节称为高字节,后一字节称为低字节;而一个字节的值若小于等于127(即字节的最高位为 0 ),自然是仍表示一个原来的 ASCII 字符( ASCII 为单字节编码)。

因此,可以认为 GB 2312是对 ASCII 的中文扩展(即 GB 2312 完全直接兼容 ASCII ),正如 EASCII 是对 ASCII 的欧洲文字扩展一样,都可以称为 ASCII 超集、8 位版等。

不过,很显然的是,GB 2312 与 EASCII 码的 128~255 这段扩展部分所表示的字符是不同的。也就是说,GB2312 与 EASCII 虽然都兼容 ASCII,但 GB 2312 并不兼容 EASCII 的扩展部分。(ps:GB 2312 与 EASCII 对 ASCII 码的扩展互不兼容)

事实上,目前世界上除 ASCII 之外的其它通行的字符编码方案,基本上都兼容 ASCII(包括直接兼容与间接兼容,详见后文介绍),但相互之间除了兼容 ASCII 字符的部分之外却并不兼容。

GB 2312 标准共收录 6763 个汉字,其中一级汉字 3755 个,二级汉字 3008 个;同时,除了汉字,GB2312 还收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的 682 个字符。

可能是出于显示上视觉美观的考虑,除汉字之外的 682 个字符中,GB 2312 甚至还包括了 ASCII 里本来就有的数字、标点、字母等字符。也就是说,这些 ASCII 里原来就有的单字节编码的字符,又再编了两个字节长的 GB 2312 编码版本。

682 个双字节编码字符就是常说的 “全角” 字符,而这些字符所对应的单字节编码的 ASCII 字符就被称之为 “半角” 字符

v2-80e23eba7a92dc0bb461b2cf6d6eb9a1_1440w

全角、半角

全角字符是中文显示及双字节中文编码的历史遗留问题。

早期的点阵显示器上由于像素有限,原先 ASCII 西文字符的显示宽度(比如 8 像素的宽度)用来显示汉字有些捉襟见肘(实际上早期的针式打印机在打印输出时也存在这个问题),因此就采用了两倍于 ASCII 字符的显示宽度(比如16像素的宽度)来显示汉字。

这样一来,ASCII 西文字符在显示时其宽度为汉字的一半。或许是为了在西文字符与汉字混合排版时,让西文字符能与汉字对齐等视觉美观上的考虑,于是就设计了让西文字母、数字和标点等特殊字符在外观视觉上也占用一个汉字的视觉空间(主要是宽度),并且在内部存储上也同汉字一样使用 2 个字节进行存储的方案。这些与汉字在显示宽度上一样的西文字符就被称之为全角字符。

而原来 ASCII 中的西文字符由于在外观视觉上仅占用半个汉字的视觉空间(主要是宽度),并且在内部存储上使用 1 个字节进行存储,相对于全角字符,因而被称之为半角字符。

后来,其中的一些全角字符因为比较有用,就得到了广泛应用(比如全角的逗号”、问号”、感叹号”、空格span”等,这些字符在输入法中文输入状态下的半角与全角是一样的,英文输入状态下全角跟中文输入状态一样,但半角大约为全角的二分之一宽),专用于中日韩文本,成为了标准的中日韩标点字符。而其它的许多全角字符则逐渐失去了价值(现在很少需要让纯文本的中文和西文字符对齐了),就很少再用了。

现在全球字符编码的事实标准是 Unicode 字符集及基于此的 UTF-8、UTF-16 等编码实现方式。Unicode 吸纳了许多遗留(legacy)编码,并且为了兼容性而保留了所有字符。因此中文编码方案中的这些全角字符也保留下来了,而国家标准也仍要求字体和软件都支持这些全角字符。

不过,半角和全角字符的关系在 UTF-8、UTF-16 等中不再是简单的 1 字节和 2 字节的关系了。具体参见后文。

原文综合了知乎《中文输入法为什么会有全角和半角的区别?》下多位答主的回答,有多处修改。

GBK 码

GB 2312-1980 共收录 6763 个汉字,覆盖了中国大陆 99.75% 的使用频率,基本满足了汉字的计算机处理需要。

但对于人名、古汉语等方面出现的罕用字、生僻字,GB 2312 不能处理,如部分在 GB 2312-1980 推出以后才简化的汉字(如 “啰” )、部分人名用字(如歌手陶喆的 “喆” 字)、台湾及香港使用的繁体字、日语及朝鲜语汉字等,并未收录在内。

于是全国信息技术标准化技术委员会利用 GB 2312-1980 未使用的码点空间,收录 GB 13000.1-1993 的全部字符,于 1995 年 12 月1日发布了《汉字内码扩展规范GBK)》(Guo-Biao Kuozhan 国家标准扩展码,是根据 GB 13000.1-1993( GB 13000 下文有详细介绍),对 GB2 312-1980 的扩展;英文全称 Chinese Internal Code Specification)。

不过,收录了 GB 13000.1-1993 全部字符的 GBK,虽然是基于 GB 2312-1980 进行的扩展,但在编码方式上与 GB 2312-1980并不完全相同(与为了跟国际标准 ISO/IEC 10646接轨的 GB 13000.1-1993 更是完全不同)。

虽然 GBK 跟 GB 2312一样是双字节编码,但 GBK 只要求第一个字节即高字节大于 127 就固定表示这是一个汉字的开始(即 GBK 编码高字节的首位必须是 1;0~127 当然表示的还是 ASCII 字符),不再像 GB 2312 一样要求第二个字节即低字节也必须大于 127(即 GBK 编码低字节首位既可以是 0,也可以是 1 )。

正因为如此,作为同样是双字节编码的 GBK 才可以收录比 GB 2312 更多的字符。

GBK 字符集向后完全兼容 GB 2312,同时还支持 GB 2312-1980 不支持的部分中文简体、中文繁体、日文假名(不过这个编码不支持韩国文字,也是其在实际使用中与 Unicode 编码相比欠缺的部分),共收录汉字 21003 个、符号 883 个,并提供 1894 个造字码位,简、繁体字融于一体

v2-4be74606f5de988f89d9ca0948b03be6_1440w

GBK 的编码框架(Code Scheme):其中 GBK/1 收录除 GB 2312 字符外的其他增补字符,GBK/2 收录 GB2312 字符,GBK/3 收录 CJK 字符,GBK/4 收录 CJK 字符和增补字符,GBK/5 为非中文字符,UDC 为用户自定义字符。

微软早在 Windows 95 简体中文版中就采用了 GBK 编码,也就是对微软内部之前的 代码页 936(即 Code Page 936,简写为 CP936 )进行了扩展,之前 CP936 和 GB 2312-1980 几乎一模一样。(代码页后文有详细介绍)

微软的 CP936 通常被视为等同于 GBK,连 IANA( Internet Assigned Numbers Authority 互联网号码分配局)也将 “CP936” 视作 “GBK” 的别名。

但事实上比较起来,GBK 定义的字符较 CP936 多出了 95 个( 15 个非汉字及 80 个汉字),都是当时没有收入 ISO/IEC 10646(即 UCS )/ Unicode 的字符。( UCS、Unicode 后文有详细介绍)。

GB 18030 码

中国国家质量技术监督局于 2000 年 3 月 17 日推出了 GB 18030-2000 标准,以取代 GBK。GB 18030-2000 除保留全部 GBK 编码汉字之外,在第二字节再度进行扩展,增加了大约一百个汉字及四位元组编码空间。

GB 18030《信息交换用汉字编码字符集基本集的补充》是我国继 GB 2312-1980 和 GB 13000-1993 之后最重要的汉字编码标准,是我国计算机系统必须遵循的基础性标准之一。

2005 年,GB 18030 编码方案在 GB 18030-2000 的基础上又进行了扩充,于是又有了 GB 18030-2005《信息技术中文编码字符集》。

如前所述,GB 18030-2000 是 GBK 的升级版本,它的主要特点是在 GBK 基础上增加了 CJK 中日韩统一表意文字扩充 A 的汉字;而 GB 18030-2005 的主要特点是在 GB 18030-2000 基础上又增加了 CJK 中日韩统一表意文字扩充 B 的汉字。

微软也为 GB 18030 定义了专门的代码页:CP54936,但是这个代码页实际上并没有真正使用(在 Windows 7 的 “控制面板” - “区域和语言” - “管理” - “非 Unicode 程序的语言” 中没有提供选项;在 Windows cmd 命令行中可通过命令 chcp 54936 更改,之后在 cmd 中可显示中文,但却不支持中文输入)。

GB 13000 码

在所有的 GB 编码方案中,除了逐步扩展并保持向下兼容的 GB 2312、GBK、GB 18030 等 GB 系列编码方案,还有一个GB 系列编码方案不兼容的、特殊的 GB 编码方案 — GB 13000 编码方案。(注意,虽然 GBK 的制定,主要目的就是为了收录 GB 13000 中的所有字符,但 GBK 的编码方式与 GB 13000 是完全不同的。因此,习惯上所称的 GB 系列编码方案一般并不包括 GB 13000 在内。)

为了对世界各个国家和地区的所有字符进行统一编码,以实现对世界上所有字符在计算机上的统一处理,国际标准化组织制定了新的编码标准 — ISO/IEC 10646 标准(即 Universal Character Set 通用字符集,简称UCS,与统一联盟制定的Unicode 标准兼容,两者的关系详见后文)。

该标准第一次颁布是在 1993 年,当时只颁布了其第一部分,即 ISO/IEC 10646.1:1993,除了收录了世界上其他文字字符之外,其中也收录了中国大陆、台湾、日本及韩国的汉字,总共 20,902 个。

为了与国际标准接轨,中国于是制定了与 ISO/IEC 10646.1:1993 标准相对应的中国国家标准 — GB 13000.1-1993《信息技术通用多八位编码字符集(UCS)第一部分:体系结构与基本多文种平面》。

2010 年又发布了其替代标准 — GB 13000-2010《信息技术通用多八位编码字符集(UCS)》,此标准等同于国际标准ISO/IEC 10646:2003《信息技术通用多八位编码字符集(UCS)》。

GB 13000 与国际标准 ISO/IEC10646 及 Unicode 标准目前在基本平面(即 BMP,详见后文)上基本保持一致。

v2-994316db9e56b862f3c849e24805228a_1440w

各汉字(中文字符)编码方案之间的关系( Big5 为繁体汉字编码方案,主要通行于港澳台地区,原文不作详细介绍)

CJK 码

CJK 指的是中日韩统一表意文字( CJK Unified Ideographs ),也称统一汉字( Unihan),目的是要把分别来自中文(包含壮文)、日文、韩文、越文中,起源相同、本义相同、形状一样或稍异的表意文字在 Unicode 标准及 ISO/IEC 10646 标准内赋予相同的码点值。( Unicode 标准及 ISO/IEC 10646 标准后文有详细解释)

CJK 是中文(Chinese)、日文(Japanese)、韩文(Korean)三国文字英文首字母的缩写。顾名思义,它能够支持这三种文字,但实际上,CJK 能够支持包括中文(包含壮文)、日文、韩文、越文在内的多种亚洲双字节文字

所谓 “起源相同、本义相同、形状一样或稍异的表意文字”,主要为汉字,包括繁体字、简体字;但也有仿汉字,包括方块壮字、日本汉字(漢字/かんじ)、韩国汉字(漢字/한자)、越南的喃字( ?喃/Chữ Nôm )与儒字( ?儒/Chữ Nho)等。ps:越南汉字看不懂。。。

此计划原本只包含中文、日文及韩文中所使用的汉字和仿汉字,统称中日韩(CJK)统一表意文字(Unified Ideographs)。后来,此计划才加入了越南文(Vietnamese)的喃字,所以又合称为中日韩越CJKV统一表意文字

小结 GB 系列

GB 类字符集均属于双字节字符集 DBCS(Double Byte Character Set)。

注意,这里的 “GB 类字符集” 指的是除了单字节编码的 ASCII 字符之外的部分,因此属于狭义;严格来讲,广义上的 “GB类字符集” 包括了单字节编码的 ASCII 字符以及双字节编码的非 ASCII 字符,因此广义上的 GB 类字符集属于单字节与双字节混合字符集。在一段表述中,具体指的是狭义还是广义,需根据上下文而定。

基于 DBCS 的编码方案里,最大的特点两字节长的中文字符和一字节长的英文字符( ASCII 字符)完全兼容,可以并存于同一个文件内。

因此,在使用基于 DBCS 的编码方案的年代,写程序时为了支持中文处理,必须要注意字符串里的每一个字节的值,如果这个值是大于 127 的,那么就认为一个双字节字符集里的字符出现了。

使用 GB 类编码方案时一般都要时刻记住:一个汉字由两个字节组成(即一个汉字占用的存储空间相当于两个英文字符所占用的存储空间)

1.4 简体汉字编码实现

GB 2312、GBK、GB 18030 等 GB 系列汉字编码方案的具体实现方式是怎样的?区位码是什么?国标码是什么?内码外码字形码又是什么意思?它们是如何转换的,又为什么要这样转换?

下面以 GB 2312 码为例来加以说明。

区位码

整个 GB2312 字符集分成 94 个区,每区有 94 个位,每个区位上只有一个字符,即每区含有 94 个汉字或符号,用所在的区和位来对字符进行编码(实际上就是码点值、码点编号、字符编号),因此称为区位码(或许叫 “区位号” 更为恰当)。

换言之,GB 2312 将包括汉字在内的所有字符编入一个 94 * 94二维表行就是 “区”、列就是 “位”,每个字符由区、位唯一定位,其对应的区、位编号合并就是区位码。

比如 “万” 字在 45 区 82 位,所以 “万” 字的区位码是:45 82(注意,GB 类汉字编码为双字节编码,因此,45 相当于高位字节,82 相当于低位字节)。

ps:多字节码元中,第一个字节即高位字节,后面的字节即低位字节。

区位码名词来源:ISO 2022 用于兼容当时的 7 比特宽的通信协议/通信设备。对于 7 比特宽的编码空间,0x00 -0x1F 保留给控制字符,0x20-0x7F 用来表示图形字符( printing/"graphic" characters )。因此,在 1 个 7 比特的字符编码空间,图形字符总计为 94 个(由于空格符占用了 0x20 码位、Del 符占用了 0x7F 码位)或者 96 个。对于双字节的 7 比特编码空间,图形字符可以有 94 x 94 即 8836 个。对于三字节的 7 比特编码空间,图形字符可以有 94 × 94 × 94 即 830584 个(虽然没有三字节字符集向 ISO 登记)。1970 年代至 1980 年代,中文、日文、韩文的字符集汉字编码数量基本上在这个范围内。对于双字节编码的字符的每个 code point,日文译作区点,中文译作码位;area 在中日文均译作 “区”point 在日文译作“点”,在中文译作 “位”。因此,GB 2312 及其相关字符集国标,采用了 “区位码”。

在 GB 2312 字符集中:

  • 01~09 区( 682个):特殊符号、数字、英文字符、制表符等,包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母等在内的 682 个全角字符;

  • 10~15 区:空区,留待扩展;

  • 16~55 区( 3755 个):常用汉字(也称一级汉字),按拼音排序;

  • 56~87 区( 3008 个):非常用汉字(也称二级汉字),按部首/笔画排序;

  • 88~94 区:空区,留待扩展。

国标码(交换码)

为了避开 ASCII 字符中的不可显示字符 0000 0000 ~ 0001 1111(十六进制为 0 ~ 1F,十进制为 0 ~ 31 )及空格字符0010 0000(十六进制为 20,十进制为 32)(至于为什么要避开、又为什么只避开 ASCII 中 0~32 的不可显示字符和空格字符,后文有解释),国标码(又称为交换码)规定表示汉字的范围为( 0010 0001,0010 0001 ) ~ ( 0111 1110,0111 1110 ),十六进制为( 21,21 ) ~ ( 7E,7E ),十进制为( 33,33 ) ~ ( 126,126 )。

因此,必须将 “区码” 和 “位码” 分别加上 32(十六进制为 20,可写作 20H 或 0x20,后缀 H 或前缀 0x 都可表示十六进制),作为国标码。也就是说,国标码相当于将区位码向后偏移了 32,以避免与 ASCII 字符中 0~32 的不可显示字符和空格字符相冲突。

**注意,**国标码中是分别将区位码中的 “区” 和 “位” 各自加上 32( 20H )的,因为 GB 2312 是 DBCS 双字节字符集,因此国标码属于双字节码。

这样我们可以算出 “万” 字的国标码十进制为:(45+32,82+32)=(77,114),十六进制为:(4D,72),二进制为:(0100 1101,0111 0010)。

内码(机内码)

不过国标码还不能直接在计算机上使用,因为这样还是会和早已通用的 ASCII 码冲突,从而导致乱码。

比如,“万” 字国标码中的高位字节 77 与 ASCII 的 “M” 冲突,低位字节 114 与 ASCII 的 “r” 冲突。因此,为避免与 ASCII 码冲突,规定国标码中的每个字节的最高位都从 0 换成 1,即相当于每个字节都再加上 128(十六进制为 80,即 80H;二进制为 1000 0000 ),从而得到国标码的 “机内码” 表示,简称 “内码”,这样就做到了与基础 ASCII 码的兼容。

由于 ASCII 码只用了一个字节中的低 7 位,所以,这个首位(最高位)上的 “1” 就可以作为识别汉字编码的标志,计算机在处理到首位是 “1” 的编码时就把它理解为汉字,在处理到首位是 “0” 的编码时就把它理解为 ASCII 字符。

比如:

  77 + 128 = 205(二进制为 1100 1101,十六进制为 CD );

  114+ 128 = 242(二进制为 1111 0010,十六进制为 F2 )。

我们可以来检验一下。打开记事本输入 “万” 字,编码选择为 ANSI( Windows 记事本中的 ANSI 编码在简体中文版操作系统中默认为 GB 类编码,详见后文解释),保存,如下图所示。

v2-b9920f79774fd1aaf31bb00130f062f7_1440w

然后用二进制编辑器(比如 UltraEdit)打开刚才保存的文件,切换到十六进制模式,会看到:CD F2,这就是 “万” 字的内码,如下图所示。

v2-45d5fe3b1261b8f69549cbf08c46a26d_1440w

三码之间转换

从区位码(国家标准定义) ---> 区码和位码分别 + 32(即 + 20H )得到国标码 ---> 再分别 + 128(即 + 80H)得到机内码(与 ACSII 码不再冲突)。

因此,区位码的区和位分别 + 160(即 + A0H,32 + 128 = 160 )可直接得到内码。用十六进制表示就是:

区位码(区码, 位码)+( 20H, 20H )+( 80H, 80H )= 国际码(区码, 位码)+( A0H, A0H)= 内码(高字节, 低字节)。

(注:十六进制数既可通过添加后缀H来表示,也可通过添加前缀0x来表示)

注:十六进制数既可通过添加后缀 H 来表示,也可通过添加前缀 0x 来表示。

为什么要加上 20H 和 80H ?

区位码、国标码、内码的转换(ps:可以理解为三码相对 ASCII 各自做偏移)非常简单,但令人迷惑的是为什么要这么转换?

首先,需要注意到一点,GB 2312 虽说是汉字编码方案,但其实里面也有针对 26 个英文字母和一些特殊符号的编码,按理说这些和 ASCII 重合的字符(33~127)应该无需再重新编码,直接沿用 ASCII 编码不就行了?

加 20H 原因

原来,当时在制定 GB 2312 时,决定对 ASCII 中的可打印字符,也就是英文字母、数字和符号部分( 33~126,127 为不可打印的 DEL )重新编入 GB 2312 中,以两个字节表示,称之为全角字符(全角字符在屏幕上的显示宽度为 ASCII 字符的两倍,后来也因此而将对应的 ASCII 字符称之为半角字符)。

而对于 ASCII 中前 32 个不可显示也不可打印的控制字符( ASCII 码为 0~31 ),以及第 33 个可显示但不可打印的空格字符( ASCII 码为 32 )等一共 33 个不可打印字符的编码则直接沿用,不再重新编码。

因为要保留这 33 个不可打印字符,就不能直接采用区位码作为计算机直接处理的机内码,需要将区位码向后偏移 32 以避开冲突(为什么是偏移 32,而不是偏移 33?因为区位码中的区码和位码都是从 1 开始计数的,不像 ASCII 码是从0开始计数的)。

十进制数字 32 的十六进制表示就是 20H,这也就是区位码的区码和位码都分别要加上 20H 才能得到国标码的原因。

加 80H 原因

不过,如果直接采用国标码作为计算机直接处理的机内码的话,还是会与SCII 编码产生冲突,导致乱码。

因为国标码虽然相较于区位码避开了 ASCII 码中 0~32 的前 33 个不可打印字符,但并没有避开 ASCII 码中的英文字母、数字和符号等可打印字符( 33~126,共 94 个字符)以及不可打印的 DEL(127)。也就是说,国标码并不是完全兼容ASCII 码的。

ps:前文说了,GB 2312 采用了全新的双字节(即全角)33 ~ 126、127等,原版基础 ASCII 是半角字符,这是不兼容的。

为了彻底避免与 ASCII 码的冲突,考虑到 ASCII 码只使用了一个字节中的低 7 位,其最高位(即首位)总是为 0,于是决定将国标码中每个字节的最高位设为 1(国标码两个字节中的最高位都总是为 0,即国标码中的每个字节与 ASCII 码一样实际上也只用了一个字节中的低 7 位),这就是 GB 2312 的机内码(即内码),简称 GB 2312 码

这样一来就彻底区分开了 ASCII 码和 GB 2312 码。这也是为什么国标码还要加上( 80H,80H)才能得到机内码的原因。

未解决的疑问

原文:看到这里,有人或许又要问了:如果仅仅是为了避免与 ASCII 码相冲突,为什么最初不直接将区位码的区码和位码的最高位从 0 改为 1(相当于各自直接加上 128 ),这样不就无需经过国标码多此一举的中间转换了吗?而且还无需后移32,也就不用浪费这部分编码空间。

对此本人也很困惑,在网上搜了很久也没找到答案,因此具体原因不得而知。或许是一开始考虑不周?或许是为了未来扩展所需而预留一部分空间?又或许是有其他不得已的原因?有知道的朋友还望能指点迷津。

原作者的这个疑问在知乎 - 原作者此文下方评论中得到了一个合理的解释:(回答者知乎 ID:中等难度的贪吃蛇)据我所知内码避开 ASCII 控制字符的原因,可能是为了容错。文字信息在储存、传输时,难免发生某一个 bit被倒转( 0 变为 1 或 1 变为 0 )的错误。如果内码的某个字节内后 7 位中的一位发生这样的错误,将使得表示的中文字符发生移位;而如果是首个位发生错误,按照现在的编码方式,错误的字节将变成可显示的其他字符。如果设计内码时没有增加那个 20H,后一种错误将导致错误字节变为控制字符,可能在一部分使用控制字符进行操作的设备上引起较大的错误。例如某些电传打字机、电报机等,一个字变成另一个字或者变成字母,没有什么危害,但变成了控制字符比如退格、制表符等,将会导致格式、信息混乱,甚至引起设备故障。

image-20200712183816858

v2-8fb63a6c55cd3ff11fb9911327b7c9fa_1440w

GB 2312 区位码、国标码、内码对照表(其中汉字内码 B0A1 ~ F7FE,共 6763 个)

外码(输入码)

外码也叫输入码输入法编码,是用来将汉字输入到计算机中的一组键盘符号,是作为汉字输入用的编码。

英文字母只有 26 个,可以把所有的字符都放到键盘上(所以西欧、美国等编码标准没有输入码,即不需要外码),而使用这种办法把所有的汉字都放到键盘上是不可能的。所以汉字系统需要有自己的输入码体系,使汉字与键盘能建立起对应关系(映射关系)。

目前常用的汉字外码分为以下几类:

  • 数字编码,比如区位码;

  • 拼音编码,比如全拼、双拼、自然码等;

  • 字形编码,比如五笔、表形码、郑码等。

汉字外码往往会出现重码。所谓重码,指的是同一个汉字外码对应于多个汉字,反过来说,也就是可能有多个汉字的外码是相同的,相当于重复了,所以称之为 “重码”。比如使用拼音作为外码时(即使用拼音输入法输入汉字时,同音字相当多),重码现象是相当普遍的。

当出现重码时,往往需要附加选择编号以具体确定所要输入的汉字(输入法打出汉字后,按键盘数字键选择哪个汉字),这种情况下,可认为外码实际上相当于隐式地包括了选择编号在内。

字形码(输出码)

字形,又称为字型码字模码输出码,属于点阵代码的一种。

为了将汉字在显示器或打印机上输出,把汉字按图形符号设计成点阵图,就得到了相应的点阵代码(字形码)。

也就是用 0、1 表示汉字的字形,将汉字放入 n 行 * n 列 的正方形(即点阵)内,该正方形共有 n^2 个小方格,每个小方格用一位二进制数表示,凡是笔划经过的方格其值为 1,未经过的方格其值为 0。

显示一个汉字一般采用 16×16 点阵或 24×24 点阵或 48×48 点阵。已知汉字点阵的大小,可以计算出存储一个汉字所需占用的字节空间。

比如,用 16×16 点阵表示一个汉字,就是将每个汉字用 16 行,每行 16 个点表示,一个点需要 1 位二进制数,16 个点需用 16 位二进制数(即 2 个字节),所以需要 16 行× 2 字节 / 行= 32 字节,即以 16×16 点阵来表示一个汉字,字形码需要 32 字节。

因此,字节数 = 点阵行数 ×(点阵列数 / 8 )。

ps:各位需要注意,原文中的汉字点阵并非显示器的像素点阵不能混淆了概念

字形码主要是计算机内部用于输出(显示输出或打印输出)字形之用,我们所看到的只是文字字形而已,字形码本身是无法直接 “看到” 的。

image-20200712185951482

显然,字形码所表示的字符,相对于抽象字符表 ACR 里的 “抽象” 字符,可称之为 “具体” 字符,因为已经具有了 “具体” 的外形。

v2-ae9d20753295a4afbbf257d77dfda212_1440w

字库、字摸库

为了将汉字的字形显示输出或打印输出,汉字信息处理系统还需要配有汉字字形库,也称字模库,简称字库,它集中存储了汉字的字形信息。

字库按输出方式可分为显示字库和打印字库。用于显示输出的字库叫显示字库,工作时需调入内存。用于打印输出的字库叫打印字库,工作时无需调入内存。

字库按存储方式也可分为软字库硬字库。软字库以字体文件(即字形文件)的形式存放在硬盘上,现多用这种方式(软字库)。硬字库则将字库固化在一个单独的存储芯片中,再和其它必要的器件组成接口卡,插接在计算机上,通常称为汉卡。这种方式现已淘汰

小结

可以这样理解,为在计算机内表示汉字而采取统一的编码方式所形成的汉字编码叫内码。为方便汉字输入而形成的汉字编码为外码,也叫输入码。为显示输出和打印输出汉字而形成的汉字编码为字形码,也称为字模码、输出码。

v2-d0b85c4227104092cf3e05ace1327d36_1440w

汉字从输入到输出过程

通过键盘输入汉字外码,然后输入法将汉字外码(输入码)转换为当前操作系统所默认采用的字符编码方案的字符编号(即码点值),再根据字符编号通过代码页查表的方式转换为汉字内码(机内码)(代码页详见后文的介绍),以实现输入汉字的目的;然后根据所选择的字体,通过汉字内码在字模库(即字库)中找出与字体相对应的字形码(输出码),从而将汉字内码转换为汉字字形码,以实现显示输出和打印输出汉字的目的。

v2-cfd2acfbd91133bfbfba52765872ccf4_1440w

事实上,英文字符的输入、处理和显示过程大致上也差不多,只不过英文字符不需要输入码(即外码),直接在键盘上输入对应的英文字母即可。

ASCII 码,以及 EASCII、ISO 8859 系列、GB 系列、Big5 和 Shift JIS 等既兼容 ASCII 码又互相之间不兼容的 ANSI 编码(“ANSI编码” 是对世界上各个国家和地区所制定的既兼容 ASCI I码又互相之间不兼容的各种字符编码的统称,下一篇文章将详细介绍这个统称的来龙去脉),都属于传统字符编码模型,而不属于现代字符编码模型,很难直接简单套用现代字符编码模型中的概念来表述。

如果一定要套用的话,就 GB 系列编码而言,勉强来说,区位码相当于现代字符编码模型中编号字符集 CCS 的字符编号,国标码相当于字符编码方式 CEF 的码元序列,而机内码则相当于字符编码模式 CES 的字节序列。

不过,由于 GB 系列编码虽然是多字节编码,但码元却是单字节码元(码元的概念后文有详细介绍),因此不存在字节序问题,也就不存在字符编码模式 CES 中的大端序、小端序的概念(字节序以及大端序、小端序的概念后文有详细介绍)。

1.5 ANSI 编码与代码页

ANSI 编码

如前文所述,在全世界所有国家和地区的文字符号统一编码的 UCS / Unicode 编码方案问世之前,各个国家、地区为了用计算机记录并显示自己的字符,都在 ASCII 编码方案的基础上,设计了各自的编码方案。

欧洲先后设计了 EASCII 和 ISO/IEC 8859 系列字符编码方案;为了显示中文及相关字符,中国设计了 GB 系列编码( “GB” 为 “国标” 的汉语拼音首字母缩写,即“国家标准”之意)。

同样,日文、韩文以及其他世界各个国家和地区的文字都有它们各自的编码。所有这些各个国家和地区所独立制定的既兼容 ASCII 又互相之间不兼容的字符编码(准确地来说应该是既兼容 ASCII 又互相之间不完全兼容,因为这里所说的 “不兼容” 实际上指的是从整体中除开兼容 ASCII 之外的部分,下同,不再赘述),微软将它们统称为 ANSI 编码。

所以,即使知道是 ANSI 编码,还需要知道这是哪一个国家或地区的才能解码;而且,同一份文本,只能采用一种 ANSI 编码方案来编码,比如,无法用同一种 ANSI 编码来表示既有汉字、又有韩文的文本。

为什么叫微软 ANSI 编码

严格来说,ANSI 的字面意思并非字符编码,而是美国的一个非营利组织 — 美国国家标准学会( American National Standards Institute )的缩写。ANSI 这个组织做了很多标准制定工作,包括 C 语言规范 ANSIC,还有与各国和地区既兼容 ASCII 又互相不兼容的字符编码相对应的 “代码页( Code Page )” 标准。

比如 ANSI 规定简体中文 GB 编码的代码页是 936,所以 GB 编码又叫做 ANSI Code Page 936( ANSI 标准的代码页 936 )。

各国和地区既兼容 ASCII 又互相不兼容的字符编码之所以被微软 统称为 ANSI 编码的原因即在于此。

后来,或许是出于沿用统一的称呼之目的,有些在当时还并未被 ANSI 定为标准的代码页,也被微软称之为 ANSI 代码页,比如 CP943 代码页。

在 Windows 系统的编码处理中,ANSI 编码一般代表系统默认的编码方式,而且并不是确定的某一种编码方式 — 在简体中文操作系统中 ANSI 编码默认指的是 GB 系列编码( GB2312、GBK、GB18030 );在繁体中文操作系统中 ANSI 编码默认指的是 Big5 编码(港澳台地区使用的繁体汉字编码);在日文操作系统中 ANSI 编码默认指的是 Shift JIS 编码,等等。可在系统区域设置的系统 Locale 中查看、更改。(本文后面有详细介绍)

代码页

原作者注:有关代码页的内容,网上资料较少,因此下面有关代码页的内容无法通过对多方资料来源的比对甄别以去芜存菁,错漏可能在所难免,还望多多指正。

代码页的英文为 Code Page,往往简称为 CP。代码页也称为 “内码表”,是计算机中与特定字符集(准确地说是字符集的某个字符编码方式 CEF )相对应的一张字符编码对照表(这里的字符编码实际上指的是字符编码模式 CES,因此实际为 “字符 - 字节” 或 “字符 - 字节序列” 对照表,详见后文)。

代码页由来

最初,IBM 用代码页来称呼其计算机 BIOS 所支持的字符编码。当时通用的操作系统都是命令行界面的,这些操作系统直接使用 BIOS 提供的字符绘制功能来显示字符(或者是一组嵌入在显卡字符生成器中的字形)。这些 BIOS 代码页也被称为 OEM 代码页。

随着图形用户界面操作系统的广泛使用(最初被广为接受的图形用户界面操作系统是 Windows 3.1),操作系统本身具有了字符绘制的功能。微软于是在 Windows 操作系统还没有转向 UTF-16UTF-16 的推出要早于现在被广为认可的 UTF-8 )作为编码实现之前(即 Windows 2000 发布之前),基于 ANSI 代码页(早期 ANSI 编码称呼)标准定义了一系列支持 ANSI 编码的代码页,因而被称作 “ANSI 代码页”。代表性的是实现了 ISO 8859-1(即 Latin-1 )的代码页1252 (即 CP1252 ),以及实现了 GBK 的代码页 936(即CP936)。

现代操作系统中不同的语言和区域设置可能使用不同的代码页。

除了上面提到的较为常见的 ANSI 代码页(即微软所采用的代码页标准),以及 IBM 代码页,其他有些商业巨头也制定有自己的代码页,比如 Oracle 代码页、SAP 代码页,还有由多家公司联合制定的代码页,比如 EUC 代码页( EUC 为Extended Unix Code 的缩写,是由多家 Unix 系统开发公司针对 Unix 系统而联合制定的代码页,使用 8 位编码(即采用 8 位单字节码元)来表示字符,于1991年标准化。EUC 现在主要用于 Unix、MacLinux 等类 Unix 系统中表示及储存汉语、日语及朝鲜语文字)。

后来代码页进一步扩展,除了原先针对 ANSI 编码所定义的 ANSI 代码页,针对 Unicode 字符集的各 UTF 编码方式( UTF-8、UTF-16、UTF-32 等),各个组织和厂商往往也定义了相应的代码页。

代码页与字符集对应关系

另外,不同的组织或厂商,对于同一种编码方式往往使用各自不同的代码页名称(一般都以数字来进行命名区分)。例如,UTF-8 在 IBM 称作代码页 1208(即 CP1208 ),在微软称作代码页 65001(即 CP65001 ),在 SAP 称作代码页4110(即 CP4110 );Windows 使用 936 代码页 (CP936)、Mac 系统使用 EUC-CN 代码页来表示 GBK 编码(EUC-CN 在类 Unix 系统中相当于 GBK 编码方案的别名,等同于Windows下的 CP936 代码页)。

(附:微软公司定义的代码页一览表:Code Page Identifiers - Windows applications

需要注意的是,在实践中,代码页一般与其所直接对应的字符集之间并非完全等同,往往因为种种原因(比如标准跟不上现实实践的需要)而会对字符集有所扩展。

例如,微软所采用的对应于 ISO 8859-1 字符集(即 Latin-1 字符集)的 ANSI 代码页 1252(即 CP1252 ),就对 Latin-1 字符集有所扩展,其中编码 128~159 也被定义了字符,这是与 Latin-1 字符集不同之处,用于表示英语和大多数欧洲语言(西班牙语和各种日耳曼 / 斯堪的纳维亚语)。另外,IBM 所采用的对应于 Shift JIS 日文字符集的 OEM 代码页932(即 CP932 ),也对 Shift JIS 有所扩展;而对应于 Shift JIS 字符集的微软 ANSI 代码页 943(即 CP943 ),也同样对 Shift JIS 有所扩展。

代码页表现形态

代码页可以体现为从字符映射到单字节值或多字节值的一张表格

注意,针对 ANSI 编码而言,虽然其属于传统字符编码模型,但从现代字符编码模型的角度来看,这里所提到的单字节值与多字节值指的是特定于系统平台的物理意义上的字节序列,不是指与系统平台无关的逻辑意义上的码元序列(虽然对属于传统字符编码模型的早期字符编码方案而言,字符的码元序列与字节序列其实是一样的)。

而针对属于现代字符编码模型的 Unicode 字符集的各 UTF 编码方式而言,则更是同样如此。比如针对 UTF-16 所定义的代码页,其存储的是针对 UTF-16 这种字符编码方式 CEF 的某种字符编码模式 CES(即大端序或小端序之一,大端序、小端序的概念后文有详细介绍)。

正因为这样,代码页也被称之内码表。

也就是说,代码页是字符集在计算机中的具体编码实现;特别是从现代字符编码模型的角度而言,代码页可认为是字符集的某种字符编码方式 CEF 的具体字符编码模式 CES 在计算机中的具体实现,可以将其理解为一张“字符 - 字节”(或更准确地理解为“字符 - 字节序列”)映射表,计算机通过查表实现 “字符 - 字节” 之间的双向 “翻译”

代码页作用机制、查表

代码页主要用于具体实现各编码方案中的字符在计算机系统中的物理存储显示。当计算机读取了一个二进制字节,那这个字节到底属于哪个字符,就需要到存储在计算机中的某个代码页中查找,这个查找的过程就被称为查表

比如,当使用输入码(即外码)输入汉字时,输入法软件需要将输入码(出现重码时另加选择编号)根据代码页转换为机内码(即查表)进行存储,以及再根据机内码和相应的字体设定到对应的字体文件中查找字形码进行显示(ps:前文有详细介绍,《 1.4 简体汉字编码实现 > 小结 > 汉字从输入到输出过程》)。

Win 系统代码页设置

在 Windows 中,代码页是系统默认设置的(即默认系统区域设置),也可在( Windows7 的,ps:Win10 同理)“控制面板 - 区域和语言 -管理-非 Unicode 程序的语言 - 更改系统区域设置” 中选择列表中的语言进行更改。

**注意,**系统区域设置 System Locale 可用于确定在不使用 Unicode 编码的程序(即非 Unicode 程序)中输入和显示字符的默认编码方案(显然主要是指 ANSI 编码方案)和字体,这样就可以让非 Unicode 程序在计算机上使用指定的语言(实质上是使用指定的 ANSI 编码)得以正常运行。

因此,在计算机上安装某些非 Unicode 程序时,如果出现乱码,则可能需要更改默认的系统区域设置。为系统区域设置选择不同的语言并不会影响 Windows 系统本身或其他使用 Unicode 编码方案的程序(即 Unicode 程序)的语言显示。

但是,很显然,如果同一个操作系统中,多个非 Unicode 程序采用了各不相同的 ANSI 编码,则同一时间只有一种 ANSI 编码的非 Unicode 程序的语言显示是正常的,采用其他 ANSI 编码的非 Unicode 程序的语言显示为乱码;而同一个非Unicode 程序是无法采用不同的 ANSI 编码的,比如同时采用中文和韩文,在非 Unicode 程序中是无法实现的,因为同一时间必定有其中之一是乱码的。

不过,现在 Unicode 编码方案已经成为了主流,非 Unicode 程序已经难得一见了。

Win 系统中的 Locale

微软为了适应世界上不同地区用户的文化背景和生活习惯,在 Windows 中设计了区域(Locale)设置的功能。

Locale 是指特定于某个国家或地区的一组设定,包括代码页,以及数字、货币、时间和日期的格式等。

在 Windows 内部,其实有两个 Locale 设置:系统 Locale 和用户 Locale。系统 Locale 决定代码页,用户 Locale 决定数字、货币、时间和日期的格式等。

可以在 Windows 控制面板的 “区域和语言选项” 中设置系统 Locale(非 Unicode 程序的语言)和用户 Locale(标准和格式)。

系统 Locale 对应的代码页被作为 Windows 系统的默认代码页。在没有明确指定某个文本所采用的编码方案时,Windows 系统将按照系统 Locale 中所指定的默认代码页(实质上代表了某个编码方案)来解释该文本数据。这个默认代码页通常被称作 ANSI 代码页(简称 ACP;注意,如前所述,虽然非 ANSI 编码的 Unicode 各 UTF 编码也同样定义了代码页,但系统 Locale 中所设定的默认代码页之所以通常被称为 ACP,是因为系统 Locale 主要是针对非 Unicode 程序而设置的)。

在 Windows XP 的 “区域和语言选项” 高级页面的 “代码页转换表” 中,可看到各种语言(实质上是各个编码方案)的代码页(但 Windows7 中已经不能直接看到了)。

下一篇 字符编码(三:Unicode 编码系统与字节序)