在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),而8个二进制位(bit)组合称为一个字节(Byte),所以一个字节能够组合出256中状态,即从00000000到11111111。
1.标准ASCII
ASCII码使用指定的7位或8位二进制数组合来表示128或256种可能的字符。标准ASCII码也叫基础ASCII码,使用7位二进制数来表示所有的大写和小写字母,数字0到9、标点符号,以及在美式英语中使用的特殊控制字符。其中:
0-31及127(共33个)是控制字符或通信专用字符(其余为可显示字符),如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等;通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等;ASCII值为8、9、10和13分别转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响。
32-126(共95个)是字符(32是空格),
48-57为0到9十个阿拉伯数字。
65-90为26个大写英文字母,
97-122号为26个小写英文字母,
其余为一些标点符号、运算符号等。
在标准ASCII中,其最高位(b7)用作奇偶校验位。所谓奇偶校验,是指在代码传送过程中用来检验是否出现错误的一种方法,一般分奇校验和偶校验两种。奇校验规定:正确的代码一个字节中1的个数必须是奇数,若非奇数,则在最高位b7添1;偶校验规定:正确的代码一个字节中1的个数必须是偶数,若非偶数,则在最高位b7添1。
2.扩展ASCII
一个字节中的后7位总共只能表示128个不同的字符,英语用这些字符已经足够了,可是要表示其他语言却是不够。比如,在法语中,字母上方有注音的符号,就无法用ASCII表示。于是,一些国家就利用了字节中闲置的最高位编入新的符号。这样一来,就可以表示最多256个符号,这就是扩展的ASCII 。
3.GB2312, GBK
后来,中国也引进了计算机,发现常用的汉字有6000多个,但是在ASCII编码方案中的所有字符已经被使用殆尽。此时,GB2312编码应运而生。GB2312编码方案规定:两个范围在0x80~0xFF 的字符表示一个汉字。0x00~0x7F之间的字符,依旧是1个字节代表1个字符。,但两个大于127的字符连在一起时,就表示一个汉字。这样就可以组合出大约7000多个简体汉字。注意上面提到的包含欧洲符号的扩展ASCII码在GB2312中也就不复存在了。
后来,使用GB2312编码方案还是无法表示某些字,于是干脆不再要求低字节一定是0x80~0xFF范围内的字符,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。这既是GBK编码,GBK 包括了 GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。
在解码的时候,一次读取一个字节的内容,看一下该字节的最高位是否为1(说明范围在0x80~0xFF),如果为1,暂存该字节,并读取下一个字节,这样将两个字节合并然后去查询对应的字符;如果第一次读到的一个字节最高位为0,那么就按此字节的内容直接查询传统的ASCII码表,找到对应的字符。
4.ANSI
为在windows系统中ANSI并不是某一种特定的字符编码,而是在不同语言的系统中,ANSI表示不同的编码。如在简体中文Windows操作系统中,ANSI编码代表GBK编码;在日文Windows操作系统中,ANSI编码代表Shift_JIS编码。所以你用ANSI格式的txt文档存储的中文在其他语言系统中会出现乱码。微软用一个叫“Windows code pages”(在命令行下执行chcp命令可以查看当前code page的值)的值来判断系统默认编码,比如:简体中文的code page值为936(它表示GBK编码,win95之前表示GB2312,详见:Microsoft Windows' Code Page 936),繁体中文的code page值为950(表示Big-5编码)。
这里还需要提一下ANSI背景下的全角和半角的概念:全角是指中由两个字符表示的各种符号。半角是指英文ASCII码中的各种符号。
5.Unicode
由于各个国家的不同编码标准,导致相互之间谁也理解不了对方的编码,所以当时是必须安装对应的字符系统才能解读存储的内容。之后ISO (国际标准化组织)的国际组织决定着手解决这个问题,制定了Unicode字符集,涵盖了目前人类使用的所有字符,并为每个字符进行统一编号,分配唯一的字符码。
需要注意的是,广义上的Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。比如,汉字 “严” 的unicode是十六进制数0x4E25,转换成二进制数足足有15位(1001110 00100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
Unicode的最初目标,是用1个16位的编码来为超过65000字符提供映射。但这还不够,它不能覆盖全部历史上的文字,也不能解决传输的问题(implantation head-ache's),尤其在那些基于网络的应用中。已有的软件必须做大量的工作来处理16位的数据。 因此,Unicode用一些基本的保留字符制定了三套编码方式。它们分别是UTF-8, UTF-16和 UTF-32。正如名字所示,在UTF-8中,字符是以8位序列来编码的,用一个或几个字节来表示一个字符。这种方式的最大好处,是UTF-8保留了ASCII字符的编码做为它的一部分,例如,在UTF-8和ASCII中,“A”的编码都是0x41. UTF-16和UTF-32分别是Unicode的16位和32位编码方式。考虑到最初的目的,通常说的Unicode就是指UTF-16。
注意Unicode背景下无论是半角的英文字母,还是全角的汉字,它们都是统一的“一个字符”,也就是统一的“两个字节"。
但是Unicode还是存在一些问题:
如何才能区别Unicode和ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?
英文字母只用一个字节表示就够了,如果Unicode统一规定,每个符号用两个字节表示,那么每个英文字母前都必然有一字节是0x00,这对于存储来说是极大的浪费,纯英文的文本文件的大小会因此大出一倍,这是无法接受的。
UNICODE与GBK等两字节编码完全不兼容,无法找到一种简单的方式转换(只能使用查找表的方式)。
6.Unicode 和 UCS
根据维基百科全书的记载,历史上存在两个试图独立设计Unicode的组织,即国际标准化组织(ISO)和一个软件制造商的协会(unicode.org)。ISO开发了ISO 10646项目,Unicode协会开发了Unicode项目。
在1991年前后,双方都认识到世界不需要两个不兼容的字符集。于是它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode2.0开始,Unicode项目采用了与ISO 10646-1相同的字库和字码。
Unicode在编码上和UCS保持一致,在实现上有自己的规则,而UCS只定义了编码标准。Unicode的实现形式上 有UTF-8,UTF-16,UTF-32,还有UTF-7等。UCS编码也有自己的格式:UCS-2和UCS-4等等。
Unicode的编码可以和UCS-2和UCS-4保持一致。但是又略有不同。UTF-16是UCS-2的扩展,UTF-32是UCS-4的子集。也就是说,UTF-16的实现上对code point的支持范围超过UCS-2,而UTF-32对code point的表示却又在UCS-4的范围之内(下面第7点也会说到这个方面)。
UCS只是规定如何编码,并没有规定如何传输、保存这个编码。例如“汉”字的UCS编码是 0x6C49,我可以用4个ASCII数字来传输、保存这个编码;也可以用utf-8编码:3个连续的字节0xE6 0xB1 0x89来表示它。关键在于通信双方都要认可。UTF-8、UTF-7、UTF-16都是被广泛接受的方案。UTF-8的一个特别的好处是它与ISO-8859-1完全兼容。UTF是 “UCS Transformation Format”的缩写。
7.UCS-2 UCS-4 和 BMP
Unicode是为整合全世界的所有语言文字而诞生的。任何文字在Unicode中都对应一个值, 这个值称为代码点(code point)。代码点的值通常写成 U+ABCD 的格式。 而文字和代码点之间的对应关系就是UCS-2(Universal Character Set coded in 2 octets)。
顾名思义,UCS-2是用两个字节来表示代码点,其取值范围为 U+0000~U+FFFF。为了能表示更多的文字,人们又提出了UCS-4,即用四个字节(实际上只用了31位,最高位必须为0)表示代码点。 它的范围为 U+00000000~U+7FFFFFFF,其中 U+00000000~U+0000FFFF和UCS-2是一样的。
要注意,UCS-2和UCS-4只规定了代码点和文字之间的对应关系,并没有规定代码点在计算机中如何存储。 规定存储方式的称为UTF(Unicode Transformation Format),其中应用较多的就是UTF-16和UTF-8了。
UCS-4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个plane。每个plane根据第3个字节分为256行 (rows),每行包含256个cells。当然同一行的cells只是最后一个字节不同,其余都相同。group 0的plane 0被称作Basic Multilingual Plane, 即BMP。或者说UCS-4中,高两个字节为0的码位被称作BMP。
将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。在UCS-2的两个字节前加上两个零字节,就得到了UCS-4的BMP。而目前的UCS-4规范中还没有任何字符被分配在BMP之外。
Unicode最初支持16位的code point,后来发现不够用,于是用UTF-16扩展UCS-2。在BMP区域内的一片连续空间(U+D800~U+DFFF)的码位区段是永久保留不映射到字符(至于为什么请看下面第9条),因此UTF-16利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。具体算法参考wiki:utf-16 .
所以,utf-16能表示的范围最大能到U+10FFFF,包含1个基本平面(BMP)和16个辅助平面。
理论上UCS-4编码范围能达到U+7FFFFFFF,但是因为Unicode和ISO达成共识,只会用17个平面内的字符,所以UTF-32是UCS-4的子集。但是UTF-16是定长的编码,和UCS-4无论实现和编码都是基本一样的。
当前,Unicode深入人心,且UTF-8大行其道,UCS编码基本被等同于UTF-16,UTF-32了,所以目前UCS基本谈出人们的视野中。(Windows NT用的就是UCS-2)
8.Unicode 的 Big Endian 和 Little Endian
Endian是CPU处理多字节数的不同方式。例如 “汉” 字的Unicode编码是 0x6C49。那么写到文件里时,究竟是将 0x6C 写在前面,还是将 0x49 写在前面?
如果将 0x6C 写在前面,就是Big Endian。如果将 0x49 写在前面,就是Little Endian。
9.UTF-16
UTF-16由RFC2781规定,它使 用两个字节来表示一个代码点。
UTF-16是完全对应于UCS-2的,即把UCS-2规定的代码点通过Big Endian或Little Endian方式 直接保存下来。UTF-16包括三种:UTF-16,UTF-16BE(Big Endian),UTF-16LE(Little Endian)。
UTF-16BE和UTF-16LE不难理解,而UTF-16就需要通过在文件开头以名为BOM(Byte Order Mark)的字符 来表明文件是Big Endian还是Little Endian。BOM为U+FEFF这个字符。
其实BOM是个小聪明的想法。由于UCS-2没有定义U+FFFE, 因此只要出现 FF FE 或者 FE FF 这样的字节序列,就可以认为它是U+FEFF, 并且可以判断出是Big Endian还是Little Endian。
举个例子。“ABC”这三个字符用各种方式编码后的结果如下:
UTF-16BE | 00 41 00 42 00 43 |
UTF-16LE | 41 00 42 00 43 00 |
UTF-16(Big Endian) | FE FF 00 41 00 42 00 43 |
UTF-16(Little Endian) | FF FE 41 00 42 00 43 00 |
UTF-16(不带BOM) | 00 41 00 42 00 43 |
Windows平台下默认的Unicode编码为Little Endian的UTF-16(即上述的 FF FE 41 00 42 00 43 00)。
另外,UTF-16还能表示一部分的UCS-4代码点——U+10000~U+10FFFF。 表示算法比较复杂,简单说明如下:
1.从代码 点U中减去0x10000,得到U'。这样U+10000~U+10FFFF就变成了 0x00000~0xFFFFF。
2.用20位二进制数表示U'。 U'=yyyyyyyyyyxxxxxxxxxx
3.将前10位和后10位用W1和W2表示,W1=110110yyyyyyyyyy,W2=110111xxxxxxxxxx,则 W1 = D800~DBFF,W2 = DC00~DFFF。
例如,U+12345表示为 D8 08 DF 45(UTF-16BE),或者08 D8 45 DF(UTF-16LE)。
但是由于这种算法的存在,造成UCS-2中的 U+D800~U+DFFF 变成了无定义的字符。
10.UTF-32
UTF-32用四个字节表示代码点,这样就可以完全表示UCS-4的所有代码点,而无需像UTF-16那样使用复杂的算法。 与UTF-16类似,UTF-32也包括UTF-32、UTF-32BE、UTF-32LE三种编码,UTF-32也同样需要BOM字符。 仅用'ABC'举例:
UTF-32BE | 00 00 00 41 00 00 00 42 00 00 00 43 |
UTF-32LE | 41 00 00 00 42 00 00 00 43 00 00 00 |
UTF-32(Big Endian) | 00 00 FE FF 00 00 00 41 00 00 00 42 00 00 00 43 |
UTF-32(Little Endian) | FF FE 00 00 41 00 00 00 42 00 00 00 43 00 00 00 |
UTF-32(不带BOM) | 00 00 00 41 00 00 00 42 00 00 00 43 |
11.UTF-8
UTF-16和UTF-32的一个缺点就是它们固定使用两个或四个字节, 这样在表示纯ASCII文件时会有很多00字节,造成浪费。 而RFC3629定义的UTF-8则解决了这个问题
UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号(理论上最多可以用6个字节表示一个符号),根据不同的符号而变化字节长度。
UTF-8的编码规则很简单,只有两条:
(1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
(2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围(十六进制) | UTF-8编码方式(二进制) |
0000 0000-0000 007F | 0xxxxxxx |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
以汉字“严”为例,演示如何实现UTF-8编码。
已知“严”的unicode是 0x4E25 (01001110 00100101),根据上表,可以发现 0x4E25 处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是 0xE4B8A5。
12.BOM
BOM(byte-order mark),即字节顺序标记,它是插入到以UTF-8、UTF16或UTF-32编码Unicode文件开头的特殊标记,用来识别Unicode文件的编 码类型。对于UTF-8来说,BOM并不是必须的,因为BOM用来标记多字节编码文件的编码类型和字节顺序(big-endian或little- endian)。
为了识别 Unicode 文件,Microsoft 建议所有的 Unicode 文件应该以 ZERO WIDTH NOBREAK SPACE(U+FEFF)字符开头。这作为一个“特征符”或“字节顺序标记(byte-order mark,BOM)”来识别文件中使用的编码和字节顺序。
Linux/UNIX 并没有使用 BOM,因为它会破坏现有的 ASCII 文件的语法约定。
不同的编辑工具对BOM的处理也各不相同。使用Windows自带的记事本将文件保存为UTF-8编码的时候,记事本会自动在文件开头插入BOM(虽然BOM对UTF-8来说并不是必须的),但是editplus就不会这样做。
以下是摘自维基百科中的BOM表:
Encoding | Representation (hexadecimal) | Representation (decimal) | Bytes as CP1252 characters |
---|---|---|---|
UTF-8[t 1] | EF BB BF |
239 187 191 |
 |
UTF-16 (BE) | FE FF |
254 255 |
þÿ |
UTF-16 (LE) | FF FE |
255 254 |
ÿþ |
UTF-32 (BE) | 00 00 FE FF |
0 0 254 255 |
␀␀þÿ (␀ refers to the ASCII null character) |
UTF-32 (LE) | FF FE 00 00 |
255 254 0 0 |
ÿþ␀␀ (␀ refers to the ASCII null character) |
UTF-7[t 1] | 2B 2F 76 38 2B 2F 76 39 2B 2F 76 2B 2B 2F 76 2F [t 2]2B 2F 76 38 2D [t 3] |
43 47 118 56 43 47 118 57 43 47 118 43 43 47 118 47 43 47 118 56 45 |
+/v8 +/v9 +/v+ +/v/ +/v8- |
UTF-1[t 1] | F7 64 4C |
247 100 76 |
÷dL |
UTF-EBCDIC[t 1] | DD 73 66 73 |
221 115 102 115 |
Ýsfs |
SCSU[t 1] | 0E FE FF [t 4] |
14 254 255 |
␎þÿ (␎ represents the ASCII "shift out" character) |
BOCU-1[t 1] | FB EE 28 |
251 238 40 |
ûî( |
GB-18030[t 1] | 84 31 95 33 |
132 49 149 51 |
„1•3 |
看了上面的内容你也许会问,没有BOM的情况下,系统会不会因为不知道文本的编码方式而导致乱码?
确实有这么个问题存在,最常见的就是你在简体中文的系统下新建一个txt文件,里面只写入“联通”两个字,保存后再重新打开文本,会发现读出来的是乱码。
因为当你新建一个文本文件时,记事本的编码默认是ANSI, 如果你在ANSI的编码输入汉字,那么他实际就是GB系列的编码方式,在这种编码下,"联通"的内码是:
0xC1 1100 0001
0xAA 1010 1010
0xCD 1100 1101
0xA8 1010 1000
第一二个字节、第三四个字节的起始部分的都是"110"和"10",正好与UTF8规则里的两字节模板是一致的,于是再次打开记事本时,记事本就误认为这是一个UTF8编码的文件,我们把第一个字节的110和第二个字节的10去掉,就得到了"00001 101010",再把各位对齐,补上前导的0,就得到了"0000 0000 0110 1010",这是UNICODE的006A,也就是小写的字母"j",而之后的两字节用UTF8解码之后是0x0368,这个字符什么也不是。这就是"联通"两个字的文件没有办法在记事本里正常显示的原因。而如果你在"联通"之后多输入几个字,其他的字的编码不见得又恰好是110和10开始的字节,这样再次打开时,记事本就不会坚持这是一个utf8编码的文件,而会用ANSI的方式解读之,这时乱码又不出现了。