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. 1 字节(8位)

  2. 2 字节(16位)

  3. 4 字节(32位)

这些长度可变的编码在 CPython 实现中被称为:

  1. 1 字节的 Py_UCS1,存储为 8 位无符号 int 类型 uint8_t

  2. 2 字节的 Py_UCS2,存储为 16 位无符号 int 类型 uint16_t

  3. 4 字节的 Py_UCS4,存储为 32 位无符号 int 类型 uint32_t

相关源文件

以下是与字符串有关的源文件。

文件用途

Include/unicodeobject.h

Unicode 字符串对象定义

Include/cpython/unicodeobject.h

Unicode 字符串对象定义

Objects/unicodeobject.c

Unicode 字符串对象的实现

Lib/encodings

encodings 中包含所有可能的编码

Lib/codecs.py

编解码模块

Modules/_codecsmodule.c

Codecs 模块的 C 扩展实现了特定于操作系统的编码

Modules/_codecs

其他编码的编解码实现

处理 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

有两种常见的编码方式:

  1. UTF-8 是一种 8 位的字符编码,支持 UCD 中所有可能的字符,码点为 1-4 字节。

  2. UTF-16 是一种 16 位的字符编码,它与 UTF-8 类似,但它与 7 位或 8 位的编码不兼容(例如 ASCII)。

UTF-8 是最常用的 Unicode 编码。

