# 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 网站](https://unicode.org/charts/)上。

另一个需要支持 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` 和码点的十六进制值进行编码。

```python
>>> print("\u0107")
ć
```

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

```python
 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`。

```python
>>> "\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"` 创建二进制代码。

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

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

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

```python
>>> 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 行

```c
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`设置的。

```python
>>> 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`。

```python
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` 字典。这个字典用于给编码添加别名。例如，`utf8`、`utf-8` 和 `u8` 都是 `utf_8` 编码的别名。

## 编解码器模块

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

```python
>>> 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`）的实现包含以下编码方法。

| Codec                | Encoder                              |
| -------------------- | ------------------------------------ |
| `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_escape` 和 `raw_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()` 一起使用，例如：

```python
>>> 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行

```c
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. 返回结果。

你的代码应该像这样：

```c
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 上给出以下结果：

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