Python 3 中的 String 对象实现了对 Unicode 的支持,在深入研究 String 的实现前,需要对 Unicode 有比较深入的了解。
最初的字符编码 ASCII
如果有 C 语言编程经验的朋友,一定直到 char 类型,它是一个 8 位宽的数据类型,用来表示英文和一些常见的打印字符,由于计算机技术由英文世界发明,因此最初的使用中只考虑了英语的表示需要,经过发展最终就有了知名的 ASCII 码表。
在 ASCII 码中只用了 char 类型 8 位中的后 7 位,最高一位始终为 0,因此 ASCII 码总共可以表示 128 个字符。这便是计算机中最早的通用字符表示。
多语言编码
随着计算机的普及,越来越多非英语国家有了使用需求,但是当时流行的码表中只能表示英文和一些符号,因此一些其他语言就将 ASCII 码中没有用到的最高位利用起来,增加了 127 个额外的字符,用来表示各自语言的字符。这样就有两个问题:
- 编号为 128 ~ 255 的字符在不同语言中表示不同;
- 有些语言(比如采用象形文字的中、日文)不够用。
此时一种 “世界级” 编码就诞生了,它就是 Unicode。Unicode 将字符编码从 8 位扩展到最高 32 位,这样就可以表示 4,294,967,296 个字符,应该足够表示世界上所有字符了。各国语言的基本字符就被排布在 127 以上的编码上。
Unicode 有三种编码形式:
- utf-8
- utf-16
- utf-32
utf-8
这是一种非常紧凑的表示方式,它可以完美的兼容 ASCII 码。
unicode 码点 | unicode 编码 |
---|---|
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 采用可变长度编码,节省了存储空间,并且其最低分段的表示,与 ASCII 码完全相同,这使得 utf-8 可以轻松的兼容旧的文本编码。
utf-32
按照 unicode 的定义,将编码位数扩展到了 32 位,在 utf-32 中,每一个字符都有 32 位表示,未使用的位则按 0 填充,这样表示的好处是处理非常简单,但增大了存储消耗,比如一个 ASCII 码文本转换为 utf-32,体积将是 ASCII 文本的 4 倍。
utf-16
这是一种折中方案,即没有太过于紧凑,也没有像 utf-32 那样铺张。使用 utf-16 表示字符的时候,如果将所有字符都用来映射字符,那么总共可以表示 2^16 = 65536 个字符,确实很多了,但是相比世界的字符数量还是不够。为了解决这个问题,utf-16 将 2^16 种可能中的第 U+D800 到 U+DFFF 位置保留, 其中:
- U+D800 到 U+DBFF 用来表示一个 5 位数据 H;
- U+DC00 到 U+DFFF 用来表示一个 5 位数据 L;
将 H 和 L 拼接成为一个 20 位的值,这样就可以在 在 utf-16 编码中,1 个 16 位码表示 2^16 以下编码的字符,遇到一个 U+D800 到 U+DBFF 的码点,就需要将其后的码点共 32 位一起解析。这样就可以动态的用 16 位表示 unicode 了。
实际上这里有一个抽象概念,utf-16 将编码抽象到一个 17 层 x 2^16 字符/层 的结构中。
utf-16 能直接表示的字符都在 “基平面” 内,对于扩展平面上的字符则需要组合两部分数据表示:
- 扩展平面编号;
- 平面内编码。
扩展层一共 16 层,需要 4 位就可以表示,每一层内的编码是 16 位,那么只需要 4 + 16 = 20 位就可以表示任意扩展层中的数据,这 20 位编码由基平面 U+D800 到 U+DFFF 的两个码点组合表示。