12.6 Unicode 字符串类型
Python 的 Unicode 字符串很复杂。任何跨平台 Unicode 类型都很复杂。
造成这种复杂性的原因是 Python 提供多种不同编码方式,以及 Python 所支持的平台上的默认配置不同。
以前 Python 2 的字符串类型使用 C 语言中的 char 类型存储。单字节的 char 类型足够存储任何一个 ASCII 码(American Standard Code for Information Interchange,美国信息交换标准代码)字符,ASCII 码自 20 世纪 70 年代以来一直被用于计算机编程中。
ASCII 不支持世界上使用的成千上万种语言和字符集。此外,还有一些扩展的字形字符集它也无法支持,如表情符号。
为了解决这些问题,Unicode 联盟于 1991 年推出了一种标准的编码系统和字符数据库,称为 Unicode 标准。现代 Unicode 标准包括所有书面语言的字符,以及扩展的字形和字符。
截至 13.0 版本,Unicode 字符数据库(Unicode Character Database, UCD)包含 143,859 个命名字符,而 ASCII 中只有 128 个。Unicode 标准在一个名为通用字符集(Universal Character Set, UCS)的字符表中定义了这些字符。每个字符都有一个独特的标识符,被称为码点(code point)。
有许多不同的编码使用 Unicode 标准将代码点转换为二进制值。
Python Unicode 字符串支持三种长度的编码:
1 字节(8位)
2 字节(16位)
4 字节(32位)
这些长度可变的编码在 CPython 实现中被称为:
1 字节的
Py_UCS1
,存储为 8 位无符号int
类型uint8_t
。2 字节的
Py_UCS2
,存储为 16 位无符号int
类型uint16_t
。4 字节的
Py_UCS4
,存储为 32 位无符号int
类型uint32_t
。
相关源文件
以下是与字符串有关的源文件。
处理 Unicode 码点
CPython 并不包含 UCD 的副本,即使 Unicode 标准中加入了新的字符体系和字符,CPython 也不需要变化。
CPython 中的 Unicode 字符串只需要关心编码问题。操作系统负责用正确的字符体系表示码点。
Unicode 标准包括 UCD,并定期更新新的字符体系、emoji 和字符。操作系统通过补丁接收 Unicode 的这些更新。这些补丁包括新的 UCD 码点以及对各种 Unicode 编码的支持。UCD 被分成若干个称为代码块的部分。
Unicode 码表发布在 Unicode 网站上。
另一个需要支持 Unicode 的是 Web 浏览器。Web 浏览器会解码带有编码标记的 HTTP 编码头中的 HTML 二进制数据。如果你用 CPython 作为 Web 服务器,那么你的 Unicode 编码必须与发送给用户的 HTTP 头中的编码标记匹配。
UTF-8 vs UTF-16
有两种常见的编码方式:
UTF-8 是一种 8 位的字符编码,支持 UCD 中所有可能的字符,码点为 1-4 字节。
UTF-16 是一种 16 位的字符编码,它与 UTF-8 类似,但它与 7 位或 8 位的编码不兼容(例如 ASCII)。
UTF-8 是最常用的 Unicode 编码。
在所有的 Unicode 编码中,码点都可以用十六进制来表示。这里有几个例子。
U+00F7
表示除法字符('÷'
)U+0107
表示带尖音符的拉丁文小写字母 c,('ć'
)
在 Python 中,Unicode 码点可以直接使用转义字符 \u
和码点的十六进制值进行编码。
CPython 不会对这些数据进行填充,所以如果你尝试 \u107
,那么就会出现以下异常。
XML 和 HTML 都支持带有特殊转义字符 &#val;
的 Unicode 码点,其中 val
是码点的十进制值。如果你需要将 Unicode 码点编码到 XML 或 HTML 中,那么你可以将 .encode()
方法中错误处理程序设为 xmlcharrefreplace
。
输出将包含 HTML 或 XML 转义码点。 所有现代浏览器都会将此转义序列解码为正确的字符。
ASCII 码兼容性
如果你正在处理 ASCII 编码的文本,那么了解 UTF-8 和 UTF-16 之间的区别就很重要。UTF-8 的主要优点是与 ASCII 编码的文本兼容。ASCII 编码是一种 7 位编码。
Unicode 标准的前 128 个码点代表了 ASCII 标准中的前 128 个字符。例如,拉丁字母 "a"
是 ASCII 中的第 97 个字符,也是 Unicode 中的第 97 个字符。十进制的 97 相当于十六进制的 61,所以 "a"
的 Unicode 码点是 "U+0061"
。
在 REPL 中,你可以为字母 "a"
创建二进制代码。
它可以正确地被解码为 UTF-8。
UTF-16 适用于 2 至 4 字节的码点。字母 "a"
的 1 字节表示法将不会被解码。
在选择编码机制时需要注意这一点。如果你需要导入 ASCII 码编码的数据,UTF-8 是一个比较安全的选择。
宽字符类型
如果你在 CPython 源代码中处理未知编码的 Unicode 字符串输入,那么将使用 C 语言中的 wchar_t
类型。
wchar_t
是宽字符串的 C 标准,足以在内存中存储 Unicode 字符串。在 PEP 393 之后,wchar_t
类型被选为 Unicode 存储格式。Unicode 字符串对象提供了 PyUnicode_FromWideChar()
,一个将 wchar_t
常量转换为字符串对象的函数。
例如,python -c
使用的 pymain_run_command()
将 -c
参数转换为 Unicode 字符串。
Modules/main.c
第 226 行
字节顺序标记
当解码一个输入时,比如一个文件,CPython 可以从字节顺序标记(Byte Order Marker,BOM)中检测出字节顺序。BOM 是出现在 Unicode 字节流开始的特殊字符。它告诉接收者数据是以何种字节顺序存储的。
不同的计算机系统可以用不同的字节顺序进行编码。如果你使用了错误的字节顺序,即使是正确的编码,那么数据也会出现乱码。大端顺序将最高位字节放在前面。小端顺序是将最低位字节放在前面。
UTF-8 规范确实支持 BOM,但它没有任何作用。UTF-8 的 BOM 可以出现在编码数据序列的开头,表示为b'\xef\xbb\xbf'
,这将向 CPython 表明数据流很可能是 UTF-8。UTF-16 和 UTF-32 支持小端和大端 BOM。
CPython中默认的字节序是由sys.byteorder
设置的。
encodings
包
encodings
包Lib/encodings
中的 encodings
包为 CPython 提供了超过 100 种内置编码方式。每当对一个字符串或字节串调用 .encode()
或 .decode()
方法时,都会从这个包中查找编码方式。
每个编码都定义成一个单独的模块。例如,ISO2022_JP
是一个广泛用于日本电子邮件系统的编码,它在 Lib/encodings/iso2022_jp.py
中声明。
每个编码模块都会定义一个 getregentry()
函数,并注册以下特征:
其唯一名称
其来自编解码模块的编码和解码函数
其增量编码器和解码器类
其流式读者类和写者类
许多编码模块共享 codecs
或 _mulitbytecodec
模块中编解码器。一些编码模块在 C 语言中使用来自 Modules/cjkcodecs
中单独的编解码器模块,。
例如,ISO2022_JP
编码模块从 Modules/cjkcodecs/_codecs_iso2022.c
中导入一个 C 语言扩展模块,_codecs_iso2022
。
encodings
包也有一个模块,Lib/encodings/aliases.py
,它包含一个 aliases
字典。这个字典用于给编码添加别名。例如,utf8
、utf-8
和 u8
都是 utf_8
编码的别名。
编解码器模块
codecs
模块处理具有特定编码格式的数据的转换。可以分别使用 getencoder()
和 getdecoder()
获取特定编码格式的编码或解码函数:
编码函数将返回包含二进制结果和输出中的字节数的元组。 codecs
还实现了内置函数 open()
,用于从操作系统打开文件句柄。
编解码器的实现
Unicode 对象(Objects/unicodeobject.c
)的实现包含以下编码方法。
所有的解码方法都有类似的名字,只不过用 Decode
代替 Encode
。
其他编码的实现在 Modules/_codecs
中,以避免对主 Unicode 字符串对象的实现造成混乱。unicode_escape
和 raw_unicode_escape
编解码器属于 CPython 的内部实现。
内部的编解码器
CPython 带有许多内部编码。 这些是 CPython 所独有的,对于某些标准库函数以及生成源代码时很有用。
这些文本编码可用于任何文本输入或输出:
还有一些仅限二进制的编码,需要与带有字节字符串输入的 codecs.encode()
或 codecs.decode()
一起使用,例如:
以下是仅限二进制的编码列表:
例子
在 PyUnicode_Type
中,tp_richcompare
类型槽被分配给 PyUnicode_RichCompare()
函数。这个函数用于做字符串的比较,并可以调整为使用 ~=
操作符。你将要实现的函数行为是对两个字符串进行不区分大小写的比较。
首先,添加一个 case 语句来检查左右两边的字符串是否有二进制等价关系。
Objects/unicodeobject.c
第11361行
然后添加一个新的 else if
块来处理 Py_AlE
运算符。这将执行以下操作:
将左边的字符串转换为新的全大写字符串。
将右边的字符串转换为新的全大写字符串。
比较这两个字符串。
解除对两个临时字符串的引用,使它们被释放
返回结果。
你的代码应该像这样:
重新编译后,你的不区分大小写的字符串比较应该在 REPL 上给出以下结果:
Last updated