目录
- 一、前置知识
- 二、ASCII
- 三、从ASCII到Unicode
- 四、Unicode
- 五、UTF-8
- 六、总结
- References
一、前置知识
- 一个字节有8-bit;
- 一个十六进制数占4-bit,故一个字节可以表示为两个十六进制数(通常会加上
0x
前缀); - 十六进制通常用来简化二进制的表示,例如
10101100
可以表示为AC
(可以通过在Python中执行hex(0b10101100)
来验证)。
二、ASCII
ASCII(/ˈæski/,American Standard Code for Information Interchange,美国信息交换标准代码)是一个字符编码系统,用于在计算机和其他设备中表示文本。该系统最初由美国标准协会(ASA,现为ANSI)在1963年制定,并在后续几年中进行了几次修改和扩展。
ASCII编码使用7位二进制数(即0到127,共128个)来表示大部分英文标点符号、数字和大小写字母。例如,大写字母“A”的ASCII编码是65,小写字母“a”的ASCII编码是97。
ASCII表包含以下部分:
- 控制字符(0-31和127,共33个):这些字符主要用于控制设备,如换行(10)和回车(13)。
- 标准可见字符(32-126,共95个):包括数字(48-57)、大写字母(65-90)、小写字母(97-122)和各种标点符号。
一个标准的ASCII字符只占用一个字节(只使用了其中的7位),对于早期的计算机系统,这已经足够了,因为当时的计算机主要使用英语,并且对于其他语言的支持并不是一个主要关注点。然而,随着计算机技术的普及和国际化,这种仅限于128个字符的系统很快就显得不够用了。许多非英语的语言,包括欧洲的许多语言,都有一些ASCII无法表示的特殊字符。此外,还有一些常见的符号,例如货币符号,也无法用ASCII表示。
此时,一种解决方案是使用完整的8位字节来编码字符,从而可以表示256个不同的字符。这就是所谓的扩展ASCII,它提供了额外的128个字符来表示各种特殊字符和符号。
三、从ASCII到Unicode
ASCII编码主要支持英语,但对于包含非拉丁字符的其他语言(如中文、阿拉伯语、希腊语等)无法进行表示。这个局限性促使人们寻找新的编码方案,以便支持更多的字符集。
在中国,针对中文字符,人们开发了一系列的国标码(GB,即国家标准),包括GB2312、GBK和GB18030。
-
GB2312:这是中国在1980年代制定的编码标准,包括了全部的6763个简体中文汉字和682个其他语言字符。GB2312是双字节编码,使用两个字节来表示一个字符。所以理论上,GB2312可以表示65536(=2^16)个字符,但实际上并未全部使用。
-
GBK:全名《汉字内码扩展规范》。GBK是在GB2312基础上的扩展,包含了21003个汉字和883个图形符号。GBK的字节编码与GB2312相容,也是双字节编码,因此旧有的GB2312文档可以在GBK编码的处理器中打开,不会出现乱码。
-
GB18030:是GBK的进一步扩展,于2000年由中国国家标准总局发布。GB18030相较于GBK包含更多的字符,它覆盖了Unicode 11.0版本的所有字符。GB18030包含单字节、双字节和四字节字符,增加了对繁体字和少数民族文字的支持,是一个多字节编码。
然而,这些编码标准仍然存在局限性。首先,这些编码主要针对中国,对于其他语言的支持有限。其次,由于每个国家可能都有自己的编码标准(如日文的Shift_JIS,韩文的EUC-KR),这就导致了一个问题:同一个字符在不同字符集下的字符代码不同,而且往往一个字符集无法包含另一个字符集的所有字符,因此跨语言交流时往往会出现乱码。
为了解决这个问题,Unicode(统一码、万国码)被提出并得到广泛接受。Unicode是一种在全球范围内统一、唯一的字符集,它包括几乎所有的写作系统的字符。这就使得不同语言和不同国家的计算机系统可以使用同一种编码标准,从而简化了国际化软件的开发和支持。
四、Unicode
简单来讲,Unicode是一个字符集,它收录了世界上几乎所有的字符,并给每个字符分配了一个唯一的ID,这个ID就被称为码位(Code Point)。
Unicode码位是一个整数,范围是从0到1114111(十进制)或从0x0到0x10FFFF(十六进制)。这意味着Unicode标准理论上可以包含超过110万个不同的字符。然而,需要注意的是并非所有的码位都已经被分配给字符,目前只分配了约15万个。
码位一般会用十六进制数表示,并在前面加上 U+
前缀。例如,字母A的码位是 U+0041
,中文汉字「中」的码位是 U+4E2D
。Python 3中的字符默认是Unicode字符,这意味着Python 3中的字符可以表示几乎所有语言的字符。在Python中查找一个字符的码位仅需执行 hex(ord('A'))
。
📝 可以发现,Python中
chr()
函数能够接受的参数范围就是0到1114111,该函数接收一个码位并将其转化成对应的Unicode字符。
Unicode标准把码位分成了17个「平面」,每个平面包含65536个码位。第一个平面,从0x0000到0xFFFF,被称为基本多语言平面(Basic Multilingual Plane,简称BMP,几乎包含了现代语言的所有字符),剩余16个是辅助平面,因此Unicode总共有17 × 65536 = 1114112个码位。
除了使用 chr()
来获得码位对应的Unicode字符,我们还可以使用转义序列 \u
或 \U
。具体来说:
\u
后面跟四个十六进制数字,表示一个Unicode字符。例如,\u03B1
表示希腊字母"α"。\U
后面跟八个十六进制数字,用于表示超出基本多语言平面的Unicode字符。也就是说,它能表示的Unicode字符的范围更广。
例如:
print('\u03B1') # 输出:α,等价于chr(0x03b1)
print('\U0001F604') # 输出:😄,等价于chr(0x1f604)
\U
后面需要跟八个数字,即使前面的数字是0。如果一个字符可以用 \u
来表示,通常会优先使用 \u
,因为它更简短。
五、UTF-8
前面提到过,Unicode仅仅是一个字符集,它为每个字符分配了一个唯一的编号,但却没有规定这些编号应当如何存储。
例如,字母A的码位是 U+0041
,至少需要用一个字节来存储;汉字「中」的码位是 U+4E2D
,至少需要用两个字节来存储;表情「😃」的码位是 U+1F603
,至少需要用三个字节来存储。这就导致一个严重的问题:计算机怎么知道三个字节表示一个字符,而不是分别表示三个字符呢?
一个简单粗暴的做法是,我们为每个Unicode码位都分配四个字节,这意味着每个字符都用相同数量的字节表示。这便是UTF-32。
📝 即使是
U+10FFFF
也只需要三个字节就够了,至于为什么不统一分配三个字节,是因为计算机读取 2 i 2^i 2i 个字节的效率更高。
UTF-32(32-bit Unicode Transformation Format)是一种Unicode字符编码方案,它使用32位(4字节)来表示每个字符。可以看出,它是一种定长编码方案。
UTF-32的编码方式非常直观和简单,每个字符使用固定的4字节进行表示。例如,字母A的UTF-32表示为 00000041
,同样,汉字「中」的UTF-32表示为 00004E2D
(这里采用的是Big Endian,即UTF-32BE)。
可以发现,在UTF-32编码中,ASCII字符也需要使用4个字节来编码,这与其只需要使用1个字节来编码的ASCII字符集本身相比,造成了空间的大量浪费。为解决这一问题,UTF-16考虑使用两个字节来表示BMP(U+0000-U+FFFF),用四个字节来表示剩余的辅助平面(U+10000-U+10FFFF)。
⚠️ 实际上UTF-16还会涉及到高位代理(High Surrogate)和低位代理(Low Surrogate),这里不再赘述。
UTF-8是一个变长的编码方案,它使用一到四个字节来表示不同的字符,具体方案如下:
- U+0000 - U+007F:采用一个字节;
- U+0080 - U+07FF:采用两个字节;
- U+0800 - U+FFFF:采用三个字节;
- U+10000 - U+10FFFF:采用四个字节。
不过一个问题在于,给定一个字节序列,由于UTF-8是变长的,我们怎么得知第一个字符占用了多少字节呢?
这里就要用到自同步(Self-synchronization)的概念了。自同步是指,在字符编码的上下文中,能从字节流的任意位置开始读取,并能正确地确定字符的边界,无需回溯到字节流的开始处。
UTF-8编码就是一种自同步编码方式,它的设计允许在任何字节序列中定位字节边界:
- 如果一个字节的最高位是0,那么这个字节就是一个ASCII字符,即这个字节代表一个字符;
- 如果一个字节的最高位是1,那么这个字节就是一个多字节字符的一部分。连续的1的数量表示了这个多字节字符的总字节数。例如,如果一个字节的前两位是11,那么这就是一个多字节字符的开始,且这个字符总共有两个字节;如果一个字节的前三位是111,那么这就是一个多字节字符的开始,且这个字符总共有三个字节;
- 如果一个字节的前两位是10,那么这就是一个多字节字符的中间字节。
可以看出UTF-8兼容ASCII字符集,因此在处理纯ASCII文本时,UTF-8编码和ASCII编码是一样的,每个字符都只需要一个字节。
UTF-8的二进制格式列在下表中:
码位范围 | 二进制格式 |
---|---|
U+0000 - U+007F | 0XXXXXXX |
U+0080 - U+07FF | 110XXXXX 10XXXXXX |
U+0800 - U+FFFF | 1110XXXX 10XXXXXX 10XXXXXX |
U+10000 - U+10FFFF | 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX |
那么给定一个Unicode字符,我们如何计算它的UTF-8编码呢?
例如对于汉字「严」,执行 hex(ord('严'))
可得知它的码位是 U+4E25
,根据上表,该字的UTF-8编码需要三个字节,即格式是 1110XXXX 10XXXXXX 10XXXXXX
,接下来进行填空。执行 bin(ord('严'))
可得知「严」的二进制为 100111000100101
,按照上述空缺处进行分组可得到 100 111000 100101
,填入后得到 1110X100 10111000 10100101
,剩下的 X
用0填充,于是就得到了「严」的UTF-8编码 11100100 10111000 10100101
,转换成十六进制就是 E4B8A5
。
可以通过在Python中运行如下代码来直接获得「严」的UTF-8编码:
print('严'.encode('utf-8'))
Python 3使用的编码标准就是UTF-8,在命令行执行如下命令来查看默认编码:
python -c "import sys; print(sys.getdefaultencoding())"
六、总结
Unicode是字符集,而UTF-32、UTF-16、UTF-8是Unicode的一种编码方案(实现方式),旨在将码位转换成可以存储和传输的字节序列。
References
[1] https://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
[2] https://blog.csdn.net/qq_36761831/article/details/82291166
[3] https://www.zhihu.com/question/23374078/answer/24385963
[4] https://zhuanlan.zhihu.com/p/51828216
[5] https://docs.python.org/zh-cn/3/howto/unicode.html#