刨根究底字符编码
前言
(图片来自网络)
一、
字符编码是计算机世界里最基础、最重要的一个主题之一。不过,在计算机教材中却往往浮光掠影般地草草带过,甚至连一本专门进行深入介绍的著作都找不到(对这一点我一直很困惑,为什么就没有哪位大牛对这个如此基础、重要而又如此容易让人困惑的主题写一本专著予以介绍呢)。
而在编程实践中,如果不发扬死磕到底的精神将字符编码问题的来龙去脉、前世今生彻底搞清楚,那么它终将会像幽灵一样挥之不去,导致时不时地被各种与字符编码相关的“灵异”事件折磨得死去活来。
本人正是在经受了字符编码所带来的种种令人崩溃的痛苦之后,才在痛定思痛之余,最终痛下决心,誓要将它刨根究底。
二、
字符编码的基础性、重要性,主要体现在它涉及面广。向下涉及到计算机的底层技术,甚至是硬件实现;向上几乎跟所有的操作系统、编程语言、应用程序都密切相关。
因此,要想真正搞明白字符编码问题,必须得从计算机的基本概念——位、字节、字等等开始,再结合不同的系统环境与编程环境,进行具体分析。
三、
类似于字符编码这样基础、重要、应用广泛而又特别容易让人困惑的主题还有字节序(即大小端表示)、正则表达式以及浮点数实现、日期时间处理等等。其中,字节序、正则表达式跟字符编码的关系又密切相关,尤其是字节序,直接影响字符编码的字节序列。而由于正则表达式主要用于在字符串中查找、提取字符或子字符串,要想真正理解正则表达式,也离不开对字符编码的深入理解。
基于此,本人准备将自己对字符编码(包括字节序)与正则表达式进行刨根究底后的一些心得体会写成两个系列文章,一方面整理一下自己的思路以备忘,另一方面也真心希望能够起到抛砖引玉的作用。
(图片来自网络)
四、
下面是字符编码系列文章将会涉及到的内容:
一)关键术语解释:位、字节、字与字长、字符集、编码、解码、字符编码、现代字符编码模型
二)字符编码的由来
三)ASCII字符编码方案
四)扩展ASCII字符编码方案EASCII(Extended ASCII)以及ISO/IEC 8859字符编码方案
五)汉字编码方案:GB2312、GBK、GB18030、GB13000、全角与半角、CJK中日韩统一表意文字
六)汉字编码中区位码、国标码(交换码)、内码(机内码)、外码(输入码)、字形码(输出码)的区别及关系
七)ANSI编码
八)代码页(Code Page)、微软与ANSI代码页
九)Unicode编码方案的面世
十)Unicode字符集概述
十一)字符编码系统(字符编码模型)的变化、字节序
十二)Unicode字符集的编码方式:码点、码元、UTF-8、UTF-16、UTF-32
十三)同样存在多字节编码,为什么说UTF-8没有字节序的问题,而UTF-16、UTF-32却有?
十四)微软为什么跟联通有仇——Windows记事本的字符编码方式
十五)Windows记事本的四种编码方式(ANSI、Unicode、Unicode big endian、UTF-8)有何区别?
十六)深入剖析奇葩的Python字符编码
十七)Vim中的字符编码问题
十八)Unicode常见问题解答
十九)总结
关键术语解释
一、位
即比特(Bit),亦称二进制位、比特位、位元、位,指二进制数中的一位,是计算机中信息表示的最小单位。
Bit是Binary digit(二进制数位)的缩写,由数学家John Wilder Tukey提出,习惯上以小写字母b表示,如8比特可表示为8b。
每个比特有0和1两个可能的值,除了代表数值本身之外,还可代表:
数值的正、负;
两种状态,如电灯的开、关,某根导线上电压的有、无,等等;
一个抽象逻辑上的是、否。
二、字节
在计算机中,通常都会使用一连串的位(比特),称之为位串(bit string比特串)。很显然,计算机系统都不会让你使用任意长度的位串,而是使用某个特定长度的位串。
一些常见的位串长度形式具有约定好的名称,如,半字节(nibble,貌似用的不多)代表四个位的组合,字节(byte)代表8个位的组合;还有字(word)、双字(Double word,简写为Dword)、四字(Quad word,简写为Qword)、十字节(Ten byte,简写为Tbyte)。
字节(byte),又称为位元组,音译为“拜特”(但很少使用这个译名),是计算机中计量存储容量和传输容量的一种基本计量单位,是由连续的、固定数量的位(即比特)所组成的位串(即比特串),一般由8个位组成,即1 byte = 8 bit。习惯上用大写的B表示,如3字节可表示为3B。
现代个人计算机(PC)的存储器编址,一般是以字节为单位的,称之为按字节编址,因此字节一般也是存储器的最小存取单元以及处理器的最小寻址单位(也有按位寻址、按字寻址等等,但在个人计算机上应用不普遍,这里不讨论)。
字节作为存储器的最小存取单元以及处理器的最小寻址单位这一重要特点,跟字符编码的关系极为密切(比如,码元的单字节与多字节、字节序的大端序与小端序等,都与以字节为基础的基本数据类型密切相关,详见后文介绍)。
习惯上,按照下面的图来排列一个字节上的各个位的顺序,即按照从右到左的顺序,依次为最低位(第0位)到最高位(第7位):
注意,字节不一定非得是8位,以前也有过4位、6位或7位作为一个字节的标准,比如IBM 701(36位字长,18位为一字节)、IBM 702(7位字长,7位为一字节)、CDC 6600(60位字长,12位为一字节byte)等,只是现代计算机的事实标准就是用8位来代表一个字节(最终形成这一事实标准除了历史原因和商业原因之外,最重要的原因应该是由于二进制的特性:2的次方计算更方便快捷)。
正是因为这个原因,在很多较为严谨的技术规格文献中,为了避免产生歧义,更倾向于使用8位组(Octet)而不是字节(Byte)这个术语来强调8比特位串。
不过,由于大众基本上都将字节理解为8比特位的8位组,因此一般文章中如果未作特别说明,基本上都将8位组直接称之为字节。
三、字与字长
虽然字节是大多数现代计算机的最小存储单元和传输单元,但并不代表它是计算机可以最高效地处理的数据单位。
一般来说,计算机可以最高效地处理的数据大小,应该与其字的字长相同,这就涉及到了字及字长的概念。
字(Word):在计算机中,一串比特位(位串、比特串)是作为一个整体来处理或运算的,这串比特位称为一个计算机字,简称字。字通常分为若干个字节(每个字节一般是8位)。
字长(Word Length):即字的长度,是指计算机的每个字所包含的位数。字长决定了CPU一次操作所处理的实际比特位数量的多少。字长由CPU对外数据通路的数据总线宽度决定。
计算机处理数据的速率,显然和它一次能加工的位数以及进行运算的快慢有关。如果一台计算机的字长是另一台计算机的两倍,若两台计算机的速度相同,在相同的时间内,前者能做的工作一般是后者的两倍。因此,字长与计算机的功能和用途有很大的关系,是计算机的一个重要技术指标。
在目前来讲,桌面平台的处理器字长正处于从32位向64位过渡的时期,嵌入式设备基本稳定在32位,而在某些专业领域(如高端显卡),处理器字长早已经达到了64位乃至更多的128位
四、字符集
字符集(Character Set、Charset),字面上的理解就是字符的集合,是一个自然语言文字系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括文字、数字、字母、音节、标点符号、图形符号等。
例如ASCII字符集,定义了128个字符;GB2312定义了7445个字符。而计算机系统中提到的字符集准确地来说,指的是已编号的字符的有序集合(但不一定是连续的)。
常见字符集有ASCII字符集、ISO 8859系列字符集、GB系列字符集(GB2312、GBK、GB18030)、BIG5字符集、Unicode字符集等。
注:图中所示微软在GB2312的基础上扩展制订了GBK(Guo-Biao Kuozhan),然后GBK才成为“国家标准”(也有说GBK不是国家标准,只是“技术规范指导性文件”);但网上也有资料说是先有GBK(由全国信息技术标准化技术委员会1995年12月1日制订),然后微软才在其内部所用的CP936字码表(Code Page 936代码页936,代码页的解释详见后文)中以GBK为基础进行了扩展(即Windows系统的代码页CP936是GBK汉字内码扩展规范的一个实现)。
五、编码
编码(Encode),是信息从一种形式或格式转换为另一种形式或格式的过程,比如用预先规定的方法将字符(文字、数字、符号等)、图像、声音或其它对象转换成规定的电脉冲信号或二进制数字。
六、解码
解码(Decode),为编码的逆过程。
七、字符编码
字符编码(Character Encoding),是把字符集中的字符按一定格式(形式、方式)编码为某指定集合中某一对象(比如由0和1两个数字所组成的位串模式、由0~9十个数字所组成的自然数序列、电脉冲等)的过程,亦即在字符集与指定集合两者之间建立一个对应关系(映射关系)的过程。这是信息处理的一项基础技术。
而在计算机科学中,通常以字符集来表达信息,以计算机为基础的信息处理系统则利用电子元件(硬件)的不同状态的组合来表示、存储和处理信息。
电子元件不同状态(一般是开和关或称为开和闭两种状态)的组合能代表数字系统中的数字(比如开和关代表二进制中的0和1),因此字符编码的过程也就可以理解为将字符转换映射为计算机可以接受的二进制数字的过程,其目的是为了便于字符在计算机中表示、存储、处理和传输(包括在网络中传输)。
常见的例子包括将拉丁字母表编码成摩斯电码和ASCII码。其中,ASCII将字母、数字和其它符号进行编号,并且在计算机中直接用7比特的二进制数字来表示这个编号。通常会额外地在最高位(即首位)再增加一个扩充的比特位“0”,以便于计算机系统刚好以1个字节(8比特位)的方式来进行处理、存储和传输。
八、字符编码模型
字符编码模型(Character Encoding Model),是反映字符编码系统的结构特点和各构成部分相互关系的模型框架。
由于历史的原因,早期一般认为字符集和字符编码是同义词,并不需要进行严格区分。因此在像ASCII这样的简单字符集为代表的传统字符编码模型中,这两个概念的含义几乎是等同的。
因为在传统字符编码模型中,基本上都是将字符集里的字符进行编号(字符编号转化为二进制数后一般不超过一个字节),然后该字符编号就是字符的编码。
但是,由统一码(Unicode)和通用字符集(UCS)为代表的现代字符编码模型则没有直接采用ASCII这样的简单字符集的编码思路,而是采用了一个全新的编码思路。
这个全新的编码思路将字符集与字符编码的概念更为细致地分解为了以下几个方面:
1)有哪些字符;
2)这些字符的编号是什么;
3)这些编号如何编码成一系列逻辑层面有限大小的数字,即码元序列;
4)这些逻辑层面的码元序列如何转换为(映射为)物理层面的字节流(字节序列);
5)在某些特殊的传输环境中(比如Email),再进一步将字节序列进行适应性编码处理。
这几个方面作为一个整体,于是构成了现代字符编码模型。
现代字符编码模型之所以要分解为这么几个方面,其核心思想是创建一个能够用不同方式来编码的通用字符集。注意这里的关键词:“不同方式”与“通用”。
这意味着,同一个字符集,可以通用于不同的编码方式;也就是说,可以采用不同的编码方式来对同一个字符集进行编码。字符集与编码方式之间的关系可以是一对多的关系。
更进一步而言,在传统字符编码模型中,字符编码方式与字符集是紧密结合在一起的;而在现代字符编码模型中,字符编码方式与字符集脱钩了。用软件工程的专业术语来说,就是将之前紧密耦合在一起的字符编码方式与字符集解耦了。
因此,为了正确地表示这个现代字符编码模型,需要采用更多比“字符集”和“字符编码”更为精确的概念术语来描述。
在Unicode Technical Report (UTR统一码技术报告)#17《UNICODE CHARACTER ENCODING MODEL》中,现代字符编码模型分为了5个层次,并引入了更多的概念术语来描述(下面所涉及到的一些全新的概念术语,这里只做简介,暂时不作解释,但后文会陆续进行详细解释):
第1层抽象字符表ACR(Abstract Character Repertoire抽象字符清单):明确字符的范围(即确定支持哪些字符)
第2层编号字符集CCS(Coded Character Set):用数字编号表示字符(即用数字给抽象字符表ACR中的字符进行编号)
第3层字符编码方式CEF(Character Encoding Form字符编码形式、字符编码格式、字符编码规则):将字符编号编码为逻辑上的码元序列(即逻辑字符编码)
第4层字符编码模式CES(Character Encoding Scheme):将逻辑上的码元序列映射为物理上的字节序列(即物理字符编码)
第5层传输编码语法TES(Transfer Encoding Syntax):将字节序列作进一步的适应性编码处理
后面将分层予以介绍。
关键术语解释(下)
一、第1层 抽象字符表ACR (Abstract Character Repertoire抽象字符清单):明确字符的范围(即确定支持哪些字符)
抽象字符表ACR是一个编码系统支持的所有抽象字符的集合,可以简单理解为无序的字符集合,用于确定字符的范围,即要支持哪些字符。
抽象字符表ACR的一个重要特点是字符的无序性,即其中的字符并没有编排数字顺序,当然也就没有数字编号。
“抽象”字符不具有某种特定的字形,不应与具有某种特定字形的“具体”字符混淆。
字符表可以是封闭的(即字符范围是固定的),即除非创建一个新的标准,否则不允许添加新的字符,比如ASCII字符表和ISO/IEC 8859系列都是这样的例子;字符表也可以是开放的(即字符范围是不固定的),即允许不断添加新的字符,比如Unicode字符表和一定程度上Code Page代码页(代码页后文会有详细解释)是这方面的例子。
二、第2层 编号字符集CCS(Coded Character Set):用数字编号表示字符(即用数字给字符编号)
【注:一般将“Coded Character Set”翻译为“编码字符集”或“已编码字符集”,但这里的“编码”二字容易导致与后文的“编码方式”及“编码模式”中的“编码”二字混淆,带来理解上的困扰,因此觉得翻译为“编号字符集”为宜。】
前面讲了,抽象字符表里的字符是没有编排顺序的,但无序的抽象字符表只能判断某个字符是否属于某个字符表,却无法方便地引用、指称该字符表中的某个特定字符。
为了更方便地引用、指称字符表中的字符,就必须为抽象字符表中的每个字符进行编号。
所谓字符编号,就是将抽象字符表ACR中的中每个抽象字符(简称字符)表示为1个非负整数N或者映射到1个坐标(非负整数值对x, y),也就是将抽象字符的集合映射到一个非负整数或非负整数值对的集合,映射的结果就是编号字符集CCS。因此,字符的编号也就是字符的非负整数代码。
例如,在一个给定的抽象字符表中,表示大写拉丁字母“A”的字符被赋予非负整数65、字符“B”是66,如此继续下去。
由此产生了编号空间(Code Space,一般翻译为代码空间、码空间、码点空间)的概念:根据抽象字符表中抽象字符的数目,可以设定一个字符编号的上限值(该上限值往往设定为大于抽象字符表中的字符总数),从0到该上限值之间的非负整数范围就称之为编号空间。编号空间的描述:
1)可以用一对非负整数来描述,例如:GB2312的汉字编号空间是94 x 94;
2)也可以用一个非负整数来描述,例如:ISO-8859-1的编号空间是256;也可以用字符的存储单元尺寸来描述,例如:ISO-8859-1是一个8比特的编号空间(2^8=256);
3)还可以用其子集来描述,如行、列、面(Plane平面、层面)等等。
编号空间中的一个位置(Position)称为码点(Code Point代码点)或码位(Code Position代码位)。一个字符占用的码点所在的坐标(非负整数值对)或所代表的非负整数,就是该字符的编号,又称之为码点值(即码点编号)。
不过,严格来讲,字符编号并不完全等同于码点编号(即码点值)。因为事实上,由于某些特殊的原因,编号字符集CSS里的码点数量要大于抽象字符表ACR中的字符数量。
在编号字符集中,除了字符码点之外,还存在着非字符码点和保留码点,所以字符编号不如码点编号准确;但若对字符码点的码点编号称之为字符编号,倒也更为直接。
在Unicode编码方案中,字符码点又称之为Unicode标量值(Unicode Scalar Value,感觉不如字符码点更为直观),非字符码点和保留码点详见后文介绍。
注意,经常直接以“码点”指代“码点值”——当说到“码点”的时候,有可能是在说编号空间(即码点空间)中的一个位置,即码点;也有可能实际是在说某个码点的码点值,即码点编号。具体根据上下文而定。
这种类似的指代非常普遍,又比如“字符集”、“字符编号”和“字符编码”这三个概念就经常相互指代。以“码点”指代“码点值”,根据上下文,倒也还不难理解;但“字符集”、“字符编号”和“字符编码”三者也经常相互指代,虽然有其历史原因,但目前的实际情况所导致的结果却是使人迷惑、让人抓狂!
因此,所谓编号字符集,可简单理解为就是把抽象字符进行逐个编号或者说逐个映射为码点值(即码点编号)后所得到的结果。编号字符集CSS常简称为字符集。
注意不要将编号字符集CSS和抽象字符表ACR相混淆。多个不同的编号字符集CCS可以表示同一个抽象字符表ACR。
在Unicode标准中,一个单个的抽象字符,既有可能与多个码点对应(为了与其它标准兼容,比如码点编号为U+51C9与U+F979的这两个码点实际上是同一个字符“凉”,这是为了兼容韩国字符集标准KS X 1001:1998,具体可参看Unicode的官方文档),也有可能使用一个由多个码点所组成的码点序列表示(为了表示由基本字符与组合字符组合在一起所组成的字符,比如à,由码点编号为U+0061的基本字符字母“a”和码点编号为U+0300的组合字符读音符号“̀”组成);同时,也并非每一个码点都对应于一个字符,也存在着非字符码点或保留码点。详见后文解释。
特别注意:虽然“编号”与后文某种字符编码方式CEF中的“编码”(即码元序列,解释详见后文)以及某种字符编码模式CES中的“编码”(即字节序列,解释详见后文)存在着对应的关系,但“编号”与“编码”是截然不同的两个概念。
对字符编号的过程——即确定字符码点值的过程,跟计算机还没有直接关系,可认为是一个纯数学的问题,因为只是将字符与编号(即码点值、码点编号)对应起来;根本还没涉及编码算法的问题——即根据指定的字符编码方式CEF对编号进行编码以形成码元序列,以及根据指定的字符编码模式CES对码元序列进一步编码以形成字节序列。但这两个概念经常被混用,实实在在是让人困惑的源头,这是很令人无奈与遗憾的现实。
三、第3层 字符编码方式CEF(Character Encoding Form字符编码形式、字符编码格式、字符编码规则):将字符编号(即码点值)编码为码元序列(即字符编码)
在讲抽象字符表ACR时说过,不同于传统的封闭的ASCII字符表,Unicode字符表是一个现代的开放的字符表,未来可能有更多的字符加入进来(比如很多Emoji表情符就被源源不断地加入进来)。因此,Unicode编号字符集所需要的码点数量,必然是会不断增加的,相对来说是无限的。
但计算机所能表示的整数范围却是相对有限的。比如,一个无符号单字节整型数(unsigned char, uint8)能够表示的编号只有0~0xFF,共256个;一个无符号双字节短整型数(unsigned short, uint16)的能够表示的编号只有0~0xFFFF,共65536个;而一个无符号四字节长整型数(unsigned long, uint32)能表示的码位只有0~0xFFFFFFFF,共4294967296个。
那么问题来了:
1)一方面,怎么通过相对有限的整型数来高可扩展地、高可适应地应对未来相对无限增长的字符数量呢?是用多个单字节整型数来间接表示,还是用一个足够大的多字节整型数来直接表示?
2)另一方面,ASCII字符编码作为最早出现、已被广泛应用的编码方案,完全不兼容显然不明智,那么是直接兼容呢,还是间接兼容呢?
这两方面的问题需要一个综合解决方案,这就是字符编码方式CEF(Character Encoding Form)。
字符编码方式CEF,是将编号字符集里字符的码点值(即码点编号、字符编号)转换成或者说编码成有限比特长度的编码值(即字符编码)。该编码值实际上是码元(Code Unit代码单元、编码单元)的序列(Code Unit Sequence)。
那什么是码元呢?为什么要引入码元这个概念呢?字符集里的字符编号又是如何转换为计算机中的字符编码(即码元序列)的呢?别急,这里先记下这个概念,暂不深究,后文有详细解释。
字符编码方式CEF也被称为“存储格式”(Storage Format)。不过,将CEF称之为存储格式实际上并不合理,因为CEF还只是逻辑层面上的、与特定的计算机系统平台无关的编码方式,尚未涉及到物理层面上的、与特定的计算机系统平台相关的存储方式(第4层才涉及到)。
在ASCII这样传统的、简单的字符编码系统中,字符编号就是字符编码,字符编号与字符编码之间是一个直接映射关系。
而在Unicode这样现代的、复杂的字符编码系统中,字符编号不一定等于字符编码,字符编号与字符编码之间不一定是一个直接映射关系,比如UTF-8、UTF-16为间接映射,而UTF-32则为直接映射。
UTF-8、UTF-16与UTF-32等就是Unicode字符集(即编号字符集)常用的字符编码方式CEF。(UTF-8、UTF-16与UTF-32后文各有详细介绍)
很多文章中,将Unicode与各UTF(Unicode/UCS Transformation Format,包括UTF-8、UTF-16与UTF-32等)之间的关系表述为:Unicode为标准规范、各UTF为编码实现。
不严格地来讲,也可以勉强这么认为。不过,这种表述毕竟不够严谨,而且无助于理解Unicode标准(即Unicode编码方案、Unicode编码系统)、Unicode字符集与各UTF字符编码方式之间更为深入细致的关系。
注:从现代字符编码模型的角度来看,Unicode标准、Unicode编码方案、Unicode编码系统基本上为同义词,是包括了抽象字符表ACR、编号字符集CCS、字符编码方式CEF以及下面要讲到的字符编码模式CES甚至传输编码语法TES在内的一整套标准方案系统,并不特指某一个层面。
四、第4层 字符编码模式CES(Character Encoding Scheme):将码元序列映射为字节序列
【注:一般将“Character Encoding Scheme”翻译为“字符编码方案”,但习惯上通常将某个字符编码系统称之为“字符编码方案”,为避免引起理解上的困扰,应译作“字符编码模式”为宜。】
字符编码模式CES,也称作“序列化格式”(Serialization Format),指的是将字符编号进行编码之后的码元序列映射为字节序列(即字节流),以便编码后的字符在计算机中进行处理、存储和传输。
如果说将编号字符集的码点值(即字符编号)映射(编码)为码元序列的过程属于跟特定的计算机系统平台无关的逻辑意义上的编码,那么将码元序列映射(编码)为字节序列的过程就属于跟特定的计算机系统平台相关的物理意义上的编码。
由于硬件平台与操作系统设计上的历史原因,对于UTF-16、UTF-32等采用多字节码元的编码方式而言,必须使用一个原先称之为零宽度不中断空格(ZERO WIDTH NO-BREAK SPACE)的字符(Unicode字符编号为U+FEFF)来指定字节序(Byte-Order字节顺序、位元组顺序,或称为Endianness端序)是大端序还是小端序,计算机才能够正确地进行处理、存储和传输。(什么是字节序以及其大端序、小端序,解释详见后文)
不过,对于UTF-8这种采用单字节码元的编码方式来说,并不存在字节序问题,不需要指明字节序。因此,在各种计算机系统平台中,UTF-8编码的码元序列与字节序列都是相同的。(为什么UTF-8不存在字节序问题,解释详见后文)
注意,由于字符编码方式CEF与字符编码模式CES中都有“编码”二字,因此,通常所说的动词编码(Encode)有可能指的是通过字符编码方式CEF将编号字符集CCS中的字符编号转变为码元序列;也有可能指的是通过字符编码模式CES将字符编码方式CEF中的码元序列转变为字节序列。
动词解码(Decode)则反过来,当然也同样存在两种可能。
而通常所说的名词编码(Coding、Encoding)也就相应地有可能指的是码元序列,也有可能指的是字节序列。因此,必须根据上下文语境来具体理解。
对于程序员而言,通过字符编码方式CEF编码后所形成的码元序列,更多的是一种逻辑意义上的中间编码(即中介编码,属于从字符编号到字节序列的中间状态,作为将字符编号转换为字节序列的中介而存在),不是平时直接“打交道”的对象。
而通过字符编码模式CES将码元序列进一步编码后所形成的字节序列,才是平时直接“打交道”最多的物理意义上的最终编码(虽然事实上还有第5层的传输编码语法TES所形成的编码,但这种编码毕竟仅用于某些特殊的传输环境,平时“打交道”的机会较少)。
五、第5层 传输编码语法TES(Ttransfer Encoding Syntax):将字节序列作进一步的适应性编码处理
由于历史的原因,在某些特殊的传输环境中,需要对上一层次的字符编码模式CES所提供的字节序列(字节流)作进一步的适应性编码处理。一般包括两种:
1)一种是把字节序列映射到一套更受限制的值域内,以满足传输环境的限制,例如用于Email传输的Base64编码或者quoted-printable编码(可打印字符引用编码),都是把8位的字节映射为7位长的数据(Email协议设计为仅能传输7位的ASCII字符);
2)另一种是压缩字节序列的值,如LZW或者进程长度编码等无损压缩技术。
(这一部分内容本文不深入阐述,有兴趣者可自行查找资料。)
六、总结一下现代字符编码模型:
对于Unicode这样的现代字符编码系统来说,同一个字符因多个不同的字符编码方式CEF(如UTF-8、UTF-16、UTF-32等)而具有多个不同的码元序列(Code Unit Sequence),同一个码元序列因两个不同的字符编码模式CES(大端序、小端序)而又可能具有两个不同的字节序列(Byte Sequence)。
但这些不同的码元序列也好,字节序列也好,只要表示的是同一个字符,所对应的码点值(即码点编号、字符编号)一般都是相同的(在Unicode标准中,为了与其它标准兼容,有少数字符可能与多个码点对应)。
好了,关键术语先解释到这里,其他术语将在后文中陆续解释。
字符编码的由来
一、为什么需要对字符进行编码
计算机一开始发明出来时是用来解决数字计算问题的,后来人们发现,计算机还可以做更多的事,例如文本处理。
但计算机其实挺笨的,它只“认识”010110111000…这样由0和1两个数字组成的二进制数字,这是因为计算机的底层硬件实现就是用电路的开和闭两种状态来表示0和1两个数字的。因此,计算机只可以直接存储和处理二进制数字。
为了在计算机上也能表示、存储和处理像文字、符号等等之类的字符,就必须将这些字符转换成二进制数字。
当然,肯定不是我们想怎么转换就怎么转换,否则就会造成同一段二进制数字在不同计算机上显示出来的字符不一样的情况,因此必须得定一个统一的、标准的转换规则。
二、EBCDIC码与ASCII码
于是最开始出现了EBCDIC(Extended Binary Coded Decimal Interchange Code扩展二进制编码的十进制交换码)编码标准。EBCDIC码是由国际商用机器公司(IBM)为大型机操作系统而开发设计的,于1964年推出。
在EBCDIC码中,英文字母不是连续排列的,中间出现多次断续,这带来了一些困扰和麻烦。
因此,在后来IBM的个人计算机和工作站操作系统中并没有采用EBCDIC码,而是采用了晚于EBCDIC码推出、且后来成为了英文字符编码工业标准的ASCII编码方案。
EBCDIC编码表
ASCII码(American Standard Code for Information Interchange美国信息交换标准码),由美国国家标准学会ANSI(American National Standard Institute)于1968年正式制定。后又于1972年被ISO/IEC采用,制定为ISO/IEC 646标准(ISO,即国际标准化组织International Standardization Organization,成立于1946年;IEC,即国际电工技术委员会International Electrotechnical Commission,成立于1906年;ISO/IEC往往用来表示由这两大国际组织联合制定的标准)。
由于ASCII码要晚于EBCDIC码出现(网上也有文章说是ASCII码要早于EBCDIC码开始设计,但1968年ASCII码才正式确定为标准),ASCII码的编码方式参照了EBCDIC码,并吸取了其经验教训,将英文字母进行了连续排列,这方便了程序处理。
ASCII编码方案虽然不是最早出现的字符编码方案,但却是最基础、最重要、应用最广泛的字符编码方案。
目前所通行的其他字符编码方案,比如ISO-8859、GB系列(GB2312、GBK、GB18030、GB13000)、Big5、Unicode等等,均直接或间接兼容ASCII码。
而像EBCDIC这样与ASCII完全不兼容的编码方案,基本上处于已淘汰或将要淘汰的境地。
三、ASCII字符编码方案介绍
ASCII码使用七个二进制数字(bit比特、位)来表示一个字符,总共表示128个字符(2^7 = 128,二进制编码为00000000 ~ 0111 1111,对应的十进制就是0~127)。
由于个人计算机普遍采用8位一个字节来进行存取与处理,因此剩下最高位的那1比特一般为0,但有时也被用作一些通讯系统的奇偶校验位。
ASCII编码表
ASCII字符集共计有128个字符(见上表),码点编号(即字符编号)从0到127(二进制为从0000 0000到0111 1111,十六进制为从0x00到0x7F),二进制最高位都是0。其中:
1)0~31:控制字符或通讯专用字符(不可显示不可打印字符),如0x07(BEL响铃)会让计算机发出哔的一声、0x00(NUL空,注意不是空格)通常用于指示字符串的结束、0x0D(CR回车)和0x0A(LF换行)用于指示打印机的打印针头退到行首(即回车)并移到下一行(即换行)等。
2)32~126:可显示可打印字符(其中32为可显示但不可打印的空格字符),48~57为0-9的阿拉伯数字,65~90为26个大写英文字母,97~122为26个小写英文字母,其余的是一些标点符号、运算符号等。
3)127:控制字符DEL。
这时候的字符编解码非常简单,比如若要将字符序列编码为二进制流写入存储设备,只需要将该字符序列里的各个字符在ASCII字符集中的字符编号(即码点编号),直接以一个二进制字节写入存储设备即可,字符编号就是字符编码,中间不需要经过特别的编码算法进行字符编号到字符编码的转换计算,更不存在所谓码元序列到字节序列的转换。
EASCII及ISO 8859字符编码方案
计算机出现之后,首先逐渐从美国发展到了欧洲。由于欧洲很多国家所用到的字符中,除了基本的、美国也用的那128个ASCII字符之外,还有很多衍生的拉丁字母等字符。比如,在法语中,字母上方有注音符号;而欧洲其他国家也有各自特有的字符。
考虑到一个字节能够表示的编码实际上有256个(2^8 = 256),而ASCII字符却只用到了一个字节中的低7位(因此在ASCII码中最高位总是为0),编号为0x00~0x7F(十进制为0~127)。也就是说,ASCII只使用了一个字节所能表示的256个编码中的前128个(2^7 = 128)编码,而后128个编码相当于被闲置了。因此,欧洲各国纷纷打起了后面这128个编码的主意。
可问题在于,欧洲各国同时都有这样的想法。于是各国针对后面的0x80~0xFF(十进制为128~255)这128个编码分别对应什么样的字符,就有了各自不同的设计。
为了结束欧洲各国这种各自为政的混乱局面,于是又先后设计了两套统一的,既兼容ASCII码,又支持欧洲各国所使用的那些衍生字符的单字节编码方案:一个是EASCII(Extended ASCII)字符编码方案,另一个是ISO/IEC 8859字符编码方案。
(笨笨阿林原创文章,转载请注明出处)
先来说EASCII码。EASCII码同样也是将ASCII中闲置的最高位(即首位)用来编码新的字符(这些ASCII字符之外的新字符,其最高位总是为1)。换言之,也就是将一个字节中的全部8个比特位用来表示一个字符。比如,法语中的é的编码为130(二进制1000 0010)。
显然,EASCII码虽与ASCII码一样使用单字节编码,但却可以表示最多256个字符(2^8 = 256),比ASCII的128个字符(2^7=128)多了一倍。
因此,在EASCII码中,当第一个比特位(即字节的最高位)为0时,仍表示之前那些常用的ASCII字符(实际的二进制编码为0000 0000 ~ 0111 1111,对应的十进制就是0~127),而为1时就表示补充扩展的其他衍生字符(实际的二进制编码为1000 0000 ~ 1111 1111,对应的十进制就是128~255)。
这样就在ASCII码的基础上,既保证了对ASCII码的兼容性,又补充扩展了新的字符,于是就称之为Extended ASCII(扩展ASCII)码,简称EASCII码。
EASCII码比ASCII码扩充出来的符号包括表格符号、计算符号、希腊字母和特殊的拉丁符号,如下表所示。
扩展ASCII(EASCII)编码表
不过,EASCII码目前已经很少使用,常用的是ISO/IEC 8859字符编码方案。该方案与EASCII码类似,也同样是在ASCII码的基础上,利用了ASCII的7位编码所没有用到的最高位(首位),将编码范围从原先ASCII码的0x00~0x7F(十进制为0~127),扩展到了0x80~0xFF(十进制为128~255)。
ISO/IEC 8859字符编码方案所扩展的这128个编码中,实际上只有0xA0~0xFF(十进制为160~255)被实际使用。也就是说,只有0xA0~0xFF(十进制为160~255)这96个编码定义了字符,而0x80~0x9F (十进制为128~159)这32个编码并未定义字符。
显然,ISO/IEC 8859字符编码方案同样是单字节编码方案,也同样完全兼容ASCII。
注意,与ASCII、EASCII属于单个独立的字符集不同,ISO/IEC 8859是一组字符集的总称,其下共包含了15个字符集,即ISO/IEC 8859-n,其中n=1,2,3,...,15,16(其中12未定义,所以共15个)。
这15个字符集大致上包括了欧洲各国所使用到的字符(甚至还包括一些外来语字符),而且每一个字符集的补充扩展部分(即除了兼容ASCII字符之外的部分)都只实际使用了0xA0~0xFF(十进制为160~255)这96个编码。
其中,ISO/IEC 8859-1收录了西欧常用字符(包括德法两国的字母),目前使用得最为普遍。ISO/IEC 8859-1往往简称为ISO 8859-1,而且还有一个称之为Latin-1(也写作Latin1)的别名。
其余从ISO 8859-2到ISO 8859-16各自所收录的字符如下:
ISO 8859-2字符集,也称为Latin-2,收录了东欧字符;
ISO 8859-3字符集,也称为Latin-3,收录了南欧字符;
ISO 8859-4字符集,也称为Latin-4,收录了北欧字符;
ISO 8859-5字符集,也称为Cyrillic,收录了斯拉夫语系字符;
ISO 8859-6字符集,也称为Arabic,收录了阿拉伯语系字符;
ISO 8859-7字符集,也称为Greek,收录了希腊字符;
ISO 8859-8字符集,也称为Hebrew,收录了西伯莱(犹太人)字符;
ISO 8859-9字符集,也称为Latin-5或Turkish,收录了土耳其字符;
ISO 8859-10字符集,也称为Latin-6或Nordic,收录了北欧(主要指斯堪地那维亚半岛)的字符;
ISO 8859-11字符集,也称为Thai,从泰国的TIS620标准字符集演化而来;
ISO 8859-12字符集,目前尚未定义(未定义的原因目前有两种说法:一是原本要设计成一个包含塞尔特语族字符集的“Latin-7”,但后来塞尔特语族变成了ISO 8859-14 / Latin-8;二是原本预留给印度天城体梵文的,但后来却搁置了);
ISO 8859-13字符集,也称为Latin-7,主要函盖波罗的海(Baltic)诸国的文字符号,也补充了一些被Latin-6遗漏的拉脱维亚(Latvian)字符;
ISO 8859-14字符集,也称为Latin-8,它将Latin-1中的某些符号换成塞尔特语(Celtic)的字符;
ISO 8859-15字符集,也称为Latin-9,或者被戏称为Latin-0,它将Latin-1中较少用到的符号删除,换成当初遗漏的法文和芬兰字母,还把英镑和日元之间的金钱符号,换成了欧盟货币符号;
ISO 8859-16字符集,也称为Latin-10,涵盖了阿尔巴尼亚语、克罗地亚语、匈牙利语、意大利语、波兰语、罗马尼亚语及斯洛文尼亚语等东南欧国家语言。
简体汉字编码方案(GB2312、GBK、GB18030、GB13000)以及全角、半角、CJK
一、概述
英文字母再加一些其他标点字符之类的也不会超过256个,用一个字节来表示一个字符就足够了(2^8 = 256)。但其他一些文字不止这么多字符,比如中文中的汉字就多达10多万个,一个字节只能表示256个字符,肯定是不够的,因此只能使用多个字节来表示一个字符。
于是当计算机被引入到中国后,相关部门设计了GB系列编码(“GB”为“国标”的汉语拼音首字母缩写,即“国家标准”之意)。
按照GB系列编码,在一段文本中,如果一个字节是0~127,那么这个字节的含义同ASCII编码,否则,这个字节和下一个字节共同组成汉字(或是GB编码定义的其他字符)。
因此,GB系列编码向下兼容ASCII,也就是说,如果一段用GB编码文本里的所有字符都在ASCII中有定义(即该文本全部由ASCII字符组成),那么这段编码和ASCII编码完全一样。
GB编码早期收录的汉字不足一万个,*本能满足日常使用需求,但不包含一些生僻字,因此后来又进行了扩展。
最早的GB编码是GB2312,后来有了在GB2312*础上扩展的GBK,最新的是GB18030,加入了一些国内少数民族的文字,一些生僻字被编到了4个字节,每扩展一次都完全保留之前版本的编码,所以每个新版本都向下兼容。
这里要指出的是,虽然都用多个字节表示一个字符,但是GB类的汉字编码与后文的Unicode编码方案UTF-8、UTF-16、UTF-32是毫无关系的(其中UTF-8对于ASCII字符仍用一个字节编码,而非ASCII字符则为多字节编码)。
不过,也正因为不得不使用多个字节来表示一个字符,相较于只使用单个字节的ASCII编码方案,GB类编码方案与后面要介绍的Unicode编码方案一样,无疑到导致了更高的复杂度。比如,当多字节字符与原先的ASCII字符混用时:
1)要么将原先的ASCII字符重新编码为多个字节表示,以便与其他多字节字符统一起来(UTF-16、UTF-32等采用的是这种方法);
2)要么保持ASCII字符为单个字节编码不变,但将其他多字节字符编码中的各个字节的最高位(首位)设为1,以避免与字节最高位为0的ASCII编码相冲突(GB、UTF-8等采用的是这种方法)。
前者具有更高的空间复杂度,因为原先只需要单个字节表示的ASCII字符,现在也必须用多个字节来表示,显然更为耗费存储空间;后者则具有更高的时间复杂度,因为为了避免冲突以及其他种种考虑(比如扩展性、容错性等),使用了更为复杂的编码算法(encoding algorithm),无疑更为耗费计算时间。
而且,无论是前者还是后者,若多字节编码中采用的又是多字节码元(Code Unit)的话(如UTF-16、UTF-32编码采用的就是多字节码元,而UTF-8中的非ASCII字符虽然也是多字节编码,但采用的却是单字节码元),由于历史的原因,又进一步引发了更为麻烦的字节序(Byte-Order)问题。(编码算法、码元、字节序的相关介绍,详见后文解释)
二、GB2312
GB2312编码方案,即《信息交换用汉字编码字符集——*本集》,是由中国国家标准总局于1980年发布、1981年5月1日开始实施的一套国家标准,标准号为GB2312-1980。
GB2312编码适用于汉字处理、汉字通信等系统之间的信息交换,通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB2312。
GB2312编码为了避免与ASCII字符编码(0~127)相冲突,规定表示一个汉字的编码(即汉字内码)的字节其值必须大于127(即字节的最高位为1),并且必须是两个大于127的字节连在一起来共同表示一个汉字(GB2312为双字节编码),前一字节称为高字节,后一字节称为低字节;而一个字节的值若小于127(即字节的最高位为0),自然是仍表示一个原来的ASCII字符(ASCII为单字节编码)。
因此,可以认为GB2312是对ASCII的中文扩展(即GB2312与ASCII相兼容),正如EASCII是对ASCII的欧洲文字扩展一样。
不过,很显然的是,GB2312与EASCII码的128~255这段扩展部分所表示的字符是不同的。也就是说,GB2312与EASCII虽然都兼容ASCII,但GB2312并不兼容EASCII的扩展部分。
事实上,目前世界上除ASCII之外的其它通行的字符编码方案,*本上都兼容ASCII,但相互之间并不兼容。
(笨笨阿林原创文章,转载请注明出处)
GB 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时,除了汉字,GB 2312还收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。
可能是出于显示上视觉美观的考虑,除汉字之外的682个字符中,甚至包括了ASCII里本来就有的数字、标点、字母等字符。也就是说,这些ASCII里原来就有的单字节编码的字符,又再编了两个字节长的GB2312编码版本。这682个字符就是常说的“全角”字符,而这682个字符中所对应的ASCII字符就被称之为“半角”字符。
【附:全角、半角
全角字符是中文显示及双字节中文编码的历史遗留问题。
早期的点阵显示器上由于像素有限,原先ASCII西文字符的显示宽度(比如8像素的宽度)用来显示汉字有些捉襟见肘(实际上早期的针式打印机在打印输出时也存在这个问题),因此就采用了两倍于ASCII字符的显示宽度(比如16像素的宽度)来显示汉字。
这样一来,ASCII西文字符在显示时其宽度为汉字的一半。或许是为了在西文字符与汉字混合排版时,让西文字符能与汉字对齐等视觉美观上的考虑,于是就设计了让西文字母、数字和标点等特殊字符在外观视觉上也占用一个汉字的视觉空间(主要是宽度),并且在内部存储上也同汉字一样使用2个字节进行存储的方案。这些与汉字在显示宽度上一样的字符就被称之为全角字符。
而原来ASCII中的西文字符由于在外观视觉上仅占用半个汉字的视觉空间(主要是宽度),并且在内部存储上使用1个字节进行存储,相对于全角字符,因而被称之为半角字符。
后来,其中的一些全角字符因为比较有用,就得到了广泛应用(比如全角的逗号“,”、问号“?”、感叹号“!”、空格“ ”等,这些字符在输入法中文输入状态下的半角与全角是一样的,英文输入状态下全角跟中文输入状态一样,但半角大约为全角的二分之一宽),专用于中日韩文本,成为了标准的中日韩标点字符。而其它的许多全角字符则逐渐失去了价值(现在很少需要让纯文本的中文和西文字字对齐了),就很少再用了。
现在全球字符编码的事实标准是Unicode字符集及*于此的UTF-8、UTF-16等编码实现方式。Unicode吸纳了许多遗留(legacy)编码,并且为了兼容性而保留了所有字符。因此中文编码方案中的这些全角字符也保留下来了,而国家标准也仍要求字体和软件都支持这些全角字符。
不过,半角和全角字符的关系在UTF-8、UTF-16等中不再是简单的1字节和2字节的关系了。具体参见后文。
——综合了知乎《中文输入法为什么会有全角和半角的区别?》下多位答主的回答,有多处修改】
GB2312编码表的开始部分
三、GB13000
为了便于多个文种的同时处理,国际标准化组织下属编码字符集工作组制定了新的编码字符集标准——ISO/IEC 10646(与统一联盟制定的Unicode标准兼容,两者的关系详见后文)。
该标准第一次颁布是在1993年,当时只颁布了其第一部分,即ISO/IEC 10646.1:1993,收录中国大陆、台湾、日本及韩国通用字符集的汉字,总共20,902个。制定这个标准的目的是对世界上的所有字符统一编码,以实现世界上所有字符在计算机上的统一处理。
中国相应的国家标准是GB13000.1-1993《信息技术通用多八位编码字符集(UCS)第一部分:体系结构与*本多文种平面》。
2010年又发布了替代标准——GB13000-2010《信息技术通用多八位编码字符集(UCS)》,此标准等同于国际标准ISO/IEC 10646:2003《信息技术通用多八位编码字符集(UCS)》。
GB13000与国际标准ISO/IEC 10646及Unicode标准目前在*本平面(即BMP,详见后文)上保持一致。
四、GBK
GB2312-1980共收录6763个汉字,覆盖了中国大陆99.75%的使用频率,*本满足了汉字的计算机处理需要。但对于人名、古汉语等方面出现的罕用字、生僻字,GB2312不能处理,如部分在GB2312-1980推出以后才简化的汉字(如“啰”)、部分人名用字(如前总理***的“*”字)、台湾及香港使用的繁体字、日语及朝鲜语汉字等,并未收录在内。
于是利用GB2312-1980未使用的码点空间,收录GB13000.1-1993的全部字符,于1995年又发布了《汉字内码扩展规范(GBK)》(Guo-Biao Kuozhan国家标准扩展码,是根据GB13000.1-1993,对GB2312-1980的扩展;英文全称Chinese Internal Code Specification)。
不过,虽然GBK收录了GB13000.1-1993的全部字符,但编码方式并不相同。
GBK跟GB2312一样是双字节编码,然而,GBK只要求第一个字节即高字节是大于127就固定表示这是一个汉字的开始(0~127当然表示的还是ASCII字符),不再要求第二个字节即低字节也必须是127号之后的编码。这样,作为同样是双字节编码的GBK才可以收录比GB2312更多字符。
GBK字符集向后完全兼容GB2312,还支持GB2312-1980不支持的部分中文简体、中文繁体、日文假名,还包括希腊字母以及俄语字母等字母(不过这个编码不支持韩国文字,也是其在实际使用中与Unicode编码相比欠缺的部分),共收录汉字21003个、符号883个,并提供1894个造字码位,简、繁体字融于一体。
GBK的编码框架(Code Scheme):其中GBK1收录除GB2312符号外的增补符号,GBK2收录GB2312汉字,GBK3收录CJK汉字,GBK4收录CJK汉字和增补汉字,GBK5为非中文字符集,UDC为用户自定义字符区
微软早在Windows 95简体中文版中就采用了GBK编码,也就是对微软内部之前的CP936字码表(Code Page 936)进行了扩展(之前CP936和GB2312-1980一模一样)。
微软的CP936通常被视为等同于GBK,连IANA(Internet Assigned Numbers Authority互联网号码分配局)也以“CP936”为“GBK”之别名。
但事实上比较起来,GBK定义之字符较CP936多出95个(15个非汉字及80个汉字),皆为当时未收入ISO 10646 / Unicode的符号。
【注:有说是微软在GB2312的*础上扩展制订了GBK,然后GBK才成为“国家标准”(也有说GBK不是国家标准,只是“技术规范指导性文件”);但网上也有资料说是先有GBK(由全国信息技术标准化技术委员会于1995年12月1日制定),然后微软才在其内部所用的CP936代码页中以GBK为参考进行了扩展。】
五、GB18030
中国国家质量技术监督局于2000年3月17日推出了GB18030-2000标准,以取代GBK。GB18030-2000除保留全部GBK编码汉字之外,在第二字节再度进行扩展,增加了大约一百个汉字及四位元组编码空间。
GB18030《信息交换用汉字编码字符集*本集的补充》是我国继GB2312-1980和GB13000-1993之后最重要的汉字编码标准,是我国计算机系统必须遵循的*础性标准之一。
2005年,GB18030编码方案又进行了扩充,于是又有了GB18030-2005《信息技术中文编码字符集》。如前所述,GB18030-2000是GBK的取代版本,它的主要特点是在GBK*础上增加了CJK中日韩统一表意文字扩充A的汉字;而GB18030-2005的主要特点是在GB18030-2000*础上又增加了CJK中日韩统一表意文字扩充B的汉字。
微软也为GB18030定义了代码页(Code page):CP54936,但是这个代码页实际上并没有真正使用(在Windows 7的“控制面板”-“区域和语言”-“管理”-“非Unicode程序的语言”中没有提供选项;在Windows cmd命令行中可通过命令chcp 54936更改,之后在cmd可显示中文,但却不支持中文输入)。
各汉字(中文字符)编码方案之间的关系
(注: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)等。
此计划原本只包含中文、日文及韩文中所使用的汉字和仿汉字,统称中日韩(CJK)统一表意文字(Unified Ideographs)。后来,此计划才加入了越南文的喃字,所以又合称为中日韩越(CJKV)统一表意文字。
七、小结
GB类的字符集均属于双字节字符集DBCS(Double Byte Character Set)。
在DBCS系列编码方案里,最大的特点是两字节长的中文字符和一字节长的英文字符(ASCII字符)相兼容,可以并存于同一个文件内。
因此,写程序时为了支持中文处理,必须要注意字符串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了。
使用GB类编码方案时一般都要时刻记住:一个汉字由两个字节组成(即一个汉字占用的空间相当于两个英文字符占用的空间)。
简体汉字编码中区位码、国标码、内码、外码、字形码的区别及关系
GB2312、GBK、GB18030等GB类汉字编码方案的具体实现方式是怎样的?区位码是什么?国标码是什么?内码、外码、字形码又是什么意思?它们是如何转换的,又为什么要这样转换?
下面以GB2312为例来加以说明(由于GBK、GB18030是以GB2312为基础扩展而来,因此编码实现方式与GB2312一样)。
一、区位码
整个GB2312字符集分成94个区,每区有94个位,每个区位上只有一个字符,即每区含有94个汉字或符号,用所在的区和位来对字符进行编码(实际上就是字符编号、码点编号),因此称为区位码(或许叫“区位号”更为恰当)。
换言之,GB2312将包括汉字在内的所有字符编入一个94 * 94的二维表,行就是“区”、列就是“位”,每个字符由区、位唯一定位,其对应的区、位编号合并就是区位码。比如“万”字在45区82位,所以“万”字的区位码是:45 82(注意,GB类汉字编码为双字节编码,因此,45相当于高位字节,82相当于低位字节)。
GB2312字符集中:
1)01~09区(682个):特殊符号、数字、英文字符、制表符等,包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母等在内的682个全角字符;
2)10~15区:空区,留待扩展;
3)16~55区(3755个):常用汉字(也称一级汉字),按拼音排序;
4)56~87区(3008个):非常用汉字(也称二级汉字),按部首/笔画排序;
5)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)(注意,GB类汉字编码为双字节编码)。
因此,必须将“区码”和“位码”分别加上32(十六进制为20H,后缀H表示十六进制),作为国标码。也就是说,国标码相当于将区位码向后偏移了32,以避免与ASCII字符中0~32的不可显示字符和空格字符相冲突。
注意,国标码中是分别将区位码中的“区”和“位”各自加上32(20H)的,因为GB2312是DBCS双字节字符集,国标码属于双字节码,“区”和“位”各作为一个单独的字节。
这样我们可以算出“万”字的国标码十进制为:(45+32,82+32)= (77,114),十六进制为:(4D,72H),二进制为:(0100 1101,0111 0010)。
(笨笨阿林原创文章,转载请注明出处)
三、内码(机内码)
不过国标码还不能直接在计算机上使用,因为这样还是会和早已通用的ASCII码冲突(导致乱码)。
比如,“万”字国标码中的高位字节77与ASCII的“M”冲突,低位字节114与ASCII的“r”冲突。因此,为避免与ASCII码冲突,规定国标码中的每个字节的最高位都从0换成1,即相当于每个字节都再加上128(十六进制为80,即80H;二进制为1000 0000),从而得到国标码的“机内码”表示,简称“内码”。
由于ASCII码只用了一个字节中的低7位,所以,这个首位(最高位)上的“1”就可以作为识别汉字编码的标志,计算机在处理到首位是“1”的编码时就把它理解为汉字,在处理到首位是“0”的编码时就把它理解为ASCII字符。
比如:
77 + 128 = 205(二进制为1100 1101,十六进制为CD)
114+ 128 = 242(二进制为1111 0010,十六进制为F2)
我们可以来检验一下。打开记事本输入“万”字,编码选择为ANSI(Windows记事本中的ANSI编码对于简体汉字而言就是GB类编码,详见后文解释),保存,如下图所示。
然后用二进制编辑器(比如UltraEdit)打开刚才保存的文件,切换到十六进制模式,会看到:CD F2,这就是“万”字的内码,如下图所示。
总结一下:
从区位码(国家标准定义)---> 区码和位码分别+32(即+20H)得到国标码 ---> 再分+128(即+80H)得到机内码(与ACSII码不再冲突)。
因此,区位码的区和位分别+160(即+A0H,32+128=160)可直接得到内码。用十六进制表示就是:
区位码(区码,位码)+ (20H,20H)+ (80H,80H)=区位码(区码,位码)+ (A0H,A0H)= 内码(高字节,低字节)。
四、为什么要加上20H和80H?
区位码、国标码、内码的转换非常简单,但是令人迷惑的是为什么要这么转换?
首先,需要注意到一点,GB2312虽说是对中文编码,但是里面也有对26个英文字母和一些特殊符号的编码,按理说这些和ASCII重合的字符(33~127)应该无需再重新编码,直接沿用ASCII中的不就行了?
原来,当时在制定GB2312时,决定对ASCII中的可打印字符,也就是英文字母、数字和符号部分(33~126,127为不可打印的DEL)重新编入GB2312中,以两个字节表示,称之为全角字符(全角字符在屏幕上的显示宽度为ASCII字符的两倍,后来也因此而将对应的ASCII字符称之为半角字符)。
而对于ASCII中前32个不可显示也不可打印的控制字符(ASCII码为0~31),以及第33个可显示但不可打印的空格字符(ASCII码为32)等共33个不可打印字符的编码则直接沿用,不再重新编码。
因为要保留这33个不可打印字符,就不能直接采用区位码作为计算机直接处理的机内码,需要将区位码向后偏移32以避开冲突(为什么是偏移32,而不是偏移33?因为区位码中的区码和位码都是从1开始计数的,不像ASCII码是从0开始计数的)。
十进制数字32的十六进制表示就是20(为区别于十进制,记作20H),这也就是区位码要加上20H(区码和位码各自加上20H)才能得到国标码的原因。
(笨笨阿林原创文章,转载请注明出处)
很显然,如果直接采用国标码作为计算机直接处理的机内码的话,还将会产生另一个弊端,即用ASCII码编码的英文字符在GB2312编码环境中无法打开,一打开就会乱码。
因为国标码虽然相较于区位码避开了ASCII码中0~32的前33个不可打印字符,但并没有避开ASCII码中的英文字母、数字和符号(33~126,共94个字符,127为不可打印的DEL)等可打印字符。也就是说,国标码并不是完全兼容ASCII码的。
为了解决这个弊端,考虑到ASCII码只使用了一个字节中的低7位,最高位(即首位)为0,于是决定将国标码每个字节的最高位设为1(国标码的两个字节中的最高位都恒为0,即国标码中的每个字节实际上也只用了一个字节中的低7位),这就是GB2312的机内码(即内码),简称GB2312码。
这样一来就彻底区分开了ASCII码和GB2312码。这也是为什么国标码还要加上(80H,80H)才能得到机内码的原因。
看到这里,有人或许又要问了:如果仅仅是为了避免与ASCII编码相冲突,为什么最初不直接将区位码的区码和位码的最高位从0改为1(相当于各自直接加上128),这样不就无需经过国标码多此一举的中间转换了吗?而且还无需后移32,也就不用浪费这部分编码空间。
对此本人也很困惑,在网上搜了很久也没找到答案,因此具体原因不得而知。或许是一开始考虑不周?或许为了未来扩展所需而预留一部分空间?又或许是有其他不得已的原因?有知道的朋友还望能指点迷津。
GB2312区位码、国标码、内码对照表(其中汉字内码B0A1~F7FE,共6763个)
五、外码(输入码、输入法编码)
外码也叫输入码、输入法编码,是用来将汉字输入到计算机中的一组键盘符号,是作为汉字输入用的编码。
英文字母只有26个,可以把所有的字符都放到键盘上,而使用这种办法把所有的汉字都放到键盘上是不可能的。所以汉字系统需要有自己的输入码体系,使汉字与键盘能建立对应关系。
目前常用的外码分为以下几类:
1)数字编码,比如区位码;
2)拼音编码,比如全拼、双拼、自然码等;
3)字形编码,比如五笔、表形码、郑码等。
六、字形码(字型码、字模码、输出码)
字形码,又称为字型码、字模码、输出码,属于点阵代码的一种。
为了将汉字在显示器或打印机上输出,把汉字按图形符号设计成点阵图,就得到了相应的点阵代码(字形码)。
也就是用0、1表示汉字的字形,将汉字放入n行*n列的正方形(点阵)内,该正方形共有n^2个小方格,每个小方格用一位二进制表示,凡是笔划经过的方格值为1,未经过的值为0。
显示一个汉字一般采用16×16点阵或24×24点阵或48×48点阵。已知汉字点阵的大小,可以计算出存储一个汉字所需占用的字节空间。
比如,用16×16点阵表示一个汉字,就是将每个汉字用16行,每行16个点表示,一个点需要1位二进制代码,16个点需用16位二进制代码(即2个字节),共16行,所以需要16行×2字节/行=32字节,即16×16点阵表示一个汉字,字形码需用32字节。
因此,字节数=点阵行数×(点阵列数/8)。
显然,字形码所表示的字符,相对于抽象字符表ACR里的“抽象”字符,可称之为“具体”字符,因为具有了“具体”的外形。
为了将汉字的字形显示输出或打印输出,汉字信息处理系统还需要配有汉字字形库,也称字模库,简称字库,它集中了汉字的字形信息。
字库按输出方式可分为显示字库和打印字库。用于显示输出的字库叫显示字库,工作时需调入内存。用于打印输出的字库叫打印字库,工作时无需调入内存。
字库按存储方式也可分为软字库和硬字库。软字库以文件的形式存放在硬盘上,现多用这种方式。硬字库则将字库固化在一个单独的存储芯片中,再和其它必要的器件组成接口卡,插接在计算机上,通常称为汉卡。这种方式现已淘汰。
七、小结
可以这样理解,为在计算机内表示汉字而采取统一的编码方式所形成的汉字编码叫内码。为方便汉字输入而形成的汉字编码为外码,也叫输入码。为显示输出和打印输出汉字而形成的汉字编码为字形码,也称为字模码、输出码。
计算机通过键盘输入的外码(重码时还需附加选择编号)对应于汉字内码,将汉字外码转换(即映射)为汉字内码,以实现输入汉字的目的;通过汉字内码在字模库(即字库)中找出汉字的字形码,将汉字内码转换(即映射)为汉字字形码,以实现显示输出和打印输出汉字的目的。
事实上,英文字符的输入、处理和显示过程大致上也差不多,只不过英文字符不需要输入码(即外码),直接在键盘上输入对应的英文字母即可。
ANSI编码与代码页(Code Page)
一、ANSI编码
如前所述,在全世界所有国家和民族的文字符号统一编码的Unicode编码方案问世之前,各个国家、民族为了用计算机记录并显示自己的字符,都在ASCII编码方案的基础上,设计了各自的编码方案。
比如欧洲先后设计了EASCII和ISO/IEC 8859系列字符编码方案;为了显示中文及相关字符,中国设计了GB系列编码(“GB”为“国标”的汉语拼音首字母缩写,即“国家标准”之意)。
同样,日文、韩文、世界各国文字都有它们各自的编码。所有这些各个国家和地区所独立制定的既兼容ASCII又互相不兼容的字符编码,微软统称为ANSI编码。
所以,即使知道是ANSI编码,还需要知道这是哪一个国家的才能解码;另外,也无法用同一种ANSI编码表示既有汉字、又有韩文的文本。
严格来说,ANSI的字面意思并非字符编码,而是美国的一个非营利组织——美国国家标准学会(American National StandardsInstitute)的缩写。ANSI这个组织做了很多标准制定工作,包括C语言规范ANSI C,还有各国字符编码对应的“代码页(code page)”标准。(具体什么是代码页,详见后文解释)
ANSI规定简体中文GB编码的代码页是936,所以GB编码又叫做ANSI code page 936(ANSI标准的代码页936),各国编码之所以被微软统称为ANSI编码的原因即在这里。
后来,或许是出于沿用统一的称呼之目的,有些在当时还并未被ANSI定为标准的代码页,也被微软称之为ANSI代码页,比如CP943代码页。
在Windows系统的编码处理中,ANSI编码一般代表系统默认编码方式,而且并不是确定的某一种编码方式——在简体中文操作系统中ANSI编码默认指的是GB系列编码(GB2312、GBK、GB18030);在繁体中文操作系统中ANSI编码默认指的是BIG5;在日文操作系统中ANSI编码默认指的是Shift JIS,等等。可在系统区域设置的系统Locale中更改。
(笨笨阿林原创文章,转载请注明出处)
二、代码页(Code Page)
代码页也称为“内码表”,是与特定语言的字符集相对应的一张表。操作系统中不同的语言和区域设置可能使用不同的代码页。
例如,微软所用的ANSI代码页1252(CP1252)对应于ISO 8859-1字符集(即Latin-1字符集,但CP1252对Latin-1有扩展,其中编码128~159也被定义了字符,这是与Latin-1字符集不同之处),用于英语和大多数欧洲语言(西班牙语和各种日耳曼/斯堪的纳维亚语),而IBM所用的OEM代码页932(CP932)对应于Shift JIS字符集(但CP932对Shift JIS有扩展;另外,对应的微软ANSI代码页为CP943,也对Shift JIS有扩展),用于日本字符。
代码页一般与其所直接对应的字符集之间并非完全等同,往往因为种种原因(比如标准跟不上现实实践的需要)而会对字符集有所扩展。
早期,代码页是IBM称呼计算机的BIOS所支持的字符集编码。当时通用的操作系统都是命令行界面的,这些操作系统直接使用BIOS提供的字符绘制功能来显示字符(或者是一组嵌入在显卡字符生成器中的字形)。这些BIOS代码页也被称为OEM代码页。
随着图形用户界面操作系统的广泛使用(最初被广为接受的图形用户界面操作系统是Windows3.1),操作系统本身具有了字符绘制的功能。微软于是在Windows操作系统没有转向UTF-16(UTF-16的推出要早于现在被广为认可的UTF-8)作为编码实现之前(即Windows2000发布之前),定义了一系列支持不同国家和地区所制定的字符集的代码页,被称作“Windows代码页”或“ANSI代码页”。代表性的是实现了ISO-8859-1(即Latin-1)的代码页1252(即CP1252),以及实现了GBK的代码页936(即CP936)。
代码页可以在从字符映射到单字节值或多字节值的表格中表现。注意,这里的单字节值与多字节值指的是特定于系统平台的物理意义上的字节序列,不是指与系统平台无关的逻辑意义上的码元序列。正因为这样,代码页也被称之内码表。
也就是说,代码页是字符集的具体实现,可以将其理解为一张“字符-字节”映射表,通过查表实现“字符-字节”的翻译。
代码页主要用于字符在计算机中的存储和显示,比如,计算机读取了一个二进制字节,那这个字节到底代表哪个字符,就需要到指定的代码页中查找,这个查找的过程就被称为查表。
代码页的指定在Windows中是系统默认设置的(即默认系统区域设置),也可在(Windows7的)“控制面板-区域和语言-管理-非Unicode程序的语言-更改系统区域设置”中选择列表中的语言进行更改。
注意:系统区域设置System Locale可用于确定在不使用Unicode编码的程序中输入和显示信息的默认字符集和字体,这样就可以让非Unicode程序在计算机上使用指定的语言得以正常运行。因此,在计算机上安装某些非Unicode程序时,可能需要更改默认的系统区域设置。为系统区域设置选择不同的语言并不会影响Windows系统本身或其他使用Unicode编码的程序的菜单和对话框中的语言显示。
(笨笨阿林原创文章,转载请注明出处)
早期在IBM和微软内部使用数字来标记不同的字符集,不同的厂商对同一个字符集使用各自不同的名称。
例如,UTF-8在IBM称作代码页1208,在微软称作代码页65001,在SAP称作代码页4110;Windows使用936代码页(Code Page 936,即CP936)、Mac系统使用EUC-CN代码页实现GBK字符集的编码(EUC:Extended Unix Code;EUC-CN是类Unix系统中GBK编码方案的别名,等同于Windows下的cp936代码页),名字虽然不一样,但对于同一汉字的编码肯定是一样的。
三、微软Windows操作系统中ANSI代码页的设置
微软为了适应世界上不同地区用户的文化背景和生活习惯,在Windows中设计了区域(Locale)设置的功能。
Locale是指特定于某个国家或地区的一组设定,包括代码页,以及数字、货币、时间和日期的格式等。
在Windows内部,其实有两个Locale设置:系统Locale和用户Locale。系统Locale决定代码页,用户Locale决定数字、货币、时间和日期的格式。
可以在Windows控制面板的“区域和语言选项”中设置系统Locale(非Unicode程序的语言)和用户Locale(标准和格式):
(Windows XP中的Locale设置)
(Windows 7中的Locale设置)
系统Locale对应的代码页被作为Windows的默认代码页。在没有明确指定某个文本的编码信息时,Windows将按照指定的默认代码页的编码方案来解释该文本数据。这个默认代码页通常被称作ANSI代码页(ACP)。
在历史上,IBM的个人计算机和微软公司的操作系统曾经是PC的标准配置。微软公司将IBM公司定义的代码页称作OEM代码页,在IBM公司的代码页基础上作了些增补后,称为ANSI代码页。
在Windows XP的“区域和语言选项”高级页面的“代码页转换表”中可看到各个语种的代码页(Windows7中已经不能直接看到了)。例如:
·874 (ANSI/OEM -泰文)
·932 (ANSI/OEM -日文Shift-JIS)
·936 (ANSI/OEM -简体中文GBK)
·949 (ANSI/OEM -韩文)
·950 (ANSI/OEM -繁体中文Big5)
·1250 (ANSI -中欧)
·1251 (ANSI -西里尔文)
·1252 (ANSI -拉丁文)
·1253 (ANSI -希腊文)
·1254 (ANSI -土耳其文)
·1255 (ANSI -希伯来文)
·1256 (ANSI -阿拉伯文)
·1257 (ANSI -波罗的海文)
·1258 (ANSI/OEM -越南)
Unicode编码方案概述
前面讲过,随着计算机发展到世界各地,于是各个国家和地区各自为政,搞出了很多既兼容ASCII但又互相不兼容的各种编码方案。这样一来同一个二进制编码就有可能被解释成不同的字符,导致不同的字符集在交换数据时带来极大的不便。
比如大陆和台湾是只相隔150海里、使用着同一种语言的兄弟地区,也分别采用了不同的DBCS双字节字符集编码方案。
以前大陆地区必须装上类似于“UCDOS希望汉字系统”这样的中文处理系统专门来处理简体汉字的显示、输入问题。
而台湾地区由于采用BIG5编码方案(统一繁体字编码,俗称大五码,使用2个字节表示繁体汉字),则必须安装类似于“ET倚天汉字系统”这样的繁体中文处理系统才可以正确显示、输入繁体汉字。
因此,要想打开一个文本文件,就必须首先知道它所采用的编码方案,否则用错误的编码方案进行解码,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方案不一样。
想象一下,如果有一种统一的编码方案,将世界上所有语言字符都纳入其中,每一个字符都给予一个全球独一无二的编码,那么乱码问题就会消失。于是,全球所有国家和民族使用的所有语言字符的统一编码方案诞生了。
最初,由多语言软件制造商组成了统一码联盟(The Unicode Consortium),然后于1991年发布了The Unicode Standard(统一码标准),定义了一个全球统一的通用字符集,习惯简称为Unicode字符集(Unicode常被称为统一码、万国码、单一码,严格来说,这种称呼不够严谨或不够准确,因为Unicode字符集只是一个编号字符集,尚未经过字符编码方式CEF和字符编码模式CES进行编码)。
接着,ISO及IEC也于1993年联合发布了称之为Universal Multiple-Octet Coded Character Set(通用多八位组编号字符集,习惯翻译为“通用多八位编码字符集”)、简称为UCS(Universal Character Set通用字符集)、标准号为ISO/IEC 10646-1的全球统一的通用字符集。
后来,统一码联盟与ISO/IEC双方都意识到世界上没有必要存在两套全球统一的通用字符集,于是进行整合,并为创立一个单一的全球统一的通用字符集而协同工作。到Unicode 2.0时,Unicode字符集和UCS字符集(ISO/IEC 10646-1)基本保持了一致。
虽然现在两个项目仍都存在,并独立地公布各自的标准,但统一码联盟和ISO/IEC都同意保持两者的通用字符集相互兼容,并共同调整未来的任何扩展。
目前,Unicode的知名度要比UCS知名度大得多,已成了全球统一的通用字符集或编码方案的代名词。并且在实践中,Unicode也要比UCS应用得更为广泛得多。
Unicode字符集的目标是涵盖目前人类使用的所有字符,并为每个字符分配唯一的字符编号(即码点编号、码点值),一一对应于编号空间(Code Space代码空间、码空间、码点空间)里的码点(Code Point代码点)。
Unicode字符集将所有字符按照使用上的频繁度划分为17个平面(Plane层面),每个平面上的编号空间有2^16=65536个码点。
(笨笨阿林原创文章,转载请注明出处)
其中第0个平面BMP(Basic Multilingual Plane基本多语言平面、基本多文种平面、基本平面、平面0),基本涵盖了当今世界上正在使用中的常用字符。我们平常用到的Unicode字符,一般都是位于BMP平面上的。
BMP平面以外其他的增补平面(也称为辅助平面)要么用来表示一些非常特殊的字符(比如不常用的象形文字、远古时期的文字等),且多半只有专家在历史和科学领域里才会用到它们;要么被留作扩展之用。目前Unicode字符集中尚有大量编号空间未被使用。
另外,BMP平面有一个专用区(Private Use Zone):0xE000~0xF8FF(十进制57344~63743),共6400个码点,被保留为专用(私用),因而永远不会被分配给任何字符;还有一个被称为代理区(Surrogate Zone)的特殊区域:0xD800-0xDFFF(十进制55296~57343),共2048个码点,目的是用基本平面BMP中的两个码点“代理”表示BMP以外的其他增补平面的字符(解释详见后文)。
Unicode字符集中的平面与字符映射范围
Unicode字符集的字符编码方式一开始规定用两个字节(即16位)来统一表示所有的字符(即UTF-16编码方式,UTF-16编码方式要早于UTF-8编码方式、UTF-32编码方式出现,详见后文)。
对于ASCII字符,与前面介绍的ANSI编码一样,Unicode也保持其原编码不变(准确地说,应该是保持其“编号不变”,因为在传统字符编码模型中,编号与编码不作区分,说“编码不变”也勉强可以),只是在UTF-16字符编码方式中将其长度由原来的8位扩展为16位(注意,这里的字符编码方式CEF还只是逻辑意义上的码元序列,不是字符编码模式CES——物理意义上的字节序列),而其他文化和语言的字符则全部重新统一编码。
由于ASCII字符只需要用到UTF-16的16位编码中的低8位,所以其高8位永远是0(实际上也只用到了低8位中的低7位,因此准确地说其高9位永远是0)。
在Unicode标准最初推出的UTF-16字符编码方式中,无论是半角的英文字母,还是全角的汉字,它们都表示统一的“一个字符”,同时其编码也都是统一的“两个字节”(也因此UTF-16属于双字节码元编码方式,而Unicode标准在UTF-16字符编码方式之后所推出的UTF-8字符编码方式则属于单字节码元编码方式,两者之间的关系与区别详见后文)。
请注意这里的“字符”和“字节”两个术语意义上的不同:“字节”是一个与计算机相关的物理意义上的8位存贮单元,而“字符”则是一个与文化相关的逻辑意义上的文字符号。
在Unicode标准推出之前,那些做多语言国际软件的公司遇上过很大麻烦。他们为了在不同的国家销售同一套软件,就不得不特别注意字符编码的问题。不仅要处处小心不要搞错,还要把软件中的文字在不同的字符编码中转换来转换去,而Unicode标准的出现,提供了一个很好的一揽子解决方案。
于是从Windows NT开始,微软趁机把操作系统改了一遍,把所有的核心代码都改成了采用Unicode标准的版本(实际使用的就是Unicode标准的UTF-16字符编码方式CEF所对应的UTF-16字符编码模式CES)。
从Windows NT开始,Windows系统终于无需要加装各种本土语言系统(比如“UCDOS希望汉字系统”之类的),就可以直接显示全世界上所有的字符了。当然,为了保持兼容性,对于之前的ANSI编码方案,Windows仍然是必须支持的。
(笨笨阿林原创文章,转载请注明出处)
Unicode在刚开始制订UTF-16字符编码时,并没有考虑与任何一种现有的字符编码保持完全兼容(与ASCII也只能算是间接兼容或者说半兼容,毕竟ASCII字符的UTF-16编码也同样是16位的),比如GBK与Unicode在汉字的编码上完全是不一样的,没有任何一种简单的算术方法可以将文本内容在UTF-16编码和GBK编码之间进行直接转换,要转换的话只能通过查表这样低效率的笨办法一个字符对应一个字符地来进行。
即便是ASCII字符,也属于不完全兼容,因为UTF-16也是用两个字节来表示的,虽然低7位与ASCII保持了一致,其余高位的9位均只是占位的0,但毕竟还是使用了16位共两个字节编码,不同于ASCII码的单字节编码。正是鉴于此(当然除此之外还有其他原因),于是后来又设计了UTF-8字符编码方式,则保持了跟ASCII码的完全兼容。
从字符集的角度上来讲,Unicode字符集不同于ASCII这样不能再增加字符的封闭字符集,而是一个开放的字符集,是可以不断增加字符的。因此Unicode字符集也在不断发展(比如随着互联网即时聊天工具的发展而流行起来的很多Emoji表情符就不断地被增加到了Unicode字符集中),理论上支持的字符数量是没有上限的,未来还可再扩展。
(注意,很多文章中,有时候称字符集,有时候称字符编码方案,大致上来讲,字符集与字符编码方案经常被视为同义词,尤其是在传统字符编码模型中。但若深究起来的话,在现代字符编码模型中,由于字符集实际上为编号字符集的简称,因此字符编码方案实际上涵盖了字符集。具体可参看前面对于现代字符编码模型的解释。)
Unicode字符集中的Emoji表情字符
另外,与Unicode字符集基本保持兼容的ISO/IEC UCS字符集分为UCS-2(2-byte Universal Character Set)和UCS-4(4-byte Universal Character Set)两部分,分别以2字节和4字节表示字符编号(即码点编号)。
(注意,UCS-2和UCS-4只是表示字符编号的字节数不同,与字符编码方式CEF中的2字节与4字节没有关系,也因此不能分别等同于Unicode编码方案中的UTF-16和UTF-32字符编码方式CEF,有不少文章中称两者等同,这是完全错误的。事实上,由UCS-2和UCS-4所组成的UCS字符集跟Unicode字符集一样,都可分别采用UTF-8、UTF-16和UTF-32字符编码方式CEF对字符编号进行编码。)
其中,UCS-2又被称为基本多语言平面BMP(Basic Multilingual Plane),与Unicode的基本多语言平面BMP保持了一致;而UCS-4格式用四个字节中的31位来表示一个字符的码点编号(即字符编号),这样可表示21亿个不同的字符(2^31=2147483648;最高位为0,另有用途)。
不过,实践中UCS字符集应用得不多,基本以Unicode字符集为主,因此不作详细介绍。
Unicode字符集不仅给每个字符根据其所在的码点分配了一个唯一的码点值(即码点编号,不严格地来讲,也勉强可认为是字符编号,注意不要跟UTF-16、UTF-8等字符编号的编码方式混淆了概念),而且赋予了一个正式的名称:在表示一个Unicode编号(或UCS编号)的十六进制数的前面加上“U+”。
比如,U+0041表示英语大写字母A,U+4E25表示汉字“严”。具体的字符对应表,可以查询unicode.org,汉字也可查询专门的中日韩汉字Unicode编码表。
Unicode字符集中的U+0000~U+007F(即十进制的0~127)与ASCII字符集(即ISO/IEC 646标准)是一致的,U+0000~U+00FF(即十进制的0~255)与ISO/IEC 8859-1标准(Latin-1字符集)也是一致的。
字符编码方案的演变与字节序
一、字符编码方案的演变
前文已经提及,编号字符集CCS(简称字符集)与字符编码方式CEF(简称编码方式)这两个概念,在早期并没有必要严格区分。
在Unicode编码方案出现之前,字符集及其具体的编码方式是绑定耦合在一起的,因此,“字符集”、“编码”或“编码方式”甚至“编码方案”这几个概念经常相互指代、彼此混用。
比如,字符集里的字符编号(即码点编号)在很多文章里也称之为字符编码、字符码、码点、码位、码点值、码值等,字符编码也称之为编码实现、编码方案、编码方式、编码格式、编码形式、内码、编码值、码值(你没看错,字符编号与字符编码都有可能被简称为“码值”,头大了吧),等等,非常混乱。
对于ASCII、GB2312、GBK、GB18030、Big5之类采用传统字符编码模型的历史遗留方案来说,由于基本上一个字符集只使用一种编码方式,因此这种混用问题还不大。
但在Unicode这样采用现代字符编码模型的全新方案出现之后,很多人受上述这些历史遗留方案的影响,从而导致无法正确地理解字符集和编码方式的关系,这导致了概念混乱,引起了大量误解。
然而,对于采用现代字符编码模型的Unicode标准来说,字符集和编码方式是必须明确区分的。从软件工程的角度来讲,传统字符编码模型中紧密绑定耦合在一起的字符集及编码方式这两个概念,在现代字符编码模型中被分离解耦了,而这种解耦带来了极大的灵活性。
这意味着,对于采用现代字符编码模型的同一个字符集,可以采用多个不同的编码方式对其字符编号进行编码。也因此,作为同一个Unicode字符集,目前就定义了UTF-8、UTF-16和UTF-32等(UTF,即Unicode/UCS Transformation Format)多种可选的编码方式。
所以,用Unicode来称呼一个编码方式已不合适,并且容易产生误导,引发混乱和导致困惑,而应该用UTF-8、UTF-16和UTF-32来称呼编码方式,以Unicode来称呼字符集,将包括Unicode字符集及各UTF编码方式等在内的整体称之为字符编码方案或字符编码系统或字符编码标准。
另外,同一字符编码方式CEF的码元序列,在计算机实际处理、存储和传输时,还需再次编码转换为字符编码模式CES的字节序列。
字符编码方式CEF的码元序列可理解为字符编码的逻辑表示形式,相对而言,字符编码模式CES的字节序列则可理解为字符编码在计算机中的物理表示形式。
而字节序列,则涉及到了不同的字节序(Byte-Order,主要分为大端序Big-Endian、小端序Little-Endian)。
二、字节序
字节序,又称字节顺序,其英文为Byte-Order;另外英文中还可称为Endianness,因此也翻译为端序。
Endianness这个词,源自于1726年Jonathan Swift的名著:Gulliver's Travels(格列佛游记)。在书中有一个童话故事,大意是指Lilliput小人国的国王下了一道指令,规定其人民在剥水煮蛋时必须从little-end(小端)开始剥。这个规定惹恼了一群觉得应该要从big-end(大端)开始剥的人。事情发展到后来演变成了一场战争。后来,支持小端的人被称为little-endian,反之则被称为big-endian(在英语中后缀“-ian”表示“xx人”之意)。
1980年,Danny Cohen在他的论文“On Holy Wars and a Plea for Peace”中,第一次使用了Big-endian和Little-endian这两个术语,最终它们成为了异构计算机系统之间进行通讯、交换数据时所要考虑的极其重要的一个问题。
(注:所谓异构是指不同架构、不同结构、不同构造等,而这里的异构计算机系统,主要指的是采用不同CPU和/或不同操作系统的计算机系统。)
为什么会存在字节序的问题?
当然不会是像童话故事里那样出于“无厘头”的原因,而是因为历史上设计不同计算机系统的人在当时基于各自的理由和原因(这里的理由和原因网上存在着各种不同的说法,但也或许根本就没有具体理由和原因,只是设计人员的个人偏好,甚至是随意决定的),在各自计算机系统的设计上作出了不同的选择。
字节序共分为三种:
大端序BE(Big-Endian,也称高尾端序);
小端序LE(Little-Endian,也称为低尾端序);
中间序ME(Middle-Endian,也称为混合序),不太常用,本文不作介绍。
字节序,具体来说,就是多字节数据(大于一个字节的数据)在计算机中存储、读取时其各个字节的排列顺序。
字节序也被称为端序,这里的“端”,是指多字节数据中位于两端的字节,很多情况下还特指尾端字节(也称为小端字节)。
所谓尾端字节或小端字节,假设按照人对文字通常从左到右(或从上到下)的读写顺序来看的话,多字节数据位于右端(或下端)的低位字节就是尾端字节或小端字节,而将位于左端(或上端)的高位字节称为头端字节或大端字节。
当然,如果不按照通常从左到右的顺序,而是按照从右到左的顺序,那么多字节数据位于右端的高位字节就是头端字节或大端字节,而将位于左端的低位字节称为尾端字节或小端字节。
可见,不论读写顺序如何,所谓大端、头端,指的是多字节数据中,代表更大数值的那个字节所在的那一端,而相反的那一端则是小端、尾端。
而要彻底讲清楚大端序(高尾端序)、小端序(低尾端序),则需要从人读写二进制数的方向和内存地址的增长方向两者相结合讲起:
人读写二进制数的方向为(这是确定不变的):左--->右,大端/头端/高位--->小端/尾端/低位;或上--->下,大端/头端/高位--->小端/尾端/低位;
内存地址的增长方向则为(这是确定不变的):左--->右,低地址--->高地址;或上--->下,低地址--->高地址。
不过,计算机在内存中存取数据的方向则不是确定不变的,而是分为两种(注意,由于人的读写方向和内存地址增长方向是确定不变的,因此这里指的是计算机在内存中“书写”或“阅读”数据的方向):
1)左--->右,大端/头端/高位--->小端/尾端/低位;或上--->下,大端/头端/高位--->小端/尾端/低位;
这种情况下,站在人的读写方向和内存地址增长方向(这两者的方向刚好一致)的角度来看,则是:大端在左(或在上),所以称之为大端序;或者说尾端在内存高地址,所以称之为高尾端序(即内存高地址存放多字节数据的尾端字节的字节顺序)。
2)右--->左,大端/头端/高位--->小端/尾端/低位;或下--->上,大端/头端/高位--->小端/尾端/低位。
这种情况下,站在人的读写方向和内存地址增长方向(这两者的方向刚好一致)的角度来看,则是:小端在左(或在上),所以称之为小端序;或者说尾端在内存低地址,所以称之为低尾端序(即内存低地址存放多字节数据的尾端字节的字节顺序)。
【特别提示:大端序、小端序特别容易搞混,不好记忆;因此,建议使用高尾端序、低尾端序,本人是按下面这个方法来记忆的,很容易记住:存储的数据分头和尾——左头右尾,内存的地址分低和高——左低右高;因此,“高尾端”表示内存的高地址存储数据的尾端,“低尾端”表示内存的低地址存储数据的尾端。】
注意,这里的“数据”指的是数据类型意义上的数据,因此,准确地说字节序只跟多字节的整型数据类型有关,比如int、short、long型;跟单字节的整型数据类型byte无关。
那么,为什么就只跟多字节的整型数据有关,而跟单字节的整型数据无关呢?
因为在现代计算机中,字节是计算机数据存储的基本单位,对于整体上的单一字节(a byte),涉及到的是其8个比特位的顺序(位序、比特序,由于一般直接由硬件处理,程序员大致了解即可,这里不深入探讨),显然不存在字节序问题。
然而,对于在数据类型上作为一个整体的多字节数据而言,它是由各个可被单独寻址的字节所组成的(处理器寻址的最小单位一般是1个字节),由于历史的原因,其各个字节的存储顺序在不同的系统平台(包括CPU和操作系统)上是不同的。
也就是说,如果计算机处理的数据是单字节数据类型(byte),是不存在字节序问题的,因为单个字节已经是处理器寻址的最小单位以及存储和传输的最小单元了,存储时单字节数据类型直接进行,读取时也不存在根据前后2个字节才能解析出其值的情况,而构造字节流时也不会从一个单字节数据类型的值当中产生2个或以上的字节(既然是单字节数据类型,构造字节流时当然只可能产生1个字节)。
但是,如果计算机处理的数据是多字节数据类型(int、short、long等),虽然由于构成它们的2个或2个以上的字节是作为一个整体来进行处理的(比如以汇编语言中的数据类型word或dword为单位进行一次性处理,而不是以byte为单位分次处理;更深入地来讲,CPU一般是以字为一个整体来处理数据的,当单个数据不足一个字长时,则将多个数据“拼成”一个字再进行处理),但问题是字节才是CPU对内存寻址的最小单位以及存储和传输的最小单元。
因此,CPU在读取作为一个整体来进行处理的多字节数据类型的数据时,必须根据前后2个或2个以上的字节来解析出一个多字节数据类型的值;而且构造字节序列时也会从一个多字节数据类型的值当中产生2个或2个以上的字节。
这样一来,多字节数据类型的数据内部各字节间的排列顺序,是会影响从字节序列恢复到数值的;反之,也会影响从数值到字节序列的构造。
所以,在存储和读取多字节数据类型的数据时,必须按照计算机系统所规定的字节序进行(这一点程序员了解即可,计算机会自动处理);而尤其是在跨字节序不同的异构计算机系统进行通讯并交换数据时,通讯的任何一方更是必须明确对方所采用的字节序,然后双方将各自接收到的数据按各自的字节序对数据进行转换(有时候需要程序员专门编写转换程序),否则通讯将会出错,甚至直接失败。
实际上,int、short、long等数据类型一般是编程语言层面的概念,更进一步而言,这其实涉及到了机器硬件层面(即汇编语言)中的数据类型byte字节、word字、dword双字等在硬件中的表达与处理机制(实质上字节序跟CPU寄存器的位数、存放顺序密切相关)。具体可参看附文:《本质啊本质之一:数据类型的本质》、《寄存器与字、字节》。
【附:本质啊本质之一:数据类型的本质
CSDN博客 博主:band_of_brothers发表于:2007-10-10 22:20
研究一个层面的问题,往往要从更深的层面找寻答案。这就如C语言与汇编、汇编与机器指令,然而终究要有个底限,这个底限以能使我们心安理得为准,就好比公理之于数学、三大定律之于宏观物理。
在这里就将机器指令作为最后的底限吧,尽管再深入下去还有微指令,但那毕竟是太机器了,可以了。以下所有从C代码编译生成汇编代码用的是命令:cl xxx..c /Fa /Ze。
类型的本质
类型这个概念,好多地方都有讲,但说实话,你真的理解吗?什么是类型?类型是一个抽象的概念还是一个真实的存在?嗯?开始:
1、“好多相同或相似事物的综合”(辞海)。
2、X86机器的数据类型有byte、word、dword、fword、tword、qword,等等。
3、“给内存块一个明确的名字,就象邮件上的收件人一样。给其一个明确的数据类型,就好象说,邮件是一封信,还是一个包裹。”
4、类型就是一次可以操作的块的大小,就是一个单位,就像克、千克、吨一样。双字一次操作32位;字,一次操作16位;如果没有各种类型,机器只有一个类型单位——字节,那么当需要一个4字节大小的块时,就需要4次操作,而如果有双字这个类型单位,那么只需要一次操作就可以了。
5、类型,是机器层面支持的,不是软的,是硬的,有实实在在的机器码为证。
类型的反汇编
W32dasm反汇编出来的东西,可以看出不同的类型,机器码不同,说明类型是机器硬件级别支持的,不是通过软件实现的,更不是一个抽象的概念。
Opcodes上关于mov的机器码讲的更清楚:
需要说明的是,一些大的类型单位,如qword等,在mov等标准指令里是没有的,在一些特殊指令里才能用到,如浮点指令:fmul qword ptr [0067FB08] 机器码:DC0D08FB6700。】
【附:寄存器与字、字节
字节:记为byte,一个字节由8个比特(bit)组成,可以直接存在一个8位寄存器里
1 0 1 0 1 0 0 1
一个字节
字:记为word,一个字由2个字节(共16比特)组成,可以直接存在一个16位寄存器里
1 1 1 1 1 1 1 10 0 0 0 0 0 0 0
高位字节 低位字节
一个8位寄存器用2位十六进制数表示,只能存1个字节(1个byte类型的数据)
一个16位寄存器用4位十六进制数表示,可存1个字(1个word类型的数据)或2个字节(2个byte类型的数据)
一个32位寄存器用8位十六进制数表示,可存2个字(1个dword类型的数据或2个word类型的数据)或4个字节(4个byte类型的数据)】
(笨笨阿林原创文章,转载请注明出处)
下面简要介绍一下字节序的三种类型:
a)小端序Little-Endian(低尾端序)
就是低位字节(即小端字节)存放在内存的低地址,而高位字节(即大端字节)存放在内存的高地址。
这是最符合人的直觉思维的字节序(但却不符合人的读写习惯),因为从人的第一观感来说,低位字节的值小,对应放在内存地址也小的地方,也即内存中的低位地址;反之,高位字节的值大,对应放在内存地址大的地方,也即内存中的高位地址。
b)大端序Big-Endian(高尾端序)
就是高位字节(即大端字节)存放在内存的低地址,低位字节(即小端字节)存放在内存的高地址。
这是最符合人平时的读写习惯的字节序(但却不符合人的直觉思维),因为不用像在Little-Endian中还需考虑字节的高位、低位与内存的高地址、低地址的对应关系,只需把数值按照人通常的书写习惯,从高位到低位的顺序直接在内存中从左到右或从上到下(下图中就是从上到下)按照由低到高的内存地址,一个字节一个字节地填充进去。
c)中间序Middle-Endian(混合序Mixed-Endian)
混合序具有更复杂的顺序。以PDP-11为例,32位的0x0A0B0C0D被存储为:
混合序较少见,常见的多为大端序和小端序。
【附:网络字节序(network byte order网络字节顺序、网络序)。
另外,还一种网络字节序(network byte order网络字节顺序、网络序)。
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。IP协议中定义大端序Big Endian为网络字节序。
不过,容易令人困惑的是,IP协议作为网络层协议,其面向的数据是报文,是没有字节的概念的,也就无关字节序了。因此,英文版wikipedia上说:
In fact, the Internet Protocol defines a standard big-endian network byte order. This byte order is used for all numeric values in the packet headers and by many higher level protocols and file formats that are designed for use over IP.
也就是说,IP协议里的字节序实际上是用在分组头里的数值上的,例如每个分组头会包含源IP地址和目标IP地址,在寻址和路由的时候是需要用到这些值的。
比如,4个字节的32 bit值以下面的次序传输:首先是高位的0~7bit,其次8~15bit,然后16~23bit,最后是低位的24~31bit。这种传输次序称作大端字节序。由于TCP/IP头部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。
再比如,以太网头部中2字节的“以太网帧类型”字段,表示是的后面所跟数据帧的类型。对于ARP请求或应答的以太网帧类型来说,在网络传输时,发送的顺序是以大端方式进行的:0x08,0x06。其在内存中的映象如下所示:
内存低地址
------------------------
0x08 -- 高位字节
0x06 -- 低位字节
------------------------
内存高地址
该字段的值为0x0806,也是以大端方式存放在内存中的。
其实,IP报文中的很多数据都是需要做字节序转换的,比如包长度、check sum校验和等,这些值大都是short(16bit)或者long(32bit)型,所以解析IP报文时也需要做网络->主机字节序转换,而生成报文字节流时则需要进行主机->网络字节序转换(计算机中的字节序被称之为主机字节序,简称主机序;相对于网络传输中的字节序被称之为网络字节序,简称网络序)。
为了进行网络字节序与主机字节序的转换,BSD sockets(Berkeley sockets)提供了四个转换的函数:htons、ntohs、htonl、ntohl,其中h是host、n是network、s是short、l是long:
htons把unsigned short类型从主机字节序转换到网络字节序
htonl把unsigned long类型从主机字节序转换到网络字节序
ntohs把unsigned short类型从网络字节序转换到主机字节序
ntohl把unsigned long类型从网络字节序转换到主机字节序
在使用little endian的系统中,这些函数会把字节序进行转换;在使用big endian类型的系统中,这些函数会定义成空宏。
Windows系统API中也提供了类似的转换函数。而在.Net中,网络字节序与主机字节序两者之间的转换,由IPAddress类的静态方法提供:HostToNetworkOrder和NetworkToHostOrder。】
10.
Intel和AMD的X86平台,以及DEC(Digital Equipment Corporation,后与Compaq合并,之后Compaq又与HP合并)采用的是Little-Endian,而像IBM、Sun的SPARC采用的就是Big-Endian。有的嵌入式平台是Big-Endian的。JAVA字节序也是Big-Endian的。
当然,这不代表所有情况。有的CPU即能工作于小端,又能工作于大端,比如ARM、Alpha、摩托罗拉的Power PC、SPARC V9、MIPS、PA-RISC和IA64等体系结构(具体情形可参考处理器手册),其字节序是可切换的,这种可切换的特性可以提高效率或者简化网络设备和软件的逻辑。
这种可切换的字节序被称为Bi-Endian(前缀“Bi-”表示“双边的、二重的、两个的”),用于硬件上意指计算机存储时具有可以使用两种不同字节序中任意一种的能力。具体这类CPU是大端还是小端,和具体设置有关。如Power PC可支持Little-Endian字节序,但其默认配置为Big-Endian字节序。
一般来说,大部分用户的操作系统,如windows、FreeBsd、Linux是Little-Endian的;少部分,如Mac OS是Big-Endian的。
具体参见下表:
所以说,Little Endian还是Big Endian与操作系统和CPU芯片类型都有关系。因此在一个计算机系统中,有可能同时存在大端和小端两种模式的现象。
这一现象为系统的软硬件设计带来了不小的麻烦,这要求系统设计工程师必须深入理解大端和小端模式的差别。大端与小端模式的差别体现在一个处理器的寄存器、指令集、系统总线等各个层次中。
其实很多技术人员在实际的非跨平台桌面应用开发过程中都很少会直接和字节序打交道(因为由硬件直接处理),但在跨平台及网络应用开发过程中因为涉及到异构计算机系统间的通讯交流,字节序是很难回避的问题。
Unicode字符集的编码方式以及码点、码元
一、字符编码方式CEF的选择
由于Unicode字符集非常大,有些字符的编号(码点值)需要两个或两个以上字节来表示,而要对这样的编号进行编码,也必须使用两个或两个以上字节。
比如,汉字“严”的Unicode码(Unicode码点值、Unicode编号)是十六进制数4E25,转换成二进制数有15位(100 1110 0010 0101),对“严”这个字符的编号进行编码的话,至少需要2个字节。表示其他更大编号的字符,可能需要3个字节或者4个字节,甚至更多。
这带来两个问题:
一是,如何才能区别Unicode字符和ASCII字符的编码?计算机怎么知道三个字节表示的是一个字符,而不是分别表示三个字符呢?
二是,我们知道,英文字母只用一个字节来编码就够了,而如果Unicode统一硬性规定,每个字符都用两个、三个或四个字节来编码,那么每个英文字母编码的前面都必然有一个、两个到三个字节全是0,这对于存储和传输来说是极大的浪费。
这就涉及到了字符编码方式CEF的选择问题。Unicode字符的编码方式一般有三种:UFF-8、UTF-16、UTF-32。在具体介绍这些编码方式之前,需要再次深入了解两个概念——码点(Code Point)与码元(Code Unit)。
二、码点
一个字符集一般可以用一张或多张由多个行和多个列所构成的二维表来表示。
二维表中行与列相交的点,称之为码点(Code Point代码点),也称之为码位(Code position代码位);每个码点分配一个唯一的编号,称之为码点值或码点编号,除开某些特殊区域(比如代理区、专用区)的非字符码点和保留码点,每个码点唯一对应于一个字符。
因此,除开非字符码点和保留码点,码点值(即码点编号)通常来说就是其所对应的字符的编号,所以码点值有时也可以直接称之为字符编号,虽然不够准确,但更为直接。
字符集中所有码点数量的总和,称之为编号空间(Code Space,又被称之为代码空间、编码空间、码点空间、码空间)。
码点值最初用两个字节的十六进制数字表示,比如字母A的Unicode码点值为0041,常写作U+0041,这种形式称为Unicode码点名称,不严格地来讲,也可称之Unicode字符名称(因为存在着非字符码点和保留码点,并非每个码点都分配了字符,所以这种称呼不够准确,不过目前更为普遍)。
后来随着Unicode字符集的不断增补扩大(比如现在的Unicode字符集至少需要21位才能全部表示),码点值也扩展为用三个字节或以上的十六进制数字表示。
例如,ASCII字符集用0~127这连续的128个数字编号分别表示128个字符。GBK字符集使用区位码的方式为每个字符编号,首先定义一个94×94的矩阵,行称为“区”,列称为“位”,然后将所有国标汉字放入矩阵当中,这样每个汉字就可以用唯一的“区位”码来标识了。例如“中”字被放到54区第48位,因此其区位码(字符编号)就是5448。
而目前Unicode标准中,将字符按照一定的类别划分到0~16这17个平面(Plane层面)中,每个平面中拥有2^16 = 65536个码点,因此,目前Unicode字符集所拥有的码点总数,也就是Unicode的编号空间为17*65536=1114112。
注意,网络上的很多文章中,代码点、码点、码点值、码值、代码位、码位、字符码、Unicode码、字符编号、字符编码、编码方案、编码方式、编码格式等等经常互相代替混用。
(笨笨阿林原创文章,转载请注明出处)
三、码元
在计算机存储和网络传输时,码点值(即字符编号)被映射到一个或多个码元(Code Unit代码单元、编码单元)。
码元可理解为字符编码方式CEF(Character Encoding Form)对码点值进行编码处理时作为一个整体来看待的最小基本单元(基本单位)。
为什么非要引入“码元”这个概念?或者说,为什么非要强调“码元”这个概念?
码元某种程度上可认为对应于高级语言中的基本数据类型。而高级语言层面的基本数据类型,若要更深入一步地来讲,实质上对应于机器硬件层面(汇编语言)的数据类型byte字节、word字、dword双字等在硬件中的表达与处理机制。
之所以要强调“码元”的概念,是因为字符编码作为一串数字序列,最终还是得通过机器硬件层面的数据类型来表示。
而码元的实质,就是机器硬件层面(汇编语言)的数据类型;不同的码元,代表着不同位数的数据类型。
数据类型有单字节与多字节之分,所以码元也有单字节与多字节之分;多字节数据类型由于历史的原因,存在着字节序的所谓大端序(Big-Endian)与小端序(Little-Endian)之分,因此多字节码元也存在着大端序与小端序之分(具体详见前文中有关字节序的解释;注意,单字节数据类型则没有字节序的问题,所以单字节码元也就没有字节序问题)。
这就是之所以要强调“码元”这个概念的关键原因。
码点值(即字符编号)的具体实现方式——字符编码方式CEF,就是由一个或多个码元这样的最小基本单元构成的。
最常用的码元是8位(1字节)的单字节码元,另外还有16位(2字节)和32位(4字节)两种多字节码元,分别相当于C++中的无符号整型BYTE、WORD、DWORD(在VC++6.0中,这三种数据类型的定义分别为:
typedef unsigned char BYTE;,1个字节;
typedef unsigned short WORD;,2个字节;
typedef unsigned long DWORD;,4个字节)。
(笨笨阿林原创文章,转载请注明出处)
于是,三种码元对应就有了Unicode字符编号(码点值)的三种UTF编码方式(即Unicode码转换格式Unicode Transformation Format,或称通用字符集转换格式UCS Transformation Format):
UTF-8(8-bit Unicode/UCS Transformation Format),
UTF-16(16-bit Unicode/UCS Transformation Format),
UTF-32(32-bit Unicode/UCS Transformation Format);
或者反过来说,Unicode字符编号(码点值)的三种UTF编码方式(UTF-8、UTF-16、UTF-32)分别采用了不同的码元(BYTE、WORD、DWORD)来编码。
例如,“汉字”这两个中文字符的Unicode码点值(Unicode字符编号)是0x6C49和0x5B57,其三种UTF编码在VC++6.0中可按如下定义进行“模拟”:
注意,这里之所以说是“模拟”,因为从本质上来讲,在机器硬件层面上的所有数据类型,只存在着被视作一个整体来处理的比特序列(比特流)的位数不同之分,不存在着高级语言层面上数据类型的数值、字符串、布尔值等的语义不同之分。
因此,机器硬件层面上的数据类型与高级语言层面上的数据类型,严格来讲,在本质含义上还是有着很大不同的。当然,高级语言层面上的数据类型最终还是会被转化为机器硬件层面上的数据类型,毕竟计算机只“认识”由0和1所组成的比特流。具体详见前文中有关字节序的解释。
这里用BYTE、WORD、DWORD分别表示无符号8位整数、无符号16位整数和无符号32位整数;因而UTF-8、UTF-16、UTF-32可认为分别以BYTE、WORD、DWORD作为码元。
“汉字”这两个中文字符的UTF-8编码需要六个BYTE(共6个单字节码元),大小是6个字节;UTF-16编码需要两个WORD(共2个双字节码元),大小是4个字节;UTF-32编码需要两个DWORD(共2个四字节码元),大小是8个字节。
由于多字节数据类型的数据在计算机存取时存在一个字节序的问题,因此,UTF-16、UTF-32这两种编码方式所编码出来的逻辑意义上的多字节码元序列,在映射为物理意义上的字节序列时,字节序列的字节序因系统平台的不同而不同。
前面已经多次强调过了,这里再次特别强调一下:由单字节数据类型所组成的多字节数据是不存在字节序的问题的。因此,采用单字节码元进行编码的UTF-8编码,虽然ASCII字符为单字节编码,但非ASCII字符是多字节编码的,但却不存在字节序问题,这是跟同样为多字节编码、但采用多字节码元的UTF-16、UTF-32不同之处。详见下表所列:
Unicode字符集三大编码方式(UTF-8、UTF-16、UTF-32)比较一览表
UTF-8编码方式与字节序标记
一、UTF-8编码方式
接下来将分别介绍Unicode字符集的三种编码方式:UTF-8、UTF-16、UTF-32。这里先介绍应用最为广泛的UTF-8。
为满足基于ASCII、面向字节的字符处理的需要,Unicode标准中定义了UTF-8编码方式。UTF-8应该是目前应用最广泛的一种Unicode编码方式(但不是最早面世的,UTF-16要早于UTF-8面世)。它是一种使用8位码元(即单字节码元)的变宽(即变长或不定长)码元序列的编码方式。
由于UTF-16对于ASCII字符也必须使用两个字节(因为是16位码元)进行编码,存储和处理效率相对低下,并且由于ASCII字符经过UTF-16编码后得到的两个字节,高字节始终是0x00,很多C语言的函数都将此字节视为字符串末尾从而导致无法正确解析文本。
因此,UTF-16一开始推出的时候就遭到很多西方国家的抵制,大大影响了Unicode的推行。于是后来又设计了UTF-8编码方式,才解决了这些问题。
UTF-8的码元由8位单字节组成;在UTF-8中,因为码元较小的缘故,Unicode码点值被映射到一个、两个、三个或四个码元;换言之,UTF-8使用一个至四个8位单字节码元的序列来表示Unicode字符。
UTF-8编码方式对所有ASCII码点值(0x00~0x7F)具有透明性。所谓透明性,具体指的是在U+0000到U+007F范围内(十进制为0~127)的Unicode码点值,被直接转换为UTF-8单一字节码元0x00~0x7F,与ASCII码没有区别。
并且,0x00~0x7F不会出现在UTF-8编码的非ASCII字符的首字节与非首字节的任意一个字节中(非ASCII字符的UTF-8编码为由多个单字节码元所组成的码元序列),这样就保证了与早已应用广泛且已成为工业标准的ASCII码的完全兼容,避免了歧义,同时纠错能力也强。
(笨笨阿林原创文章,转载请注明出处)
UTF-8同其他的多字节码元编码方式相比具有以下优点:
a)UTF-8的编码空间足够大,未来Unicode新标准收录更多字符,UTF-8也能适应,因此不会再出现UTF-16那样的尴尬。
(注:这里所指的编码空间并不是前文所提到的编号空间Code Space,编号空间属于编号字符集CCS里的概念,而编码空间属于字符编码方式CEF里的概念,两者不能等同;这里的编码空间可理解为编码方式的未来可扩展性、高适应性,详见后文《UTF-8究竟是怎么编码的——UTF-8的编码算法介绍》以及《UTF-16究竟是怎么编码的——UTF-16的编码算法介绍》)
b)UTF-8是变长编码(准确地说是变长码元序列,而码元本身是固定长度为8位单字节的,也就是说,UTF-8采用的单字节码元),比如一个字节足以容纳所有的ASCII字符,就用一个字节来存储,不必在高位补0以浪费更多的字节来存储,因此在英语作为国际语言的现实情况下,UTF-8因其ASCII字符的单字节编码这一特性可节省空间。
c)UTF-8完全直接兼容ASCII码,而非不完全间接兼容。
d)UTF-8的码元序列的第一个字节指明了后面所跟的字节的数目(即带有前缀码),这对字节流的前向解析非常有效(详见后文《UTF-8究竟是怎么编码的——UTF-8的编码算法介绍》)。
e)也因为UTF-8编码带有前缀码,所以容错性好,即使在传输过程中发生局部的字节错误,比如即便丢失、增加、改变了某些字节,也不会导致所有后续字符全部错乱这样传递性、连锁性的错误问题(否则,若存在错误传递性、连锁性的话,一旦中间某些字节出错,则必须丢弃从出错点开始到结尾的所有编码字节,比如GB码、UTF-32码就是如此),因此很容易重新同步,具有很强的鲁棒性(即健壮性)。
f)由于UTF-8编码没有状态,从UTF-8字节流的任意位置开始可以有效地找到一个字符的起始位置,字符边界很容易界定、检测出来,所以具有很好的“自同步性”。
g)UTF-8已经成为互联网所采用的字符编码方式的事实标准。
h)UTF-8是字节顺序无关的(因为是单字节码元,而非像UTF-16、UTF-32这样的多字节码元),它的字节顺序在所有系统中都是一样的,其码元序列与字节序列相同,因此它实际上并不需要字节顺序标记BOM(Byte-Orde Mark),虽然Windows系统经常“多此一举”地加上BOM。(有关字节序标记BOM的介绍见下文)
字节序问题在进行信息交换时会带来不小的麻烦。如果字节序未协商好,将导致乱码;若协商结果为双方一个采用大端一个采用小端,则必然有一方要进行大小端转换,性能损失不可避免(字节序的大小端问题其实不像看起来那么简单,有时会涉及硬件、操作系统、上层应用软件多个层次,可能会导致多次转换,详见前文中有关字节序Byte-Orde的介绍)。
i)字节FE(二进制为1111 1110)和FF(二进制为1111 1111)在UTF-8编码中永远不会出现(因为UTF-8编码方式中,每个字节只能以0、110、1110、11110或10开头,详见后文介绍)。因此可以用称之为零宽度不中断空格(ZERO WIDTH NO-BREAK SPACE)的字符(Unicode字符名称为U+FEFF)作为字节顺序标记BOM来标明UTF-16或UTF-32文本的字节序。
(Windows系统中BOM有时也用在UTF-8编码的文本文件的开头,虽然UTF-8编码不存在字节序问题,但Windows却用BOM来表明该文本文件的编码格式为UTF-8,看起来这有点“多此一举”,其具体原因详见后文)
j)UTF-8编码可以通过屏蔽位和移位操作快速读写。
k)字符串比较时strcmp()和wcscmp()的返回结果相同,因此使排序变得更加容易。
UTF-8编码方式也并非完美无缺,大致上有如下缺点:
A)无法根据字符数直接判断出UTF-8文本的字节数,因为UTF-8是一种变长编码方式(码元虽然固定为8位单字节,但码元序列是变长的,可能是单个码元共8位,比如ASCII字符;也可能是两个码元共16位、三个码元共24位、四个码元共32位等)。因此,无论是计算字符数,还是执行索引操作,效率都不高。
B)需要用2个字节编码那些在扩展ASCII(即EASCII)字符集中只需1个字节编码的扩展字符。
c)以8位单字节码元编码的UTF-8字符会被Email网关过滤,因为Internet上的信息传输最初设计为7位ASCII码字符(ASCII仅用到了1个字节的低7位)的传输。因此产生了UTF-7编码(类似于同样为Email传输而设计的Base64编码或quoted-printable编码,由于Base64编码或quoted-printable编码各有其不足,因此又设计了UTF-7编码)。
d)UTF-8在它的表示中使用值100xxxxx的几率超过50%,而现存的实现如ISO 2022、4873、6429和8859系统,会把它错认为是C1控制码。因此产生了UTF-7.5编码。
(笨笨阿林原创文章,转载请注明出处)
二、字节序标记BOM
在将逻辑形式的码元序列(或可称之为逻辑编码)映射为物理形式的字节序列(或可称之为物理编码)时,因系统平台的差异,存在一个字节序(Byte-Order字节顺序)的问题。Unicode/UCS规范中推荐的标记字节顺序的方法是BOM字节序标记(Byte-Order Mark字节顺序标记)。
字节序标记BOM是Unicode码点值为FEFF(十进制为65279,二进制为1111 1110 1111 1111)的字符的别名。
最初,字符U+FEFF如果出现在字节流的开头,则用来标识该字节流的字节序——是高位在前还是低位在前;如果它出现在字节流的中间,则表达为该字符的原义——零宽度不中断空格(ZERO WIDTH NO-BREAK SPACE零宽度无断空白)。该字符名义上是个空格,实际上是零宽度的,即相当于是不可见也不可打印字符(平常使用较多的是ASCII空格字符,是非零宽度的,需要占用一个字符的宽度,为可见不可打印字符)。
从Unicode 3.2开始,U+FEFF只能出现在字节流的开头,且只能用于标识字节序,就如它的别名——字节序标记——所表示的意思一样;除此以外的用法已被舍弃。取而代之的是,使用U+2060来表示零宽度不中断空格。
如果UTF-16编码的字节序列为大端序,则该字节序标记在字节流的开头呈现为0xFE 0xFF;若字节序列为小端序,则该字节序标记在字节流的开头呈现为0xFF 0xFE。如果UTF-32编码的字节序列为大端序,则该字节序标记在字节流的开头呈现为0x00 0x00 0xFE 0xFF;若字节序列为小端序,则该字节序标记在字节流的开头呈现为0xFF 0xFE 0x00 0x00。
UTF-8编码本身没有字节序的问题,但仍然有可能会用到BOM——有时被用来标示某文本是UTF-8编码格式的文本;再强调一遍:在UFT-8编码格式的文本中,如果添加了BOM,则只用它来标示该文本是由UTF-8编码方式编码的,而不用来说明字节序,因为UTF-8编码不存在字节序问题。
许多Windows程序(包含记事本)会添加BOM到UTF-8编码格式的文件中(至于为什么要添加BOM,可参看后续《微软跟联通有仇?》一文)。然而,在类Unix系统中,这种作法则不被建议采用。
因为它会影响到无法识别它的编程语言,如gcc会报告源码文件开头有无法识别的字符;而在PHP中,如果没有激活输出缓冲(outputbuffering),它会使得页面内容开始被送往浏览器(即header头被提交),这使PHP脚本无法指定header头(HTTP Header)。
对于已在IANA注册的字符编码(这里的字符编码实际为字符编码模式CES)UTF-16BE、UTF-16LE、UTF-32BE和UTF-32LE等来说,不可使用BOM。因为其名称本身已决定了其字节顺序。对于已注册的字符编码(这里的字符编码实际为字符编码方式CEF)UTF-16和UTF-32来说,则必须在文本开头使用BOM。
不同编码的字节序列中所使用的字节序标记BOM本身的字节序列呈现:
(笨笨阿林原创文章,转载请注明出处)
三、小结
由于UTF-8编码方式以一个字节(8位)作为码元,属于单字节码元,在计算机处理、存储和传输时不存在字节序问题(字节序问题只跟多字节码元有关),因此避免了平台依赖性,跨平台兼容性好。
它相对于其他编码方式对英语更为友好,同样也对计算机语言(如C++、Java、C#、JavaScript、PHP、HTML等)更为友好。它在处理ASCII等常用字符集时很少会比UTF-16低效。
所以,UTF-8是较为平衡、较为理想的Unicode编码方式。虽然Windows平台由于历史的原因API缺乏对UTF-8的原生支持(Windows原生支持的是UTF-16,因为UTF-16早于UTF-8面世),导致UTF-8推出后的早期使用不广,但目前是应用最为广泛的三大UTF编码方式之一。
因此,应该尽量使用UTF-8(准确地说,应该尽量使用UTF-8 withoutBOM,即不带字节顺序标记BOM的UTF-8)。
UTF-8究竟是怎么编码的
UTF-8编码是Unicode字符集的一种编码方式(CEF),其特点是使用变长字节数(即变长码元序列、变宽码元序列)来编码。一般是1到4个字节,当然,也可以更长。
为什么要变长呢?这可以理解为按需分配,比如一个字节足以容纳所有的ASCII字符,那何必补一堆0用更多的字节来存储呢?
实际上变长编码有其优势也有其劣势,优势是节省空间、自动纠错性能好、利于传输、扩展性强,劣势是不利于程序内部处理,比如正则表达式检索;而UTF-32这样等长码元序列(即等宽码元序列)的编码方式就比较适合程序处理,当然,缺点是比较耗费存储空间。
那UTF-8究竟是怎么编码的呢?也就是说其编码算法是什么?
UTF-8编码最短的为一个字节、最长的目前为四个字节,从首字节就可以判断一个UTF-8编码有几个字节:
如果首字节以0开头,肯定是单字节编码(即单个单字节码元);
如果首字节以110开头,肯定是双字节编码(即由两个单字节码元所组成的双码元序列);
如果首字节以1110开头,肯定是三字节编码(即由三个单字节码元所组成的三码元序列),以此类推。
另外,UTF-8编码中,除了单字节编码外,由多个单字节码元所组成的多字节编码其首字节以外的后续字节均以10开头(以区别于单字节编码以及多字节编码的首字节)。
0、110、1110以及10相当于UTF-8编码中各个字节的前缀,因此称之为前缀码。其中,前缀码110、1110及10中的0,是前缀码中的终结标志。
UTF-8编码中的前缀码起到了很好的区分和标识的作用——当解码程序读取到一个字节的首位为0,表示这是一个单字节编码的ASCII字符;当读取到一个字节的首位为1,表示这是一个非ASCII字符的多字节编码字符中的某个字节(可能是首字节,也可能是后续字节),接下来若继续读取到一个1,则确定为首字节,再继续读取直到遇见终结标志0为止,读取了几个1,就表示该字符为几个字节的编码;当读取到一个字节的首位为1,紧接着读取到一个终结标志0,则该字节显然是非ASCII字符的后续字节(即非首字节)。
(笨笨阿林原创文章,转载请注明出处)
所以,1~4字节的UTF-8编码看起来分别是这样的:
单字节可编码的Unicode码点值范围十六进制为0x0000 ~ 0x007F,十进制为0 ~ 127;
双字节可编码的Unicode码点值范围十六进制为0x0080 ~ 0x07FF,十进制为128 ~ 2047;
三字节可编码的Unicode码点值范围十六进制为0x0800 ~ 0xFFFF,十进制为2048 ~ 65535;
四字节可编码的Unicode码点值范围十六进制为0x10000 ~ 0x1FFFFF,十进制为65536 ~ 2097151(目前Unicode字符集码点编号的最大值为0x10FFFF,实际尚未编号到0x1FFFFF;这说明作为变长字节数的UTF-8编码其未来扩展性非常强,即便目前的四字节编码也还有大量编码空间未被使用,更不论还可扩展为五字节、六字节……)。
(笨笨阿林原创文章,转载请注明出处)
上述Unicode码点值范围中十进制值127、2047、65535、2097151这几个临界值是怎么来的呢?
因为UTF-8编码中的每个字节中都含有起到区分和标识之用的前缀码0、110、1110以及10之一,所以1~4个字节的UTF-8编码其实际有效位数分别为8-1=7位(2^7-1=127)、16-5=11位(2^11-1=2047)、24-8=16位(2^16-1=65535)、32-11=21位(2^21-1=2097151),如下表所示:
注:上图中的Unicode range即Unicode码点值范围(也就是Unicode码点编号范围),Hex为16进制,Binary为二进制;Encoded bytes即UTF-8编码中各字节的编码方式(即编码算法),其中,x代表Unicode二进制码点值的单字节或低字节中的低7位或8位、y代表两字节码点值的高字节中的低3位或8位以及三字节码点值的中字节中的8位、z代表三字节码点值的高字节中的低5位。
因此,UTF-8编码的算法简单地用一句话来概括就是:首先确定UTF-8编码中各个字节的前缀码;之后再将UTF-8编码中各个字节除了前缀码所占用之外的位,依次分配给Unicode字符码点值二进制中各个位的值,换言之,就是用Unicode字符码点值二进制中各个位的值,依次填充UTF-8编码中的各个字节除了前缀码所占用之外的位。
由于ASCII字符的UTF-8编码使用单字节,而且和ASCII编码一模一样,这样所有原先使用ASCII编码的文档就可以直接解码了,无需进行任何转换,实现了完全兼容。考虑到计算机世界中英文文档的数量之多,这一点意义重大。
而对于其他非ASCII字符,则使用2~4个字节的编码来表示。其中,首字节中前置的1的个数代表该字符编码的字节数(110代表两个字节、1110代表三个字节,以此类推),非首字节之外的剩余字节的高2位始终是10,这样就不会与ASCII字符编码以及非ASCII字符的首字节编码相冲突。
例如,假设某个字符的首字节是1110yyyy,前置有三个1,说明该字符编码总共有三个字节,必须和后面两个以10开头的字节结合才能正确解码该字符。
由此可知,UTF-8编码设计得非常精巧,虽说不上完美无缺,但若与后文将要介绍的UTF-16、UTF-32以及前文介绍过的那些ANSI编码相比较,对于其精巧设计将体会得更为深切透彻。因此,UTF-8越来越得到全球一致认可,大有一统字符编码之势。
UTF-16编码方式
UTF-16编码方式源于UCS-2(Universal Character Set coded in 2 octets、2-byte Universal Character Set)。而UCS-2,是早期遗留下来的历史产物。
UCS-2将字符编号直接映射为字符编码(CEF,而非CES,详见前文中对现代字符编码模型的解释),亦即字符编号就是字符编码,中间没有经过特别的编码算法转换。因此,从现代字符编码模型的角度来看的话,此时并没有将编号字符集CCS与字符编码方式CEF作严格区分,既可以将UCS-2看作是编号字符集CCS中的字符编号,也可以看作是字符编码方式CEF中的字符编码。
后来,随着Unicode联盟与ISO/IEC就创建全球统一的单一通用字符集进行合作,Unicode字符集与UCS字符集逐渐相互融合,两者最终基本保持了一致(详见前文《刨根究底字符编码之八——Unicode编码方案概述》中的介绍)。
(笨笨阿林原创文章,转载请注明出处)
这之后,Unicode逐渐占据了主导地位,并引入了UTF-16编码方式。为什么要引入UTF-16编码方式呢?
前文已经介绍过了,Unicode字符集(CCS)到目前为止定义了包括1个基本平面BMP和16个增补平面SP在内的共17个平面。
每个平面的码点数量为2^16=65536个,因此17个平面的码点总数为共65536*17=1114112个。其中,基本平面码点为65536个(码点编号范围为0x0000~0xFFFF),增补平面码点为1114112-65536=65536*16=1048576个(码点编号范围为0x10000~0x10FFFF)。
很明显,简单地用一个16位码元肯定无法表示所有17个平面的这么多码点(因为2^16=65536,而码点总数为65536*17=1114112)。而UCS-2,正是用两个字节共16位来表示一个字符的。为支持字符编号超过U+FFFF的增补字符,扩展势在必行。
UCS因而又提出了UCS-4,即用四个字节共32位来表示一个字符(此时UCS-4同样既可认为是编号字符集CCS中的字符编号,也可认为是字符编码方式CEF中的字符编码)。但码元也因此从16位扩展到了32位。
而Unicode却提出了不同的扩展方式——代理机制。具体而言,就是为了能以一个统一的16位码元同时编码基本平面以及增补平面中的字符码点编号,Unicode设计引入了UTF-16编码方式,并且通过代理机制实现了扩展。
UTF-16编码方式的引入,从现代字符编码模型的角度来看的话,彻底将编号字符集CCS与字符编码方式CEF作了严格区分。也就是说,在UTF-16编码方式中,编号字符集CCS中的字符编号与字符编码方式CEF中的字符编码不再仅仅是简单的直接映射关系。
具体来说,就是Unicode字符集基本平面BMP中的字符(大致相当于UCS字符集中的UCS-2字符,但必须除开U+D800~U+DFFF这一在Unicode字符集BMP中称之为代理码点的部分),仍然是直接映射关系,亦即这部分字符的字符编号与字符编码是等同的。
但Unicode字符集增补平面中的字符(大致相当于UCS字符集UCS-4字符中除开UCS-2字符的部分,因为广义上的UCS-4字符实际上包含了UCS-2字符,当然狭义上的UCS-4字符不包括UCS-2字符),却不是直接映射关系,而是必须通过代理机制这一编码算法的转换,亦即这部分字符的字符编号与字符编码不是等同的。
因此,在Unicode引入了UTF-16编码方式之后,站在现代字符编码模型的角度上来看的话,再将UCS-2和UCS-4直接称之为字符编码方式CEF已不是很合适,更多的应该是编号字符集CCS中的概念(当然,在了解其历史原因之后,将UCS-2和UCS-4同时理解为编号字符集CCS和字符编码方式CEF也未尝不可);而若将UCS-2等同于UTF-16,将UCS-4等同于UTF-32(后文会有介绍),显然也是不合适的。
(笨笨阿林原创文章,转载请注明出处)
UTF-16中的所谓代理机制,实际上就是用两个对应于基本平面BMP代理区(Surrogate Zone)中的码点编号的16位码元来表示一个增补平面码点,这两个用来表示一个增补平面码点的特殊16位码元被称之为代理对(Surrogate Pair)(解释详见后文《UTF-16究竟是如何编码的——UTF-16的编码算法详解》)
UTF-16编码方式及其代理机制是在Unicode 2.0中为支持字符编号超过U+FFFF的增补字符而引入的,于是从此就由UCS-2的等宽(16位)码元序列编码方式(如前文所述,从现代字符编码模型的角度来看的话,UCS-2更多是的编号字符集CCS中的概念,但考虑到其历史原因,称之为字符编码方式CEF亦未尝不可,下同,不再赘述),变成了UTF-16的变宽(16位或32位)码元序列编码方式。不过,码元依然保持了16位不变。
UCS-2所编码的字符集中的U+D800~U+DFFF这部分代理码点除外的话,UTF-16所编码的字符集可看成是UCS-2所编码的字符集的父集。
在没有引入增补平面字符之前,UTF-16与UCS-2(U+D800~U+DFFF这部分代理码点除外)的编码完全相同。但当引入增补平面字符后,UTF-16与UCS-2的编码就不完全相同了(事实上,由于UCS-2只有两个字节,根本无法编码增补平面字符)。
现在若有软件声称自己支持UCS-2编码,那相当于是在暗示其仅支持UCS字符集或Unicode字符集中的基本平面字符,而不能支持增补平面字符。
所以说,UTF-16是变长编码方式,每个字符编码为2字节或4字节;而UCS-2是定长编码方式,每个字符编码固定为2字节。
另外,UTF-16中,大部分汉字采用两个字节编码,少量不常用汉字采用四个字节编码。
Windows 2000及之后的版本是支持UTF-16的,之前的Windows NT/95/98/ME是只支持UCS-2的。
(笨笨阿林原创文章,转载请注明出处)
作为逻辑意义上的UTF-16编码(码元序列),由于历史的原因,在映射为物理意义上的字节序列时,分为UTF-16BE(Big Endian)、UTF-16LE(Little Endian)两种情况。比如,“ABC”这三个字符的UTF-16编码(码元序列)为:00 41 00 42 00 43;其对应的各种字节序列如下:
Windows平台下的UTF-16编码(即上述的FF FE 41 00 42 00 43 00)默认为带有BOM的小端序(即Little Endian with BOM)。你可以打开记事本,写上ABC,保存时选择Unicode(这里的Unicode实际上指的是UTF-16 Little Endian with BOM,即带BOM的UTF-16小端序CES编码,详见后文解释)
然后保存,再用UltraEdit编辑器看看它的编码结果:
Windows从NT时代开始就采用了UTF-16编码方式,很多流行的编程平台,例如.Net、Java、Qt还有Mac下的Cocoa等都是使用UTF-16作为基础的字符编码。例如代码中的字符串,在内存中相应的字节流就是UTF-16字节序列的。(注意,UTF-16编码在Windows环境中被误用为“widechar”和“Unicode”的同义词)
UTF-16一方面使用变长码元序列的编码方式,相较于定长码元序列的UTF-32算法更复杂(甚至比同样是变长码元序列的UTF-8也更为复杂,因为引入了独特的代理对这样的代理机制);另一方面仍然占用过多字节,比如ASCII字符也同样需要占用两个字节,相较于UTF-8更浪费空间和带宽。
因此,UTF-16在Unicode字符集的三大编码方式(UTF-8、UTF-16、UTF-32)中表现较为糟糕。它的存在是历史原因造成的,引起了很多混乱。不过由于其推出时间最早,已被应用于大量环境中,目前虽然不被推荐使用,但长期来看,作为程序人员都不得不与之打交道。因而,对于其具体的编码算法的了解是十分必要的,本系列文章的下一篇将详细介绍其复杂的编码算法(主要是代理编码算法)。
UTF-16究竟是怎么编码的
首先要注意的是,代理Surrogate是专属于UTF-16编码方式的一种机制,UTF-8和UTF-32是不用代理的。
如前文所述,为了让UTF-16能继续编码基本平面后面的增补平面中的码点值,于是扩展了UTF-16编码方式。
具体的扩展方法就是为其增加了代理机制,用两个对应于基本平面码点(即BMP代理区中的码点)的16位码元来表示一个增补平面码点,这两个用来表示一个增补平面码点的特殊16位码元就被称为“代理对”。
如果要用简单的一句话来概括,就是——所有大于0xFFFF的码点值(即增补平面码点编号,范围为0x10000~0x10FFFF,十进制为65536~1114111;注意,0xFFFF是十六位二进制数的最大值的十六进制表示)要编码成UTF-16编码方式的话,就必须使用代理机制(也就是用代理对来表示)。
在UTF-16编码方式中,被合起来称为”代理对“的这两个16位码元就其中的任一单个码元而言,其实就直接对应于基本平面BMP中的某一个码点(即BMP中每一个码点的值必然对应于一个16位码元的值,因为基本平面中的码点总数为2^16=65536个,而16位码元能表示的值也等于2^16=65536个)。
这样一来,就产生了冲突:某个UTF-16码元到底是用于表示基本平面字符的码元,还是用于表示增补平面字符的代理对中的代理码元?
因此,为避免冲突,这些被用作“代理”的任一码元所对应的码点在基本平面中均未定义字符,即均没有指定字符。
“代理”的真实含义或许就在于此:用两个基本平面中未定义字符的码点合起来“代为署理”增补平面中的码点。
因此,基本平面中这些用作“代理”的码点区域就被称之为“代理区(Surrogate Zone)”,其码点编号范围为0xD800~0xDFFF(十进制55296~57343),共2048个码点。
增补平面一共有16个平面(即第2平面~第17平面),码点编号范围为0x10000~0x10FFFF(十进制为65536~1114111,码点总数为1048576个)。用两个代理码元表示,第一个码元的取值范围为0xD800~0xDBFF(二进制为1101 1000 0000 0000 ~ 1101 1011 1111 1111,十进制为55296 ~ 56319),第二个码元的取值范围为0xDC00~0xDFFF(二进制为1101 1100 0000 0000 ~ 1101 1111 1111 1111,十进制为56320 ~ 57343)。
因此,增补平面的第一个码点的编号0x10000其UTF-16编码就是0xD800 0xDC00(即0x10000经UTF-16编码后的码元序列为0xD800 0xDC00),其余类推。展现为二进制形式后如下:
====代理码元1==== ====代理码元2====
1101 10pp ppxx xxxx 1101 11xx xxxx xxxx
其中代理码元1中的110110、代理码元2中的110111是定数,p、x是变数。去掉定数后组合起来就是pppp xxxx xxxx xxxx xxxx,共20位(2^20=1048576),刚好能够表示增补平面中的全部码点(0x10000~0x10FFFF,共1048576个)。其中pppp共4位,表示16个增补平面之一的编号(2^4=16);紧接着的16位x表示某个增补平面内的某个码点(2^16=65536,而65536*16=1048576)。
按照上面的编码方式,代理对里面的两个代理码元分别称之为高16位代理码元(或称为lead surrogates引导代理、前导代理),和低16位代理码元(或称为trail surrogates尾随代理、后尾代理)。
由于引导代理和尾随代理的值分别在0xD800~0xDBFF(十进制为55296 ~ 56319)之间和0xDC00~0xDFFF(十进制为56320 ~ 57343)之间,所以首尾两个代理总共可以组合出(56319-55296+1)*(57343-56320+1)=1048576个代理对,也就是总共可以表示1048576个增补码点,而目前Unicode标准所确定的16个增补平面的码点总和也就是65536*16=1048576个。
(笨笨阿林原创文章,转载请注明出处)
从增补平面的码点值通过基本平面中的代理对编码为增补平面字符的码元序列的具体算法如下:
1)增补平面中的码点值(0x10000~0x10FFFF,二进制为0001 0000 0000 0000 0000~1 0000 1111 1111 1111 1111,对应的字符名称为U+10000~U+10FFFF)减去0x10000(二进制为0001 0000 0000 0000 0000),可得到20位长的比特组(值的范围为0x00000~0xFFFFF,二进制为0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111);
2)将得到的20位长的比特组分拆为两部分:高位10比特和低位10比特;
3)20位长的比特组中的高位10比特(值的范围为0x000~0x3FF,二进制为00 0000 0000~11 1111 1111)加上0xD800(二进制为1101 1000 0000 0000),得到第一个代理码元即引导代理(值的范围是0xD800~0xDBFF,二进制为1101 1000 0000 0000 ~1101 1011 1111 1111);
4)20位长的比特组中的低位10比特(值范围也是0x000~0x3FF,二进制为00 0000 0000~11 1111 1111)加上0xDC00(二进制为1101 1100 0000 0000),得到第二个代理码元即尾随代理(值的范围是0xDC00~0xDFFF,二进制为1101 1100 0000 0000 ~1101 1111 1111 1111);
5)将引导代理与尾随代理按前后顺序组合在一起成为“代理对”,就得到了增补平面字符的码元序列。
例如,增补平面中码点值为10437(字符名称为U+10437)的字符(𐐷):
1)0x10437减去0x10000,结果为0x00437,二进制为0000 0000 0100 0011 0111。
2)分拆成高10位值和低10位值两部分:0000000001(即0x0001)及0000110111(即0x0037)。
3)添加0xD800到高位值,以形成高位的引导代理:0xD800+0x0001= 0xD801(二进制为1101 1000 0000 0001)。
4)添加0xDC00到低位值,以形成低位的尾随代理:0xDC00+0x0037= 0xDC37(二进制为1101 1100 0011 0111)。
5)将高位的引导代理与低位的尾随代理按前后顺序组合在一起成为“代理对”,就得到了增补平面字符𐐷(字符名称为U+10437)的码元序列:1101 1000 0000 00011101 1100 0011 0111。
下表总结了该转换。不同的颜色表示码点值是如何被分布到UTF-16码元序列中的,而由UTF-16编码过程中加入的代理附加位则以不同的红色(亮红色与暗红色)显示:
显然,增补平面中的码点值从0x10000到0x10FFFF,共计0xFFFFF + 0x1个,即1,048,576个,刚好也就是需要20位来表示(2^20=1,048,576)。如果用两个16位长的码元组成的序列来表示,意味着引导代理要容纳上述20位中的前10位,尾随代理要容纳上述20位中的后10位。
另外,还要能够根据每个16位码元来直接判断该码元到底是属于引导代理(标志位为前6位11 0110,还剩下10位,因此总个数为2^10=1024个),还是属于尾随代理(标志位为前6位11 0111,也剩下10位,因此总个数也是2^10=1024个)。
为避免冲突,因此需要在基本多语言平面BMP中保留未定义Unicode字符的1024+1024=2048个码点,就可以容纳引导代理与尾随代理所需要的编号空间(码点空间、代码空间),也就是16个增补平面所需要的编号空间,共计1024*1024=2^20=1048576个码点。这BMP中的2048个码点对于BMP总计65536个码点来说,仅占3.125%(2048/65536=0.03125)。
在UTF-16编码方式中,引导代理的后面应该是一个尾随代理,而尾随代理的前面就应该是一个引导代理;不能出现一个引导代理的后面是一个非代理的普通UTF-16码元的情况,也不能出现一个引导代理的后面还是一个引导代理的情况。
UTF-16文本(字符串)的最后一个码元不能是引导代理,不允许出现一个尾随代理的前面是一个尾随代理的情况,也不允许出现一个尾随代理的前面是一个非代理的普通UTF-16码元的情况;UTF-16文本(字符串)的第一个码元不能是尾随代理。
而单独的一个代理码元(不管是引导代理还是尾随代理)是不合法的,代理必须以一个“引导代理+尾随代理”编码对(即代理对)的形式出现。
(笨笨阿林原创文章,转载请注明出处)
UTF-16的这种“代理对”编码规则保证了文本处理程序能够正确地访问和处理包括了基本平面和增补平面在内的全部UTF-16码元序列,并消除了基本平面字符和增补平面字符之间发生冲突的可能性。
因为引导代理和尾随代理码元被各自规定在一个特定范围内取值,所以很简单的一个原则就是:凡是在代理编码范围内的码元就是“代理”增补平面SP字符的“代理码元”,否则就是“基本平面BMP字符的码元”。由于BMP中的字符码元和代理码元分别在各自独立的编码范围内进行编码,所以对于一个符合格式规范的UTF-16码元来讲,它必须满足以下三个条件之一:
非代理码元(BMP字符码元)必须避开代理码元所占用的范围0xD800~0xDFFF(二进制为1101 1000 0000 0000 ~ 1101 1111 1111 1111,共2048个);
引导代理必须是代理对中的第一个码元;尾随代理必须是代理对中的第二个码元。
在处理UTF-16文本时,为了确保文本数据的完整性,绝对不能把任意一个代理从代理对中拆出来,也不能在代理对中间插入另一个字符的码元或码元序列。
在UTF-16编码方式里面,一个Unicode字符码点值由一个或两个16位码元编码。所以,如果想在一个UTF-16码元序列里面判断某个码元是属于哪个字符的话,就需要检查那个码元的值,然后根据码元的类型(是否具有代理标志位)决定是否还需要向前或向后检查一个相邻的码元的值(可以不必理会除了前后相邻的两个码元之外的其他码元)。
由于引导代理、尾随代理、BMP字符码元,三者互不重叠,搜索就很简单,这意味着UTF-16具有”自同步”(self-synchronizing)性:通过仅检查一个码元就可以判断当前字符的下一个字符的起始码元,每个字符码元的边界很明确;同时,还具有“非传递”性:单独的一个UTF-16码元出错涉及的只是一个字符,不会传递到文本的其他部分去,因此,即使文本中某些字符数据遭到破坏,其影响也只是局部性的。
UTF-8也有类似优点。但许多早期的编码方式就不是自同步的,比如大多数的多字节编码标准如GBK、Big5等,必须从头开始分析文本才能确定不同字符的码元的边界;也不具有非传递性,局部字符数据被破坏,很可能传递到整个文件,导致整个文件无法正确显示。
因此,UTF-8和UTF-16编码方式所具有的“自同步性”、“非传递性”等特点除了增强抗干扰能力外,也提供了随机访问的能力。
由于在大多数的文本数据中,代理对(增补字符码元序列)出现的概率是很小的,很多情况下处理的还是非代理码元(即BMP字符码元),导致许多软件处理代理对的部分往往得不到充分的测试。这导致了一些长期的bug与潜在安全漏洞,甚至广为流行、得到良好评价的优秀软件也是如此。
因此,虽然编程时同时考虑文本中可能出现的不同存储长度的字符(BMP有效字符是单16位编码,即单码元编码;增补字符是双16位编码,即双码元编码)并相应做出不同的处理,会比单纯只考虑16位编码在性能上要逊色一些。但实际上,现有的遵循定长16位编码规范但不能处理代理对的程序只需做很小的一点修改就可以同时处理BMP有效字符和增补字符的编码了。
另外,需要特别注意的是,虽然Unicode标准规定BMP代理区(U+D800~U+DFFF)的码点值不对应于任何字符,即未作定义,但是在使用UCS-2的时代,U+D800~U+DFFF是被定义了的,也就是已经用于某些字符了。不过,只要不是恰好构成了代理对,许多程序还是能把这些不匹配Unicode标准的字符码元正确地辨识、转换成合规的码元。这种由历史原因造成的码元序列按现在的Unicode标准来看,应算作是编码错误。
刨根究底字符编码之十五——UTF-32编码方式
UTF-32在UTF目前常用的三种编码方式(UTF-8、UTF-16、UTF-32)中,是最为简单的一种编码方式。UTF-32编码方式不使用任何编码算法将Unicode字符码点值(即编号字符集CCS中的字符编号)转换为码元序列,而是将每个Unicode字符码点值直接表示为一个32位的码元序列。
因此,目前UTF-32是一种固定宽度(也称为等宽、等长或定长)码元序列的Unicode字符编码方式。
UTF-32中的码元由32位组成。UTF-32使用的32位码元足够大,目前Unicode字符集中所收录的每个字符的码点值都可直接映射为单个码元。
换言之,UTF-32使用一个32位的码元序列来表示Unicode字符(严格地说,是单个32位的码元,并没有形成两个或两个以上码元所组成的码元序列,除非未来Unicode码点值扩展到64位,这样才可能出现由两个32位的码元所组成的序列)。
因此,即使是ASCII字符,同样需要占用32位(即四个字节)。这在三大UTF编码方式中无疑是最为浪费存储空间的;不过,由于UTF-32是定长编码(UTF-8和UTF-16都是变长编码),因此在文本处理速度上又是三大UTF编码方式中最快的。
(笨笨阿林原创文章,转载请注明出处)
由于UTF-32直接以四个字节的码元来表示码点值,这样按目前的情况来看,UCS-4或Unicode增补平面SP中的所有码点值就都可以完全直接表示,而无需像UTF-16那样使用复杂的代理算法来间接表示。
当然,如前所述,Unicode字符集是一个在不断增加字符的开放字符集,如果未来Unicode字符集的字符编号(即码点值)超过了四个字节,则UTF-32可能也需要像UTF-16一样使用某种特殊编码算法来间接表示。不过,按目前情况来看,真到了那一天,UTF-32编码方式可能也已经完全淘汰了。
与UTF-16类似,作为逻辑意义上的UTF-32码元序列,由于历史的原因,在映射为物理意义上的字节序列时,也分为UTF-32BE大端序、UTF-32LE小端序两种编码模式,因此UTF-32也同样需要使用BOM。
比如,“ABC”这三个字符的UTF-32码元序列为:00 00 00 41 00 00 00 42 00 00 00 43;其对应的各种字节序列如下:
每个UTF-32码元的值与Unicode码点的值完全相同,但其字节序列因字节序的不同而表现为有相同也有不同。
由于UTF-32在三大UTF编码方式中,既不是最早推出的编码方式(最早推出的是UTF-16),也不是最优设计的编码方式(公认为最优设计的是UTF-8),因此在实践中使用得最少,目前几乎已处于淘汰状态。
(笨笨阿林原创文章,转载请注明出处)
版权声明:本文为CSDN博主「sinolover」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sinolover/article/details/109497582
当用一个软件(比如Windows记事本或Notepad++)打开一个文本文件时,它要做的第一件事是确定这个文本文件究竟是使用哪种编码方式保存的,以便于该软件对其正确解码,否则将显示为乱码。
一般软件确定文本文件编码方式的方法有如下三种:
检测文件头标识;
提示用户手动选择;
根据一定的规则自行推断。
文件头标识一般指的是字节顺序标记BOM(Byte Order Mark),位于文件的最开始。当打开一个文本文件时,就BOM而言,有如下几种情形:
BOM为:EF BB BF ——表示编码方式为UTF-8;
BOM为:FF FE ——表示编码方式为UTF-16LE(小端序);
BOM为:FE FF ——表示编码方式为UTF-16BE(大端序);
BOM为:FF FE 00 00 ——表示编码方式为UTF-32LE(小端序);
BOM为:00 00 FE FF ——表示编码方式为UTF-32BE(大端序);
没有BOM——要么显式地提示用户手动选择一种编码方式,要么隐式地由软件按规则自行推断出编码方式。
接下来,是见证诡异怪事的时刻。
当你在简体中文版的Windows记事本里新建一个文件,输入“联通”两个汉字之后,保存为一个txt文件。然后关闭,再次打开该txt文件后,你会发现刚才输入并保存的“联通”两个汉字竟然莫名其妙地消失了,取而代之的是几个乱码。如下图所示。
这是为什么呢?难道是微软跟联通有仇吗?
原来,当你用Windows记事本新建一个文本文件时,其编码方式默认为ANSI编码(在简体中文版Windows中实际为GBK编码),没有BOM。
(注:Windows系统中的ANSI编码指的是在区域设置中所设置的系统默认编码方式,在简体中文版Windows系统中指的是GBK,即CP936代码页,具体可参看前文《刨根究底字符编码之七——ANSI编码与代码页》)
(笨笨阿林原创文章,转载请注明出处)
在这种编码方式下,该文本文件仅仅保存了“联通”两个汉字的GB内码的四个字节,如下所示(左边为十六进制,右边为二进制)。
c1 1100 0001
aa 1010 1010
cd 1100 1101
a8 1010 1000
通过Notepad++的HEX-Editor插件可查看内码(十六进制),如下图所示。
通过UltraEdit的“十六进制编辑”模式也可查看内码(十六进制),如下图所示。
当用记事本再次打开该文本文件时,由于没有BOM,记事本又没有提供显式地提示用户手动选择编码方式的功能,于是就只能隐式地按其推断规则自行推断,推断的结果就是被误认为了这是一个UTF-8编码方式的文件。
为什么会推断错误呢?又为什么会将其编码方式错误地推断为UTF-8呢?
注意,“联通”两个汉字的GB内码,其第一第二个字节的起始部分分别是“110”和“10”,第三第四个字节的起始部分也分别是“110”和“10”,这刚好符合了UTF-8编码方式里的两码元序列的编码算法规则(即与UTF-8的两码元序列“110xxxxx 10xxxxxx”中的前缀码“110”和“10”刚好是完全一致的;详见本系列文章中《刨根究底字符编码之十二——UTF-8究竟是怎么编码的》一文的介绍)。
让我们按照UTF-8的编码算法规则,将第一个字节的前缀码110去掉,得到“00001”,将第二个字节的前缀码10去掉,得到“101010”,将两者组合在一起,得到“00001101010”,再去掉多余的前导的0,就得到了“0110 1010",这正好是Unicode字符集里的U+006A,也就是小写字母“j”的码点值。
同理,之后的第三个字节与第四个字节按同样的方法用UTF-8解码之后正好是Unicode字符集里的U+0368,这个字符为“ͨ”(抱歉,这里的左双引号貌似被这个字符所影响,看起来像是半角左双引号,而无法正常显示为全角左双引号),很像是上标的一个小c,这应该是个组合字符(组合字符是Unicode字符集中的一种特殊字符,必须与其他字符组合在一起以形成一个新字符,一般不单独使用,可参看本系列文章前面相关文章中的介绍)。
这就是只有“联通”两个汉字的文本文件没有办法在记事本里被正确解码显示的原因。这里要特别说明的是,在记事本里打开时显示的不是“j”和“ͨ”,而是显示为了“��ͨ”(注意右上角是“ͨ”)。
而用UltraEdit打开,如果在设置中选择了“自动检测UTF-8文件”,显示的是“j”和“ͨ”组合在一起的字符“jͨ”。注意这个字符不是小写字母“j”,而是小写字母“j”上面的点变成了一个上标的小c,因为U+0368这个字符“ͨ”应该是个组合字符,与其前面的小写字母“j”组合在一起而形成了一个新字符——jͨ(再次提醒注意小写字母“j”上面的点变成了“c”)。
(注意:在UltraEdit的早期版本中,没有“自动检测UTF-8文件”这一选项)
这里还有一个问题:既然已经推断为了UTF-8,那为什么Windows记事本还是将前两个字节,亦即原本为“联”字的GB内码的那两个字节,显示为了“��”这样的乱码,而不是显示为小写字母“j”呢?
我想主要是因为小写字母“j”属于ASCII字符,在UTF-8编码中ASCII字符属于单字节编码,出现在双字节编码中是非正常的,因而被Windows记事本认为是错误编码,而UltraEdit则作了容错处理,仍然将其解读为了小写字母“j”。
而后两个字节,亦即原本为“通”字的GB内码的那两个字节,之所以Windows记事本将其按UTF-8编码的规则解读为了字符“ͨ”,那是因为字符“ͨ”的UTF-8编码正好就是双字节编码,因此按UTF-8编码的规则去解读的话不属于错误。
(笨笨阿林原创文章,转载请注明出处)
其实,用记事本默认的编码方式(ANSI)分别单独保存“联”字和“通”字为两个独立的txt文件,则:
1)再用记事本打开时,“联”字显示的是“��”,“通”字显示的是“ͨ”;
2)用UltraEdit打开时,
(1)如果选择了“自动检测UTF-8文件”,“联”字显示的是小写字母“j”,“通”字显示的“ͨ”(不过看不清,我开始还以为是个空格);
(2)如果没有选择“自动检测UTF-8文件”,“联”字和“通”字均能正常显示(说明这种情况下UltraEdit正确地推断出了编码方式为GBK,从这一点来看,UltraEdit比Windows记事本要强);
3)用NotePad++打开时,
(1)如果在“格式”中选择的是“以ANSI格式编码”(亦即显式地手动选择了正确的编码方式),“联”字和“通”字均能正常显示;
(2)如果编码方式选择的是UTF-8、UTF-8无BOM、UCS-2 Big Endian或UCS-2 Little Endian时,则“联”字均显示为“xC1xAA”(有意思的是,直接复制“xC1xAA”然后粘贴到Word里,则显示为了小写字母“j”),“通”字均显示为“ͨ”。
而如果是用记事本默认的编码方式(ANSI)保存“联通通信”四个字,则用记事本、UltraEdit(即便选择的是“自动检测UTF-8文件”的情况下)打开后都可正常显示。
这充分说明,Windows记事本在文件头没有BOM的情况下,只能自行推断,由于“联通”两个汉字保存为ANSI编码方式时,内码只有四个字节,在信息不够充足的情况下(尤其是其内码又刚好符合了UTF-8的编码算法规则),于是被错误地推断为了UTF-8编码方式;当以ANSI编码方式保存的是“联通通信”四个汉字时,内码有八个字节,这时信息较为充足,因此被正确地推断为了ANSI编码方式(在简体中文版Windows中ANSI编码默认为GBK编码)。
上面分析的是Windows系统中采用ANSI编码时没有添加BOM的情况。那么,对于采用非ANSI编码时添加了BOM的情况,是否就万事大吉了呢?其实,添加BOM来标记字符编码表面看起来貌似不错,但实际上经常会带来麻烦,因为它和很多协议、规范并不兼容。
Windows里的软件在采用非ANSI编码时,即便对于根本不存在字节序问题的UTF-8编码默认也会添加BOM(详见之前文章《刨根究底字符编码之十一——UTF-8编码方式与字节序标记》的介绍),而像Unix、Linux、Mac OS等*nix系统对于UTF-8编码都默认不添加BOM。
既然*nix系统都可以不添加BOM,那为什么Windows系统却非要添加BOM呢?这很可能是因为Windows系统有大量普通用户使用,在必须兼容传统ANSI编码的情况下,从用户体验角度考虑而没有采用显式地要求用户手动选择字符编码方式的做法,因此特别依赖于通过BOM来防止隐式地自行推断字符编码方式而出错。
微软这种为了照顾广大普通用户而从用户体验角度出发“好心办坏事”的例子其实还有很多。
因此,在Windows系统中,尽量不要使用记事本来打开并编辑文本文件,尤其是作为程序员,应使用Notepad++或UltraEdit等更为专业的文本文件编辑软件。
这一方面是可以避免出现上述这样的“诡异”错误,另一方面也是为了避免Windows记事本“多此一举”地添加BOM(详见下面附文中的解释),从而给在与其他系统(比如*nix系统)交流时带来不必要的麻烦。
附:Windows记事本中对常用编码方式自行其是的“奇葩”命名
Windows记事本中,对常用编码方式的命名非常“奇葩”,微软这种自行其是的非标准命名,乍一看令人费解,现解释如下。
1)ANSI指的是对应当前系统区域设置(即系统locale)中的默认ANSI编码,不带BOM。在简体中文版Windows系统中默认ANSI编码指的就是GBK编码,即CP936,具体可参看前文《刨根究底字符编码之七——ANSI编码与代码页》。
2)Unicode指的是带有BOM的小端序UTF-16(即UTF-16LE with BOM)。
3)Unicode big endian指的是带有BOM的大端序UTF-16(即UTF-16BE with BOM)。
4)UTF-8指的是带有BOM的UTF-8(即UTF-8 with BOM)。UTF-8编码方式实际上并不存在字节序的问题,之所以仍然“多此一举”地添加BOM,应该是由于要兼容不添加BOM的ANSI编码,从用户体验角度考虑,避免用户显式地手动选择编码方式。
(注:如果UTF-8编码不添加BOM,则有两种不添加BOM的编码方式,从而导致隐式地自行推断编码方式更容易出错,上文所介绍的对“联通”推断出错即是明证。当然反过来也说明了Windows记事本对于不添加BOM的UTF-8编码其实同样是支持的,而并非简单粗暴地直接提示错误,这应该是为了兼容*nix系统不添加BOM的做法而不得不采取的策略。只是这样一来,就很难避免陷入左右为难的困境。)
彻底搞懂PYTHON的字符编码
前言:中文编码问题一直是程序员头疼的问题,而Python2中的字符编码足矣令新手抓狂。本文将尽量用通俗的语言带大家彻底的了解字符编码以及Python2和3中的各种编码问题。
一、什么是字符编码。
要彻底解决字符编码的问题就不能不去了解到底什么是字符编码。计算机从本质上来说只认识二进制中的0和1,可以说任何数据在计算机中实际的物理表现形式也就是0和1,如果你将硬盘拆开,你是看不到所谓的数字0和1的,你能看到的只是一块光滑闪亮的磁盘,如果你用足够大的放大镜你就能看到磁盘的表面有着无数的凹凸不平的元件,凹下去的代表0,突出的代表1,这就是计算机用来表现二进制的方式。
1.ASCII
现在我们面临了第一个问题:如何让人类语言,比如英文被计算机理解?我们以英文为例,英文中有英文字母(大小写)、标点符号、特殊符号。如果我们将这些字母与符号给予固定的编号,然后将这些编号转变为二进制,那么计算机明显就能够正确读取这些符号,同时通过这些编号,计算机也能够将二进制转化为编号对应的字符再显示给人类去阅读。由此产生了我们最熟知的ASCII码。ASCII 码使用指定的7 位或8 位二进制数组合来表示128 或256 种可能的字符。这样在大部分情况下,英文与二进制的转换就变得容易多了。
2.GB2312
然而,虽然计算机是美国人发明的,但是全世界的人都在使用计算机。现在出现了另一个问题:如何让中文被计算机理解?这下麻烦了,中文不像拉丁语系是由固定的字母排列组成的。ASCII 码显然没办法解决这个问题,为了解决这个问题中国国家标准总局1980年发布《信息交换用汉字编码字符集》提出了GB2312编码,用于解决汉字处理的问题。1995年又颁布了《汉字编码扩展规范》(GBK)。GBK与GB 2312—1980国家标准所对应的内码标准兼容,同时在字汇一级支持ISO/IEC10646—1和GB 13000—1的全部中、日、韩(CJK)汉字,共计20902字。这样我们就解决了计算机处理汉字的问题了。
3.Unicode
现在英文和中文问题被解决了,但新的问题又出现了。全球有那么多的国家不仅有英文、中文还有阿拉伯语、西班牙语、日语、韩语等等。难不成每种语言都做一种编码?基于这种情况一种新的编码诞生了:Unicode。Unicode又被称为统一码、万国码;它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。Unicode支持欧洲、非洲、中东、亚洲(包括统一标准的东亚象形汉字和韩国表音文字)。这样不管你使用的是英文或者中文,日语或者韩语,在Unicode编码中都有收录,且对应唯一的二进制编码。这样大家都开心了,只要大家都用Unicode编码,那就不存在这些转码的问题了,什么样的字符都能够解析了。
4.UTF-8
但是,由于Unicode收录了更多的字符,可想而知它的解析效率相比ASCII码和GB2312的速度要大大降低,而且由于Unicode通过增加一个高字节对ISO Latin-1字符集进行扩展,当这些高字节位为0时,低字节就是ISO Latin-1字符。对可以用ASCII表示的字符使用Unicode并不高效,因为Unicode比ASCII占用大一倍的空间,而对ASCII来说高字节的0对他毫无用处。为了解决这个问题,就出现了一些中间格式的字符集,他们被称为通用转换格式,即UTF(Unicode Transformation Format)。而我们最常用的UTF-8就是这些转换格式中的一种。在这里我们不去研究UTF-8到底是如何提高效率的,你只需要知道他们之间的关系即可。
总结:
**1.为了处理英文字符,产生了ASCII码。
2.为了处理中文字符,产生了GB2312。
3.为了处理各国字符,产生了Unicode。
4.为了提高Unicode存储和传输性能,产生了UTF-8,它是Unicode的一种实现形式。**
二、Python2中的字符编码
1.Python2中默认的字符编码是ASCII码,也就是说Python在处理数据时,只要数据没有指定它的编码类型,Python默认将其当做ASCII码来进行处理。这个问题最直接的表现在当我们编写的python文件中包含有中文字符时,在运行时会提示出错。如图:
这个问题出现的原因是:Python2会将整个python脚本中的内容当做ASCII码去处理,当脚本中出现了中文字符,比如这里的“小明”,我们知道ASCII码是不能够处理中文字符的,所以出现了这个错误。解决的办法是:在文件头部加入一行编码声明,如图:
这样,Python在处理这个脚本时,会用UTF-8的编码去处理整个脚本,就能够正确的解析中文字符了。
2.Python2中字符串有str和unicode两种类型。
上图中展现出了Python2中字符串的两种类型:
name变量被赋予了一个字符串“小明”;
unicode_name是name变量的unicode格式,这里我们使用了decode()方法,我们会在后面的内容中详细讲解;
两者在终端中返回了不同的字节串,type返回了不同的数据类型,但print打印出了相同的输出。
这里我们注意到一个“字节串”的名称,字节串是指该字符串在python中的标准形式,也就是说无论一个字符串是什么样的编码,在python中都会有一串字节串来进行表示。字节串是没有编码的,对应的最终交给计算机处理的数据形式。
3.Python2中可以直接查看到unicode的字节串。
在上图中,输入unicode_name的返回值,是一个unicode字节串,我们能够直接看到这个字节串。而在python3中,我们将不能直接看到unicode字节串,它会被显示为中文的“小明”;因为python3默认使用unicode编码,unicode字节串将被直接处理为中文显示出来。
总结:
**1.Python2中默认的字符编码是ASCII码。
2.Python2中字符串有str和unicode两种类型。str有各种编码的区别,unicode是没有编码的标准形式。
3.Python2中可以直接查看到unicode的字节串。**
三、decode()与encode()方法
前面我们说了这么多都是为了这一节做铺垫,现在我们开始来处理Python2中的字符编码问题。我们首先要学习Python为我们提供的两个转换编码的方法decode()与encode()。
***decode()方法将其他编码字符转化为Unicode编码字符。
encode()方法将Unicode编码字符转化为其他编码字符。*
话不多说,直接上图:
chardet模块可以检测字符串编码,没有该模块的可以用pip install chardet安装。
首先解释一下为什么name=”小明” 这里的小明是一个utf-8编码的字符。因为我使用的是Ubuntu14.04操作系统,系统默认的字符编码就是UTF-8,所以当我在终端将一个中文输入时,系统就会自动将这个中文字符以UTF-8的编码传递给Python。所以如果你的系统是windows操作系统,而大多数情况下windows的系统编码默认是gb2312,那么在windows下做上图的测试“小明”这个字符就是gb2312编码。
上图中我们将utf-8编码的name通过decode()方法转换为unicode_name,然后通过encode()方法将unicode_name转换为gb2312_name。这时我们再用print去输出gb2312编码的字符时缺产生了一个奇怪的输出。这是因为我的操作系统使用的是UTF-8编码,对于gb2312编码的字符自然不能够正确解析,如果我们将该gb2312的字节串放在windows下输出就能够得到我们想要的中文,如图:
所谓乱码本质上是系统编码与所提供字符的编码不一致导致的,我们举一个例子:
小明的电脑中存了一个utf-8的字母A,存储在计算机中是1100001;
小红的电脑中也存了一个gb2312的字母A,存储在计算机中是11000010;
当小明与小红交换信息时,各自的计算机就不会把对方传递过来的A识别为字母A,可能认为这是字母B。
所以当我们需要操作系统正确的输出一个字符时,除了要知道该字符的字符编码,也要知道自己系统所使用的字符编码。如果系统使用的是UTF-8编码,处理的却是gb2312的字符就会出现所谓“乱码”。
一个Tips:decode()方法与在字符串前加u的方法实现的效果相同比如u’小明’
总结:
1.Python2的对于字符编码的转换要以unicode作为“中间人”进行转化。
2.知道自己系统的字符编码(Linux默认utf-8,Windows默认GB2312),对症下药。
四、一个字符编码的例子
在Linux操作系统下使用python2下获取网易首页的title,并以正确的中文显示出来。
163的首页使用的字符编码是gb2312,而我们前面提到过Linux下的默认字符编码为UTF-8,我们测试一下直接提取会不会出现乱码问题。
我们发现确实提取到的title并不能正确显示,因为网页中已经声明了它是一个gb2312的字符编码,而我的系统中默认的字符编码为UTF-8显然,我必须要将title转换为UTF-8的字符。
其实由于utf-8属于unicode字符编码,在Linux中我们可以直接打印出unicode编码的字符。如:
现在我们在Windows用Python2来做另一个实验,这次我们换成百度首页的title:
这次我们发现网页上的字符编码为utf-8,那么我在Windows下会不会出现乱码:
所以我们再次强调:乱码本质上是系统编码与所提供字符的编码不一致导致的
在Pyhon3中字符编码有了很大改善最主要的有以下几点:
1.Python 3的源码.py文件 的默认编码方式为UTF-8,所以在Python3中你可以不用在py脚本中写coding声明,并且系统传递给python的字符不再受系统默认编码的影响,统一为unicode编码。
2.将字符串和字节序列做了区别,字符串str是字符串标准形式与2.x中unicode类似,bytes类似2.x中的str有各种编码区别。bytes通过解码转化成str,str通过编码转化成bytes。
PS:有一个小问题被许多新手所困扰,我们来看一下图片:
我们看到当一个中文字符出现在一个list(或tuple、dict)中时,它并不会被显示为一个中文而是字节串。但当该字符串从list中提取出来再print时就能够正常显示为中文。字节串是所有字符在python中的“本质”形态,所以你可以简单的理解为list中呈现出的字节串是给计算机看的。
python处理字符编码(原理篇)
一些关于终端的实验
代码页
Unicode
Unicode 和 utf8 之间的转换
文件的字符编码检测
一些关于终端的实验
首先先做个小实验,回答上篇两个简单的问题:
文件读写接口的具体不同?
文本分段fwrite,会不会乱码?
#include "stdafx.h"
#include<stdio.h>
#include <string.h>
#define INPUT_MAX 8
int _tmain(int argc, _TCHAR* argv[])
{
int input_len;
int read_len = 100;
char buffer[1024];
char *data;
char *prompt = ">>> ";
fprintf(stdout, "%s", prompt);
while ((data = fgets(buffer, read_len, stdin))!= NULL)
{
input_len = strchr(data, '\0')- data;
fprintf(stdout, "get your input!\n");
fprintf(stdout, "共输入%d个字符,它们是:", input_len);
while (input_len > INPUT_MAX){
const int chunk_size = INPUT_MAX;
fwrite(data, 1, chunk_size, stdout);
data += chunk_size;
input_len -= chunk_size;
}
fwrite(data, 1, input_len, stdout);
fprintf(stdout, "逐字节输出:");
for (data = buffer; *data != '\0'; data++)
{
fputc(*data, stdout);
}
fprintf(stdout, "%s", prompt);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
这么一看,fprintf fwrite fputc 均支持中文显示。
所以在python中,尽管我们看到变量名+回车(repr)和print(str)两种方式的结果不同,看到了调用接口前者是fprintf+fputc,后者是fwrite,也不能想当然得认为输出接口的改变是造成打印差异的原因。
所以,还是回到逻辑层,我们得知这个差异其实是打印时指定 Py_PRINT_RAW 的锅。到底什么才是加工后输出,什么才是原生输出(Py_PRINT_RAW)?
这里简单总结:
变量名+回车方式(repr),加工后输出,倾向于把文本当成二进制数据看待,将编码显示出来,展示的是文本在内存中的存储方式。
原生输出方式(str),Py_PRINT_RAW,倾向于易于人类语言的方式,展示的是文本在终端设备上的显示方式。
至于将数据分批写入是否乱码,实验中fwrite和fputc都给出了答案,标准输出stdout的缓冲区解决了这个问题。说到这个,会想到用fflush来冲洗缓冲区来看看字符会不会分节,试过之后依然是失败的,感觉操作系统底层或许有什么保护机制。那至少我们知道控制台的字符编码是cp936,在这个终端上,我们输入的数据是cp936,输出也是cp936,理论上怎么搞都不会乱码的。果断我把中文存到gbk编码的txt中,把终端的默认编码改成如下,得到了我要的效果。
写到这里,乱码的原因有点眉目了,相连的文本数据只要没被破坏,哪怕分段输出也没问题。
但是,文本数据的编码,和终端设备(标准输出、浏览器、文件编辑器、SecureCRT控制台等)的显示字符编码,如果不一致,就会乱码。
我们控制台是cp936,尝试把它转成utf8打印,就乱码了。
果然如此,有点小激动,感觉我们快接近问题的核心了。
但是不要急,我们需要补充一些历史知识(冷知识):这个代码页是什么鬼?
代码页
code page,刚好对应我们常看到的cp936的前缀。根据维基百科的解释,代码页是对字符编码的古老的编号,主要是IBM、微软两家公司提出的规范。所以它对目前流行的的字符编码名称有映射关系,python中{PythonDir}\Lib\encodings\aliases.py 中的那个大表,就是这么来的。
代码页936(Codepage 936)是Microsoft的简体中文字符集标准。
aliases是“别名”的意思。 可想而知,在互联网诞生之前,字符编码没有统一规范的时候,世界各地的字符编码是多么的混乱不堪。 我们从aliases这张表能看出,一些地区的字符编码逐渐统一成了一张字符集,旧的名字被新的名字所替换。cp936就是gbk,或者换句话说,cp936字符集和gbk的字符集是一样的,给你一堆数字,告诉你它是cp936/gbk,你就能在字符集里查到这个数字对应的中文长啥样。
Unicode
顺藤摸瓜,我们也补一下字符编码的冷知识吧。我们需要知道的,字符集就像有版本号一样,内容是不断扩充的,新的版本包含旧的版本。比如 gb2312 < gbk < gb18030。
关于unicode,它像是一个通用数据结构,任何字符编码都可以通过一定规则换算成unicode编码,这种换算就是decode(解码、译码)。前面我们花了很多时间理解 gbk <-> unicode 之间的转换,可能还是不太能记住哪个才是encode,哪个才是decode。这里有个方法:
因为unicode是通用的编码方式,就好比是个通用世界语言一样,你可以理解成是公文。gbk的编码,实际上是服务于中文简体用户的(记住它对应一个简体中文字符集),对于其他语言的用户来说他们就读不懂了,就像是密文。所以:
gbk -> unicode = 解密 = decode
unicode -> gbk = 加密 = encode
还有一点要注意,unicode从拼写上看,union of code,是个概念、口号、通用字符编码规则,并不是某个具体的字符编码,它没有对应的字符集,unicode对象仅仅意味着,这个对象对应的内存中的二进制数据是字符串,它是哪个地区的语言,对应哪个编码,该怎么显示,并不知道。所以你看不到终端设备将字符串以unicode形式输出的(除非你是要那种加工的方式),所以python在print unicode的时候,它知道要从标准输出取其对应的字符编码,转码(encode)之后再输出,这样的输出才是人类语言。那你可能要问了,unicode有啥用呢?
它解决了一个关键问题:字符码(Code Point)和字符的一对一关系,这样我们就能正常获取字符长度,做字符串切割和拼接也不在话下。 '汉’字在gbk中是2个字节,在utf8中是3个字节,len返回的是字节数,而非字符数。
这就是str和unicode之间最重要的区别。尽管unicode的编码也是2个字节,但是unicode知道这2个字节是一起的,其他字符编码就做不到这个。
这个口号喊出来了,UTC就提出个标准:
USC-2:每个字符用2个字节表示。然后开始研发一张超大的字符集,utf16。
后来他们担心2个字节不够用,又提出了一个新标准:
USC-4:每个字符用4个字节表示。对应的字符编码utf32。
因为这两个规范都是hard code字节数。对于最简单的ascii,会有大量的无意义的0,对于存储、网络传输都是极大的浪费,于是就有了utf8,用变长字节数表示一个字符。即头字节中读取到连续的n个1B(一直读到0为止),那包括该字节在内一共有n个字节组合起来的二进制数据,表示一个字符。 在规范中,后续的n-1个字节都必须是10B开头(剩下6位才是有效的数据位)。这种数据头(head)的设计方式,就非常适用于网络传输了。 我们最多可以有7个字节表示一个字符,最多可以有 (2^((7-1)*6)-1=)68719476735 个字符纳入编码中,但实际情况,utf8只用了1-4个字节来表示字符。
head = 1111 1110
body = 1011 1111 1011 1111 1011 1111 1011 1111 1011 1111 1011 1111
Unicode 和 utf8 之间的转换
这里以汉字为例
unicode 0110 1100 0100 1001
utf8:
head = 1110 0110 //前面3个1,表示有3个字节表示这个字符
body = 1011 0001 1000 1001 //将加粗部分拼接起来,就得到unicode
具体规则看下表
面对gbk字符串的网络传输,我们可能要这样做
发送端:
msg = a.decode('gbk’).encode('utf8’)
sendmsg(msg)
接收端:
msg = recv()
a = msg.decode('utf8’).decode('gbk’)
也就是说,当我们面对乱码时,应该聚焦到这两个问题:
终端的字符编码格式是什么?
上文已移到,去设置里查。
字符串的原始字符编码是什么?这些子问题
2.1 怎么知道字符串的字符编码?
站在网络传输的接收端,按照utf8的规则能正常接收数据。但是传过来的不是utf8编码的字符串呢?这个问题似乎很难解决。
2.2 怎么知道文件的字符编码?
文件保存时有文件头、文件未的数据,可以用于辅助判断。
版权声明:本文为CSDN博主「Ron_Eureka」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jrl137824675/article/details/90547664
vim字符编码终极方案
vim经常遇到文件乱码的情况,很多的文章都只是解决了作者遇到的乱码问题,不同的使用者由于环境不一样,参考之后,反而更加混淆和复杂。
其实vim乱码是与系统环境非常相关的,一味执着于修改vim的配置而不知道分析系统的实际环境,往往导致混淆,本文从原理分析vim编码的设计和乱码原因,帮助所有的用户解决vim的乱码。
vim为何会出现乱码:
1.首先是输入,vim以错误的格式解析文件,比如原本是utf-8,但以ansi解析,那必然是乱码了。
2.然后是处理,vim以错误的格式处理文件,比如原本是utf-8,但内部以ansi保存处理,导致乱码。
3.然后是输出,vim输出显示的编码与系统不一致,也会乱码。
4.最后是写入,vim回写文件与打开的不一致,造成编辑后文件乱码。
vim与编码相关的参数:
1.fileencoding,用于配置打开文件和保存文件的编码,但只能有一个值,只适合少数文件都是同种编码的环境,所以一般不使用
2.fileencodings,从名字上看就知道是fileencoding的增强版,可以配置多种不同的编码,常见的配置为,配置好之后,列表中的文本编码只要合法,都能被vim正确的读取,建议配置:
set fileencodings=utf-bom,utf-8,gbk,gb2312,gb18030,cp936,latin1
3.encoding,vim内部编码,vim读取文件之后,但并不以读取文件的编码来处理,而是会转换成内部编码的格式,这个编码一般与操作系统相关,linux下utf-8居多,中文windows下则是gdk,建议配置:
set encoding=utf-8
4. termencoding,vim输出的编码,输出指输出到操作系统或命令终端等,默认与操作系统的语言编码一致,如果使用linux命令终端,建议终端和linux系统配置相同的编码,然后配置相同的termencoding,否则顾全了vim就顾不上shell,不过如果shell不存在中文名文件,则配置终端和termencoding一致即可,对于windows,能自动的识别gbk和utf-8,不用特殊配置,建议配置:
set termencoding=utf-8
5.fileformats,用于区分操作系统,主要是回车\r\n的区别,建议配置:
set fileformats=unix,dos
在分析了乱码原因,了解了vim编码的参数之后,就可以根据实际情况来配置了
1.分析文件编码,配置vim文件文件解析编码fileencodings,让vim能解析出来
2.分析系统编码,配置vim内码encoding,如果linux系统语言为ansi,则必须配置内码,否则vim无法处理中文,中文windows下vim内码为gbk,但还是建议统一配置为utf-8
3.根据输出配置显示编码,linux系统如果使用了putty或者SecureCRT,需要注意配置termencoding和终端软件一致,windows系统比较少有终端软件,系统能自动解析gdb和utf-8,建议统一配置为utf-8
参考的vim编码配置如下,linux和windows配置相同,linux系统语言编码和ssh终端编码配置为utf-8,windows则不需要配置,即可正常的打开utf-8,gdk等编码的文件