在所有的 Unicode 编码中,码点都可以用十六进制来表示。这里有几个例子。

  • U+00F7 表示除法字符('÷'

  • U+0107 表示带尖音符的拉丁文小写字母 c,('ć')

在 Python 中,Unicode 码点可以直接使用转义字符 \u 和码点的十六进制值进行编码。

>>> print("\u0107")
ć

CPython 不会对这些数据进行填充,所以如果你尝试 \u107,那么就会出现以下异常。

 print("\u107")
  File "<stdin>", line 1
SyntaxError: (Unicodeerror) 'unicodeescape' codec can't decode bytes in position 0-4: truncated \uXXXX escape

XML 和 HTML 都支持带有特殊转义字符 &#val; 的 Unicode 码点,其中 val 是码点的十进制值。如果你需要将 Unicode 码点编码到 XML 或 HTML 中,那么你可以将 .encode() 方法中错误处理程序设为 xmlcharrefreplace

>>> "\u0107".encode('ascii', 'xmlcharrefreplace')
b'&#263;'

输出将包含 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" 创建二进制代码。

>>> letter_a = b'a'
>>> letter_a.decode('utf8')
'a'

它可以正确地被解码为 UTF-8。

UTF-16 适用于 2 至 4 字节的码点。字母 "a" 的 1 字节表示法将不会被解码。

>>> letter_a.decode('utf16') Traceback (most recent call last):
File "<stdin>", line 1, in <module> UnicodeDecodeError: 'utf-16-le' codec can't decode
    byte 0x61 in position 0: truncated data

在选择编码机制时需要注意这一点。如果你需要导入 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 行

static int
pymain_run_command(wchar_t *command, PyCompilerFlags *cf)
{
    PyObject *unicode, *bytes;
    int ret;
    Unicode= PyUnicode_FromWideChar(command, -1);

字节顺序标记

当解码一个输入时,比如一个文件,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设置的。

>>> import sys; print(sys.byteorder)
'little'

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

import _codecs_iso2022, codecs
import _multibytecodec as mbc

codec = _codecs_iso2022.getcodec('iso2022_jp')

class Codec(codecs.Codec):
    encode = codec.encode
    decode = codec.decode

class IncrementalEncoder(mbc.MultibyteIncrementalEncoder,
                         codecs.IncrementalEncoder):
    codec = codec

class IncrementalDecoder(mbc.MultibyteIncrementalDecoder,
                         codecs.IncrementalDecoder):
    codec = codec

encodings 包也有一个模块,Lib/encodings/aliases.py,它包含一个 aliases 字典。这个字典用于给编码添加别名。例如,utf8utf-8u8 都是 utf_8 编码的别名。

编解码器模块

codecs 模块处理具有特定编码格式的数据的转换。可以分别使用 getencoder()getdecoder() 获取特定编码格式的编码或解码函数:

>>> iso2022_jp_encoder = codecs.getencoder('iso2022_jp')
>>> iso2022_jp_encoder('\u3072\u3068') # hi-to
(b'\x1b$B$R$H\x1b(B', 2)

编码函数将返回包含二进制结果和输出中的字节数的元组。 codecs 还实现了内置函数 open(),用于从操作系统打开文件句柄。

编解码器的实现

Unicode 对象(Objects/unicodeobject.c)的实现包含以下编码方法。

CodecEncoder

ascii

PyUnicode_EncodeASCII()

latin1

PyUnicode_EncodeLatin1()

UTF7

PyUnicode_EncodeUTF7()

UTF8

PyUnicode_EncodeUTF8()

UTF16

PyUnicode_EncodeUTF16()

UTF32

PyUnicode_EncodeUTF32()

unicode_escape

PyUnicode_EncodeUnicodeEscape()

raw_unicode_escape

PyUnicode_EncodeRawUnicodeEscape()

所有的解码方法都有类似的名字,只不过用 Decode 代替 Encode

其他编码的实现在 Modules/_codecs 中,以避免对主 Unicode 字符串对象的实现造成混乱。unicode_escaperaw_unicode_escape 编解码器属于 CPython 的内部实现。

内部的编解码器

CPython 带有许多内部编码。 这些是 CPython 所独有的,对于某些标准库函数以及生成源代码时很有用。

这些文本编码可用于任何文本输入或输出:

编解码器用途

idna

实现 RFC 3490

mbcs

根据 ANSI 代码页进行编码(仅限 Windows)

raw_unicode_escape

转换为 Python 源代码中的原始字面值

string_escape

转换为 Python 源代码中的字符串字面值

undefined

尝试使用系统默认编码

unicode_escape

转换为 Python 源代码中的 Unicode 字面值

unicode_internal

返回 CPython 的内部表示

还有一些仅限二进制的编码,需要与带有字节字符串输入的 codecs.encode()codecs.decode() 一起使用,例如:

>>> codecs.encode(b'hello world', 'base64')
b'aGVsbG8gd29ybGQ=\n'

以下是仅限二进制的编码列表:

编解码器别名用途

base64_codec

base64, base-64

转换为 MIME base64

bz2_codec

bz2

使用 bz2 压缩字符串

hex_codec

hex

转换为十六进制表示,每字节两位数

quopri_codec

quoted-printable

将操作数转换为 MIME 可打印字符引用编码

rot_13

rot13

返回凯撒加密(13 位移)

uu_codec

uu

使用 uuencode 转换

zlib_codec

zip, zlib

使用 gzip 压缩

例子

PyUnicode_Type 中,tp_richcompare 类型槽被分配给 PyUnicode_RichCompare() 函数。这个函数用于做字符串的比较,并可以调整为使用 ~= 操作符。你将要实现的函数行为是对两个字符串进行不区分大小写的比较。

首先,添加一个 case 语句来检查左右两边的字符串是否有二进制等价关系。

Objects/unicodeobject.c 第11361行

PyObject *
PyUnicode_RichCompare(PyObject *left, PyObject *right, int op)
{
...
if (left == right) {
switch (op) { case Py_EQ: case Py_LE:
>>> case Py_AlE: case Py_GE:
            /* a string is equal to itself */
            Py_RETURN_TRUE;

然后添加一个新的 else if 块来处理 Py_AlE 运算符。这将执行以下操作:

  1. 将左边的字符串转换为新的全大写字符串。

  2. 将右边的字符串转换为新的全大写字符串。

  3. 比较这两个字符串。

  4. 解除对两个临时字符串的引用,使它们被释放

  5. 返回结果。

你的代码应该像这样:

else if (op == Py_EQ || op == Py_NE) {
    ...
}
/* Add these lines */
else if (op == Py_AlE){
    PyObject* upper_left = case_operation(left, do_upper);
    PyObject* upper_right = case_operation(right, do_upper);
    result = unicode_compare_eq(upper_left, upper_right);
    Py_DECREF(upper_left);
    Py_DECREF(upper_right);
    return PyBool_FromLong(result);
}

重新编译后,你的不区分大小写的字符串比较应该在 REPL 上给出以下结果:

>>> "hello" ~= "HEllO"
True
>>> "hello?" ~= "hello"
False

Last updated