前言
最近在看《深入理解计算机系统》,读到“字符编码”时不禁想起了初学时那段痛苦的岁月,同时又没找到一篇将理论和实践结合在一起的文章,为此决定自己写一份。希望能把我走过的弯路总结出来,能帮助一些还在路上的朋友。
关于计算机如何存储信息,请参考《深入理解计算机系统》的第 02 章内容,此文只讲解与字符编码有关的内容。
另外,关于常用编辑器对于字符编码的区别,请参考我的另一篇文档——《Win记事本、Sublime、Notepad++等编辑器对常见字符编码的处理和区别:GB2312、GBK、ANSI、Unicode、UTF-8》,此处也不再赘述。
字符的表示原理
简单说,计算机内所有信息都是使用0和1进行表示的。对于一个短路来说,0代表关,1代表开。那把这些电路组合起来就可以有长串0和1组成的二进制
数字,我们对这些数字进行编码和解码,我们就能用它来表示我们想要表示的东西了。比如:文字、图像、视频等等,就是一组0和1的二进制序列。
二进制数的每一个位表示一个计算机位(bit,简称位),8个位组成一个字节(byte)
。那么一个字节可以表示256种含义(2*2*2*2*2*2*2*2=256)。
虽然机器是基于二进制的,但对人类来说,因为二进制数太长了,需要做精简。因此需要将其转换成十六进制(hexadecimal,简称hex)
。转换方式很简单,使用“8421法”将四位二进制数转换成十六进制数的一位,比如:1010(binary)会转为A(hex)。在 C 语言中,十六进制数以”0x”或“0X”开头,A表示10,F表示16。
此后,00000000~11111111就可以用0x00~0xFF来表示了。
常见的字符编码
ANSI是美国国标的英文字符编码,占用一个字节,收录了127个字符。
原理很简单,用一个字节的不同的值表示一个字符、字母或数字。
文字开头有不可见的标志码,用于告知下文应该以什么方式解码。
文中所有字符编码都是这种方式。
英文字符编码占用一个字节就够了,但对于像中文、日文这种象形文字国家一个字节就肯定不够了。为此,中国的国标字符编码需要占用 2 个字节,中国标准局依次发布了GB2312、GBK、GB18030等。
虽然每个国家或地区的字符编码自行搞定了,但是多个国家间还是不能兼容。为此Unicode编码应运而生,它整合全世界的几乎所有语言文字。
下面我们依次讲解下ANSI、GB2312、GBK、GB18030、Unicode、UTF-8等编码。
ANSI编码
ANSI码占用1个字节,收录127个字符,取值范围0x00~0x7F。
ANSI编码表:
GB2312、GBK、GB18030
发布顺序依次为:GB2312、GBK、GB18030。都以ANSI格式存储。
彼此区别:
- GB2312是常用简体汉字;
- GBK基于GB2312,包含更多生僻字,支持简繁双体;
- GB18030基于GBK,包含更多生僻字,支持少数民族文字,支持
CJK(中日韩)
统一字符。 - GB2312和GBK都是双字节,GB18030分单、双、四字节三个部分。
比如:在GB2312上没有“喆”,在GBK上才被收录。
因这三种编码规则差不多,下面我们重点讲解下GB2312。
GB2312
考虑到以下因素:
- ANSI范围是:0x00~0x7F。
- 国际通用精确数字计算使用的BCD编码范围是:0x00~0x99。
为避免冲突,要预留一定空间,GB2312的范围:0xA0~0xFF。
GB2312:
- 又称GB0,国标局发布,1981年5月1日实施。
- 收录6763个汉字:一级汉字3755个,常用字,排在前面;二级汉字3008个,生僻字,排在后面。
- GB2312是一种区位码。分为94个区(01-94),每区94个字符(01-94)。
- 01-09区为特殊符号;
- 10-15区没有编码;
- 16-55区为一级汉字,按拼音排序,共3755个;
- 56-87区为二级汉字,按部首/笔画排序,共3008个;
- 88-94区没有编码;
- GB2312只是编码表,在计算机中通常都是用”
EUC-CN
“表示法,即在每个区位加上0xA0来表示。区和位分别占用一个字节。
EUC-CN表示法:
- EUC-CN是GB2312最常用的表示方法。
- GB2312字元使用两个字节来表示, 每个字节=0xA0+区位码。
- “第一位字节”取值范围:0xB0~0xF7。
- “第二位字节”取值范围:0xA1~0xFE。
举例来说,“啊”字是GB 2312之中的第一个汉字,它的区位码是1601。
在EUC-CN之中,它把0xA0+16=0xB0;0xA0+1=0xA1;得出0xB0A1。
GB2312的中文区位的首末两位是特意空出来的。原因是:
- 首位(0x00):在ANSI中表示空,中国人更习惯从1开始计数,而不是从0开始计数。
- 末位(0xFF):代表结束,因为有些程序员会将其定义为空,因此空出以避免冲突。
GB2312中文编码页的排列方式:
GB2312的编码表请参考以下链接:GB2312汉字编码字符集对照表。
《关于Windows记事本与Sublime Text对中文字符编码转换的问题》:
先对Windows记事本做个小实验:
- 在Windows记事本中,新建文件,输入“你好hello1234”,以ANSI格式保存后;
在Sublime Text中打开此文件,文件格式为GB2312,输入GB2312内不支持的汉字“喆”,提示保存失败,原因是’\u5586’是非法的多字节序列;
’gb2312’ codec can’t encode character ‘\u5586’ in position 11: illegal multibyte sequence
第 11 个字符就是“喆”。
注意:如果不删除这个字段,Sublime Text会以UTF-8格式重新保存这个文件。在Windows记事本中,输入GB2312内不支持的汉字“喆”,按ANSI格式保存成功;
- 再次用Sublime Text打开此文件,文件格式已变为GBK。
综上,在Windows记事本中,GB2312和GBK是统一以ANSI方式管理的。我猜测,Windows记事本默认使用GB2312存储汉字,如遇到GB2312不能识别的生僻字才会使用GBK格式存储。
Sublime Text并不支持这种功能,如果你强行输入“喆”字以后,会有错误框弹出。然后,它会把这个文件转换为UTF-8格式并保存。
因为Sublime Text默认是支持UTF-8编码,不支持GB2312、GBK等中文编码,需要安装ConvertToUTF-8
插件才能实现。显然,ConvertToUTF-8并没考虑这种情况。在Sublime Text中以GB2312格式无法存储没被收录的字符:
Unicode
Unicode简介:
- 统一码、万国码、单一码,包括字符集、编码方案等。
- 它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
- 1990年开始研发,1994年正式公布。
- 统一码为每个字符而非字形定义唯一的编码(即一个整数),字体视觉展示由其他软件来处理。由此,可解决汉字一字多形的认定争议(如“ɑ/a”、“户/户/戸”等)。
Unicode与UCS:
统一编码联盟都试图独立创建一套国际通用的字符编码,此后双方都意识到没必要创建两套编码,于是两者融合并承诺彼此兼容。从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码。
通用字符集(Universal Character Set, UCS)
:由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的标准字符集。UCS-2用两个字节编码,UCS-4用4个字节编码。简单说,Unicode与UCS-2相同。
Unicode编码原理:
UCS-4有4个字节:
- 第一个字节:表示组(group),最高位为0,则有128个。
- 第二个字节:表示平面(plane),256个。
- 第三个字节:表示行(row),256个。
- 第四个字节:表示码位(cell),256个。
group 0的是最底层的平面0被称作BMP(Basic Multilingual Plane),即0x0000XXXX(大写X表示任何一个十六进制数字)。
平面 | 字符范围 | 备注 |
---|---|---|
基本多语言平面 | 0~0xFFFF | BMP, or Plane 0 |
增补多语言平面 | 0x10000~0x1FFFF | SMP, or Plane 1 |
增补表意字符平面 | 0x20000~0x2FFFF | SIP, or Plane 2 |
增补专用平面 | SSP, or Plane 14 |
注意:
- BMP:Basic Multilingual Plan
- SMP:Supplementary Multilingual Plane
- SIP:Supplementary Ideographic Plane
- SSP:Supplementary Special-purpose Plane
当UCS-4的前两个字节为全零(BMP)时,那么去掉UCS-4的前两个零字节就得到了UCS-2。
在Unicode中,有几种方式唯一编码数字的转换格式:UTF-8、UTF-16、UTF-32。 UTF(Unicode Transformation Format)
,可以翻译成Unicode字符集转换格式,即怎样将Unicode定义的数字转换成程序数据。
例如,“汉字”对应的数字是0x6c49和0x5b57,而编码的程序数据是:
char data_utf8[]={0xE6,0xB1,0x89,0xE5,0xAD,0x97}; //UTF-8编码
char16_t data_utf16[]={0x6C49,0x5B57}; //UTF-16编码
char32_t data_utf32[]={0x00006C49,0x00005B57}; //UTF-32编码
注: char16_t 和 char32_t 是 C++ 11 标准新增的关键字。如果你的编译器不支持 C++ 11 标准,请改用 unsigned short 和 unsigned long。
“汉字”的UTF-8编码需要6个字节。
“汉字”的UTF-16编码需要两个char16_t,大小是4个字节。
“汉字”的UTF-32编码需要两个char32_t,大小是8个字节。
UTF-8
Unicode编码(十六进制) | UTF-8 字节流(二进制) |
---|---|
00000000 - 0000007F | 0xxxxxxx |
00000080 - 000007FF | 110xxxxx 10xxxxxx |
00000800 - 0000FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
00010000 - 001FFFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
00200000 - 03FFFFFF | 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
04000000 - 7FFFFFFF | 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
UTF-8的特点是对不同范围的字符使用不同长度的编码,兼容ASCII编码。
- 对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。
- UTF-8编码的最大长度是6个字节。
- 从上表可以看出,6字节模板有31个x,即可以容纳31位二进制数字。Unicode的最大码位0x7FFFFFFF也只有31位。
例1:
“汉”的Unicode编码是0x6C49,在0x0800-0xFFFF之间,使用3字节模板:1110xxxx 10xxxxxx
10xxxxxx。 0x6C49的二进制表示为:0110 1100 0100 1001,
用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
例2:Unicode编码0x20C30在0x010000-0x10FFFF之间,使用4字节模板:11110xxx 10xxxxxx
10xxxxxx 10xxxxxx。将0x20C30写成21位二进制数字(不足21位就在前面补0):0 0010 0000 1100
0011 0000,用这个比特流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0
A0 B0 B0。
UTF-16
根据字节序的不同,UTF-16可以被实现为UTF-16BE
或UTF-16LE
(Windows系统默认)。
Win32的API提供两种调用方式:
- 以W结尾的函数:使用UTF-16LE编码模式;
- 以A结尾的函数:使用多字节编码模式(中文编码是以ANSI格式存储的),但是Windows会在底层将其转换为UTF-16LE编码来处理。
因此对于Windows编程来说,最好默认使用Unicode(UTF-16LE)模式开发,避免系统底层多余的字符转换。
Unicode字符编码范围 | UTF-16 单个字符长度 | 备注 |
---|---|---|
U+0000~U+FFFF | 1个16位编码单元 | 16位无符号整数,固定宽度,BMP中的字符 |
U+10000~U+10FFFF | 2个16位编码单元 | 称作代理对(surrogate pair) ,变宽 |
UTF-16是早期Unicode遗留下的历史产物,原本被设计成具有固定宽度的16位编码格式。为支持超过U+FFFF的增补字符,设立了代理机制。
编码转化规则
UTF-16编码以16位无符号整数为单位。我们把Unicode编码记作U。编码规则如下:
- 如果U<0x10000,U的UTF-16编码就是U对应的16位无符号整数。
- 如果U≥0x10000,
- 我们先计算U’=U-0x10000,
- 然后将U’写成二进制形式:yyyy yyyy yyxx xxxx xxxx,
- U的UTF-16编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
为什么U’可以被写成20个二进制位?
Unicode的最大码位是0x10FFFF,减去0x10000后,U’的最大值是0xFFFFF,2的5次幂是32,所以肯定可以用20个二进制位表示。
例如:Unicode编码0x20C30,减去0x10000后,得到0x10C30,写成二进制是:0001 0000 1100 0011
0000。用前10位依次替代模板中的y,用后10位依次替代模板中的x,就得到:1101100001000011
1101110000110000,即0xD843 0xDC30。
UTF-32
根据字节序的不同,UTF-32可以被实现为UTF-32LE或UTF-32BE。
UTF-32是一种最简单的Unicode编码格式。每个Unicode码点被直接表示为一个32位的编码单元。UTF-32是一种固定宽度的字符编码格式。
每个UTF-32编码单元的值与Unicode码点的值完全相同。
因为对于多数情况下,非常浪费空间,因此应用场景很少。
各编码间的转换
总结
参考文献
深入理解计算机系统——第02章——信息的表示和处理
GB2312的区位码是94的来历
GB2312汉字编码字符集对照表
“字节序”是个什么鬼?
字符编码笔记:ASCII、Unicode、UTF-8、UTF-16、UCS、BOM、Endian
Unicode汉字对应表
US-ASCII和ISO-8859-n 控制字符表
Unicode字符编码标准