计算机存储和处理信息都是以一个8位的二进制字节为单位的,例如:0b 1111 0000。一个字母、汉字等如何用一个二进制的数(编码)来表示呢。在计算机发展初期,因为没有人能预料到计算机会有现在这么大的发展,也没有想到要处理全世界的字符,所以,在发展之初仅设计了一个简单的、能表示128个字符的编码方案————ASCII编码 。
一、ASCII 编码
ASCII 编码方案规定,在一个8位的二进制字节中,第1位(最高位)固定为0,然后其它7位不断变化,以表示26个大写、26小写的英文字母、10个数字和其它的常用符号。例如:
A ———— 0b 0100 0001、或:0x 41、或:65
B ———— 0b 0100 0010、或:0x 42、或:66
C ———— 0b 0100 0011、或:0x 43、或:67
a ———— 0110 0001、或:0x61、或:97
b ———— 0110 0010、或:0x62、或:98
c ———— 0110 0011、或:0x63、或:99
1 ———— 0011 0001、或:0x31、或:49
2 ———— 0011 0010、或:0x32、或:50
3 ———— 0011 0011、或:0x33、或:51
小于号 < ———— 0011 1100、或:0x3C、或:60
等号 = ———— 0011 1101、或:0x3D、或:61
大于 > ———— 0011 11100、或:x3E、或:62
回车键 ———— 0000 1101、或:0x0D、或:13
响铃 ———— 0000 0111、或:0x07、或:7
ASCII编码中,第1位固定为0,只有剩余7位可以进行各种变化,所以,最多可表示128个字符。
为了本文后面叙述的方面,我个人在这篇文章中,暂且将ASCII编码中这128个字符称作:原符。也就是说,我在本篇下文中所称的原符,就是指ASCII编码中这128个字符。
二、Unicode 编码
随着计算机技术的发展,原符显然远远不够。于是就有了国际上统一的 Unicode 编码,它为全世界所有字符都分配了一个唯一的编码。当然,现在我们看到的 Unicode 编码也是从简到繁不断完善形成的。
Unicode 编码规定,用一个21位的二进制数表示一个字符。即:
第1个:0b 0 0000 0000 0000 0000 0000、或:0x 00 0000
第2个:0b 0 0000 0000 0000 0000 0001、或:0x 00 0001
......
倒 二:0b 1 0000 1111 1111 1111 1110、或:0x 10 fffe
最 后:0b 1 0000 1111 1111 1111 1111、或:0x 10 ffff
实际上,Unicode 编码是将所有字符分成17个种类(17个面)进行分类编码:
0x 00 0000 ———— 00 ffff:第1面,基本多文种平面(BMP):原符、各国文字等
0x 01 0000 ———— 01 ffff:第2面,多文种补充平面
0x 02 0000 ———— 02 ffff:第3面,表意文字补充平面
0x 03 0000 ———— 03 ffff:第4面,表意文字第三平面
0x 04 0000 ———— 04 ffff:第5面,未使用
......
0x 0d 0000 ———— 0d ffff:第14面,未使用
0x 0e 0000 ———— 0e ffff:第15面,特别用途补充平面
0x 0f 0000 ———— 0f ffff:第16面,专用区-A
0x 10 0000 ———— 10 ffff:第17面,专用区-B
其中 0x 00 4E00 ---- 00 9FFF 是中、日、韩的三种文字,例如:
一 ———— 0x 00 4e00
习 ———— 0x 00 4e60
乡 ———— 0x 00 4e61
笑 ———— 0x 00 7b11
大 ———— 0x 00 5927
好 ———— 0x 00 597d
从理论计算,0x 00 0000 ———— 10 ffff,可以有1,114,112种编码,但世界所有的字符显然比这少的多,所以,有许多的编码是空的,没有被使用。
Unicode 编码是一个字符的编码方案,它确保了世界上的每一个符号都有一个唯一的编码。Unicode 编码一般用“U + 16进制编码值”的形式表示。例如:
A ———— U+41
B ———— U+42
C ———— U+43
一 ———— U+4e00
习 ———— U+4e60
乡 ———— U+4e61
一个Unicode 编码是一个21位的二进制数,从表面上看,在计算机中用3个字节来存储一个Unicode编码正合适,但计算通常不用3个字节作为一个单位,所以,用4个字节来存储一个Unicode 编码应该是最好的。编码问题本该到此结束,但实际上并非如此。
尽管Unicode 编码设计了17个面,所实际上仅第一个面被充分利用,其它面很多是空的。也就是说,绝大部分全世界常用的字符都在第一个面,即,绝大部分全世界常用的字符的Unicode 编码前面都是一样的 ———— 全是0,这样,用4个字节来存储一个Unicode 编码就显得很“浪费”,例如:
A ———— 0b 0000 0000 0000 0000 0000 0000 0100 0001、U+41
B ———— 0b 0000 0000 0000 0000 0000 0000 0100 0010、U+42
C ———— 0b 0000 0000 0000 0000 0000 0000 0100 0011、U+43
一 ———— 0b 0000 0000 0000 0000 0100 1110 0000 0000、U+4e00
习 ———— 0b 0000 0000 0000 0000 0100 1110 0110 0000、U+4e60
乡 ———— 0b 0000 0000 0000 0000 0100 1110 0110 0001、U+4e61
目前,在计算机领域 Unicode 编码方面主要有三种存储规则:UTF-32、UTF-16、UTF-8。
特别要注意的是,编码和编码的存储规则不是一回事,编码是解决一个字符用哪些数字来表示的问题,同一个字符在不同的编码方案会有不同的“码”,ASCII 编码、Unicode 编码等,都属于编码方案。除了这两种最出名的编码方案,世界上还有许许多多其它的编码方案。编码的存储规则是解决一个编码如何存储的问题。例如,我国的GB2312编码、GBK编码。如果不需要考虑优化,存储规则可以很简单的:编码有多大就用多大的单元来存储,但这样会“浪费”很多空间,也不利于提高存取的效率。于是就有了各种优化方法,即,各种存储规则。UTF-32、UTF-16、UTF-8 等都属于编码的存储规则。
不过,我们有时也确实能看到“UTF-32编码”、“UTF-16编码”、“UTF-8编码”这样的说法。这是因为Unicode 编码按照不同的规则存储后,在存储单元中的实际样子有时与Unicode 编码有较大的区别(特别是 UTF-8),为了方便称呼 Unicode 编码在实际存储后的样子,我们也就分别称它们为 UTF-32编码、UTF-16编码、UTF-8编码。
例如:“笑”字的Unicode 编码为:U+7b11,按不同规则存储后的样子如下:
UTF-32(用4个字节单元存储):0000 0000 0000 0000 0111 1011 0001 0001 ———— 0x 00007b91
UTF-16(用2个字节单元存储):0111 1011 0001 0001 ———— 0x 7b91
UTF-8(用3个字节单元存储):1110 0111 1010 1100 1001 0001 ———— 0x e7ac91
于是称:“笑”字的 UTF-32编码 0x 00007b91,UTF-16编码 0x 7b91,UTF-8编码 0x e7ac91 。
由此可见, UTF-32编码、UTF-16编码、UTF-8编码实际上都是 Unicode 编码的“异形”。
三、UTF-32
这个规则最简单,那就是直接用4个字节来表示一个Unicode 编码。当然,如上所述,这个规则会“浪费”很多的存储空间,存取的效率是最低的。
用4个字节来表示一个Unicode 编码还存在一个“大端、小端”的问题。一个编码有4字节长,也就是在存储器上要占4个存储单元。计算机中每个存储单元都有一个地址,地址从小到大有高低之分,Unicode 编码的4字节,是从低地址的单元往高地址的单元存放呢,还是反过来,这就是大、小端的问题。将高位字节放到低地址存储单元,是大端法;反之,将高位字节放到高地址存储单元,是小端法。这有点像我们写字时,是从左往右写呢,还是从右往左写。两种方法都可以,只不过,怎么写就要怎么读,二者一定要一致。
四、UTF-16
1、UTF-16 的规则
UTF-16 比 UTF-32 复杂,其存储规则是:
(1)对于Unicode 编码在U+0000 到 U+FFFF的字符(常用字符集),直接用2个字节单元表示。
(2)对于Unicode 编码在 U+10000到U+10FFFF之间的字符,需要用4个字节单元表示。
也就是说,根据UTF-16的规则,有的编码用2个字节单元存储,而有的用4个字节单元存储,那在读取时,如何识辨这两种情况呢?
2、替代区
为了配合 UTF-16 规则,Unicode 编码在设计时,做了一个特殊的规定:在第一面划出一个区块(D800 - DFFF)不分配给任何字符,也就是,不可能有一字符它的编码在 U+D800 – U+DFFF之间,这部分可称为“替代区”,即:
0b 1101 1000 0000 0000 ———— 1101 1011 1111 1111 设置为“高位替代”区、(0x D800 – DBFF)
0b 1101 1100 0000 0000 ———— 1101 1111 1111 1111 设置为“低位替代”区、(0x DC00 – DFFF)
从上可见,对于一个16位的二进制码(2个字节长),如果前6位为:1101 10 或 1101 11,它就处在了替代区。
3、读取依据 UTF-16 规则存储的 Unicode 编码
首先,一次取2个连续的存储单元,把它们拼在一起,构成一个16位的二进制数,然后,查看这数的前6位,结果有三种情况:
情况一、不在替代区,即,前6位不是 1101 10 ,也不是 1101 11 ,则认定这是一个用2个字节存储某字符的Unicode 编码,反查编码表,取得字符。例如,取到的二进制数是 0b 0111 1011 0001 0001,前6位是 0111 10,不是 1101 10 ,也不是 1101 11,所以,直接将它看作一个字符的Unicode 编码,反查编码表,得出汉字“笑”。
情况二、在高位替代区,即,前6位是 1101 10 ,例如:1101 10xx xxxx xxxx,则认定这是一个用4个字节存储的某字符的Unicode 编码,且当前取到是“高位部分”。
(1)紧接着再取2个连续的存储单元拼在一起,构成一个16位的二进制数,例如:zzzz zzyy yyyy yyyy。
(2)取两个二进制数的后10位,拼成一个新的数(高位部分的在前),例如:xxxx xxxx xxyy yyyy yyyy。
(3)把这个新拼合成的数加上 0x 10000,将和作为一个 Unicode 编码,反查编码表,取得字符。也就是说,在种情况下,认定取得的 Unicode 编码为:
xxxx xxxx xxyy yyyy yyyy + 1 0000 0000 0000 0000
情况三、在低位替代区,即,前6位是 1101 11 ,例如:1101 11xx xxxx xxxx,则认定这也是一个用4个字节存储的某字符的Unicode 编码,且当前取到是“低位部分”。
(1)紧接着再取2个连续的存储单元拼在一起,构成一个16位的二进制数,例如:zzzz zzyy yyyy yyyy。
(2)取两个二进制数的后10位,拼成一个新的数(低位部分的在后),例如:yyyy yyyy yyxx xxxx xxxx。
(3)把这个新拼合成的数加上 0x 10000,将和作为一个 Unicode 编码,反查编码表,取得字符。也就是说,在种情况下,认定取得的 Unicode 编码为:
yyyy yyyy yyxx xxxx xxxx + 1 0000 0000 0000 0000
4、UTF-16 规则存储Unicode 编码的具体过程
首先,查看编码大小,然后分两种情况存储:
情况一:编码在 U+0000 – U+FFFF 之间,则直接用2个字节单元存储。
情况二:编码在 U+10000到U+10FFFF 之间,则按以下步骤进行:
(1)将编码减去 0x 10000,得到一个20位的二进制数,例如:0b xxxx xxxx xxyy yyyy yyyy。
(2)将新得到的20位的二进制数一分为二,前10位,放在 1101 10 之后,构成一个16位数,例如:
0b 1101 10xx xxxx xxxx
(3)后10位,放在 1101 11 之后,构成另一个16位数,例如:
0b 1101 11yy yyyy yyyy
(4)将两个16位数拼成一个32位数存入了4个存储单元中。例如:
0b 1101 10xx xxxx xxxx 1101 11yy yyyy yyyy
很显然,UTF-16 规则在用两个单元存储Unicode 编码时,也存在“大端、小端”的问题。
5、高位专用替代区
实际上,Unicode 编码中把“高位替代”区中的 0x DB80 – DBFF 又称作“高位专用替代”区。因为,当一个编码的高位部分落在这一区域时,还原所得的 Unicode 编码,一定会在第16和第17面,而这两个面又被称作专用区。
“高位专用替代”区:0b 1101 1011 1000 0000 ———— 1101 1011 1111 1111、或:0x DB80 – DBFF
例如:所得4个字节组成的二进制数为:
1101 1011 1xxx xxxx 1101 11yy yyyy yyyy
各取后10位拼合成:111x xxxx xxyy yyyy yyyy
加 0x 10000 :111x xxxx xxyy yyyy yyyy + 1 0000 0000 0000 0000
结果必定为:1111 xxxx xxyy yyyy yyyy(在16面)或 1 0000 xxxx xxyy yyyy yyyy(在17面)
五、UTF-8
1、UTF-8规则
从表面上看,UTF-8 比 UTF-16 还要简单一些,它依据各字符的 Unicode编码的大小来决定用几个存储单元来存储:
(1)U+0 – U+7F:用1个字节单元存储,首位固定为0,例如:0xxx xxxx
(2)U+0 – U+7F 之外的用2-4个字节单元存储。当某符号用n个字节单元存储时(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的 Unicode 编码。
U+80 – U+7FF:2个字节,110x xxxx 10xx xxxx
U+800 – U+FFFF:3个字节,1110 xxxx 10xx xxxx 10xx xxxx
U+10000~U+10FFFF:4个字节, 1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx
由于UTF-8的处理单元为一个字节(也就是一次处理一个字节),所以,不存在“大端、小端”的问题。
2、UTF-8规则与ASCII编码
为了与ASCII编码兼容,Unicode编码中的前128个编码全部对应分配给了原符。由于这前128个字符的编码在 U+0 – U+7F 之间,所以,按照UTF-8规则,它们都是用1个字节单元存储。也就是说,按照UTF-8规则,原符实际存储的编码值、Unicode编码、ASCII编码完全相同。
六、GB2312 编码、GBK 编码
GB2312 是早期的的中文编码,它用16位二进制数(2个字节)给原符和汉字进行编码。
1、原符的GB2312编码
原符的GB2312编码形式为:
0x 0000 0xxxx (第1个字节全为0,第二个字节首位为0,码值与ASCII编码相同)
2、汉字的GB2312编码
汉字 GB2312 编码的两个字节的分别被称为:高位字节 和 低位字节。
“高位字节”使用了0xA1-0xF7,“低位字节”使用了0xA1-0xFE,即:
高位字节:0b 1010 0001 ———— 1111 0111 、 0x A1 – F7
低位字节:0b 1010 0001 ———— 1111 1110 、 0x A1 – FE
GB2312 编码共收录 6763 个汉字,也就是,一共给 6763 个汉字编了码。
GB2312编码将汉字的2个字节的首位都固定设置为1,这是为了与原码进行区别,很有必要,但除首位之外,还有一些位也被固定了,这一方面是为了读取方便,另一方面,是当时的计算机技术不需要用到太多的汉字,所以也就不需要用到所有的位。
随着技术的发展,GB2312编码共收录的汉字不够用了,于是,在其基础上开发了GBK编码。
3、GBK 编码
GBK编码也是使用2个字节编码,原符的GBK编码沿用原符的GB2312编码,没有变化。
汉字的两个字节被扩展了:
高位字节范围扩展为: 0×81-0xFE,低位字节范围扩展为: 0x40-7E 和 0x80-0xFE,即:
高位字节:0b 1000 0001 ———— 1111 1110 、 0x 81 – FE
低位字节1:0b 0100 0000 ———— 0111 1110、 0x 40 – 7E
低位字节2:0b 1000 0000 ———— 1111 1110 、0x A1 – FE
从编码范围看,在GB2312编码中有些固定不用的位,在GBK 编码中被启用。用的位多了,编码数量也就增加了。GBK 编码总计有 23940 个码位,共收入 21886 个汉字和图形符号。
GBK 编码 之后还有一个更大的 GB18030 编码。它收录汉字70000余个,但实际应用范围并不广。
4、GB2312(GBK) 编码的存储
GB2312(GBK) 编码存储时,原符用1个字节单元存储(首位固定为0),汉字用2个字节单元存储。按照GB2312(GBK) 编码存储规则,原符实际存储的编码值、GB2312(GBK) 编码、ASCII编码完全相同。
七、Python 按 UTF-8 规则读取编码
不管程序文件用什么编码方式存储,Python 解释运行程序在读取文件时,都一律是按照 UTF-8 规则进行读取。所以,不论用什么编辑器编写Python 程序,最好都保存为 UTF-8 格式。
当程序中没有汉字时,如果程序文件用GB2312(GBK)编码存储,在Python 解释运行程序读取运行时不会出错。因为,原符不论是以GB2312(GBK)编码的方式存储,还是以 UTF-8 规则存储,其码值都是一样,都与它们ASCII编码完全相同,所以,用GB2312(GBK)编码方式存储,用 UTF-8 规则读取,不会出错。但是,当程序中有汉字时,如果再用GB2312(GBK)编码方式存储,就一定会出错。
Python 在读取字符时,不论是一个数字、一个字母,还是一个汉字,都按一个字符对待。例如:
print(len('1234567')) # 7
print(len('abcdefg')) # 7
print(len('中华人民共和国')) # 7
print(len('Python解释运行程序')) # 12
print(len(' ')) # 4个空格:4
print(len('--——,:')) # 6
Python 解释运行程序是完全支持中文的,只是按 UTF-8 格式存储,读取和处理中文一点问题都没有,甚至可以用汉字来作为变量名。例如:
笑 = '高兴和快乐'
发 = 88
一 = 11
二 = 22
x = 一 + 二
print(笑,发,x) # 高兴和快乐 88 33
———————————————— 本篇完 ————————————————
看完之后,麻烦您顺手点击下方 “点赞” 两个字给我点个赞吧 ^-^ , 谢谢您了。
如果您还能像我小学一年级班主任好老师那样,给我随心写上几句表扬或批评的话语,那真是感激不尽!
在我人生的道路上,有了您的鼓励和指导,我一定成长快快。