Python 入门 26 —— ASCII 编码、Unicode 编码、 UTF-32、 UTF-16、 UTF-8、 GB2312 编码、 GBK 编码

计算机存储和处理信息都是以一个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

———————————————— 本篇完 ————————————————

看完之后,麻烦您顺手点击下方 “点赞” 两个字给我点个赞吧 ^-^ , 谢谢您了。

如果您还能像我小学一年级班主任好老师那样,给我随心写上几句表扬或批评的话语,那真是感激不尽!

在我人生的道路上,有了您的鼓励和指导,我一定成长快快。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章