>>> from env_helper import info; info()
页面更新时间: 2024-03-29 16:20:49
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-18-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

1.4. 文本和字节序列

人类使用文本,计算机使用字节序列
—— Esther Nam 和 Travis Fischer “Character Encoding and Unicode in Python”
Python 3 明确区分了人类可读的文本字符串和原始的字节序列。
隐式地把字节序列转换成 Unicode 文本(的行为)已成过去。

1.4.1. 字符与编码

字符的标识,及码位,是 0~1114111 的数字,在 Unicode 标准中用 4-6 个十六进制数字表示,如 A 为 U+0041, 高音谱号为 U+1D11E,😂 为 U+1F602.
字符的具体表述取决于所用的编码。编码时在码位与字节序列自减转换时使用的算法。
把码位转换成字节序列的过程是编码,把字节序列转成码位的过程是解码

1.4.2. 序列类型

Python 内置了两种基本的二进制序列类型:不可变的 bytes 和可变的 bytearray

>>> # 基本的编码
>>> content = "São Paulo"
>>> for codec in ["utf_8", "utf_16"]:
>>>     print(codec, content.encode(codec))
>>>
>>> # UnicodeEncodeError
>>> try:
>>>     content.encode('cp437')
>>> except UnicodeEncodeError as e:
>>>     print(e)
>>>
>>> # 忽略无法编码的字符
>>> print(content.encode('cp437', errors='ignore'))
>>> # 把无法编码的字符替换成 ?
>>> print(content.encode('cp437', errors='replace'))
>>> # 把无法编码的字符替换成 xml 实体
>>> print(content.encode('cp437', errors='xmlcharrefreplace'))
>>>
>>> # 还可以自己设置错误处理方式
>>> # https://docs.python.org/3/library/codecs.html#codecs.register_error
utf_8 b'Sxc3xa3o Paulo'
utf_16 b'xffxfeSx00xe3x00ox00 x00Px00ax00ux00lx00ox00'
'charmap' codec can't encode character 'xe3' in position 1: character maps to <undefined>
b'So Paulo'
b'S?o Paulo'
b'S&#227;o Paulo'
>>> # 基本的解码
>>> # 处理 UnicodeDecodeError
>>> octets = b'Montr\xe9al'
>>> print(octets.decode('cp1252'))
>>> print(octets.decode('iso8859_7'))
>>> print(octets.decode('koi8_r'))
>>> try:
>>>     print(octets.decode('utf-8'))
>>> except UnicodeDecodeError as e:
>>>     print(e)
>>>
>>> # 将错误字符替换成 � (U+FFFD)
>>> octets.decode('utf-8', errors='replace')
Montréal
Montrιal
MontrИal
'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte
'Montr�al'
>>> # Python3 可以使用非 ASCII 名称
>>> São = 'Paulo'
>>> # 但是不能用 Emoji…

可以用 chardet 检测字符所使用的编码

BOM:字节序标记 (byte-order mark):
\ufffe 为字节序标记,放在文件开头,UTF-16 用它来表示文本以大端表示(\xfe\xff)还是小端表示(\xff\xfe)。
UTF-8 编码并不需要 BOM,但是微软还是给它加了 BOM,非常烦人。

1.4.3. 处理文本文件

处理文本文件的最佳实践是“三明治”:要尽早地把输入的字节序列解码成字符串,尽量晚地对字符串进行编码输出;在处理逻辑中只处理字符串对象,不应该去编码或解码。
除非想判断编码,否则不要再二进制模式中打开文本文件;即便如此,也应该使用 Chardet,而不是重新发明轮子。
常规代码只应该使用二进制模式打开二进制文件,比如图像。

1.4.4. 默认编码

可以使用 sys.getdefaultincoding() 获取系统默认编码;
Linux 的默认编码为 UTF-8,Windows 系统中不同语言设置使用的编码也不同,这导致了更多的问题。
locale.getpreferredencoding() 返回的编码是最重要的:这是打开文件的默认编码,也是重定向到文件的 sys.stdout/stdin/stderr 的默认编码。不过这个编码在某些系统中是可以改的…
所以,关于编码默认值的最佳建议是:别依赖默认值。

1.4.5. Unicode 编码方案

a = 'café'
b = 'cafe\u0301'
print(a, b)                       # café café
print(ascii(a), ascii(b))         # 'caf\xe9' 'cafe\u0301'
print(len(a), len(b), a == b)     # 4 5 False
在 Unicode 标准中,é 和 e:raw-latex:`u0`301 这样的序列叫“标准等价物”,应用程序应将它视为相同的字符。但 Python 看到的是不同的码位序列,因此判断两者不相同。
我们可以用 unicodedata.normalize 将 Unicode 字符串规范化。有四种规范方式:NFC, NFD, NFKC, NFKD
NFC 使用最少的码位构成等价的字符串,而 NFD 会把组合字符分解成基字符和单独的组合字符。
NFKC 和 NFKD 是出于兼容性考虑,在分解时会将字符替换成“兼容字符”,这种情况下会有格式损失。
兼容性方案可能会损失或曲解信息(如 “4²” 会被转换成 “42”),但可以为搜索和索引提供便利的中间表述。

使用 NFKC 和 NFKC 规范化形式时要小心,而且只能在特殊情况中使用,例如搜索和索引,而不能用户持久存储,因为这两种转换会导致数据损失。

>>> from unicodedata import normalize, name
>>> # Unicode 码位
>>> a = 'café'
>>> b = 'cafe\u0301'
>>> print(a, b)
>>> print(ascii(a), ascii(b))
>>> print(len(a), len(b), a == b)
>>>
>>> ## NFC 和 NFD
>>> print(len(normalize('NFC', a)), len(normalize('NFC', b)))
>>> print(len(normalize('NFD', a)), len(normalize('NFD', b)))
>>> print(len(normalize('NFC', a)) == len(normalize('NFC', b)))
>>>
>>> print('-' * 15)
>>> # NFKC & NFKD
>>> s = '\u00bd'
>>> l = [s, normalize('NFKC', s),  normalize('NFKD', s)]
>>> print(*l)
>>> print(*map(ascii, l))
>>> micro = 'μ'
>>> l = [s, normalize('NFKC', micro)]
>>> print(*l)
>>> print(*map(ascii, l))
>>> print(*map(name, l), sep='; ')
café café
'cafxe9' 'cafeu0301'
4 5 False
4 4
5 5
True
---------------
½ 1⁄2 1⁄2
'xbd' '1u20442' '1u20442'
½ μ
'xbd' 'u03bc'
VULGAR FRACTION ONE HALF; GREEK SMALL LETTER MU

1.4.6. Unicode 数据库

unicodedata 库中提供了很多关于 Unicode 的操作及判断功能,比如查看字符名称的 name,判断数字大小的 numric 等。
>>> import unicodedata
>>> print(unicodedata.name('½'))
>>> print(unicodedata.numeric('½'), unicodedata.numeric('卅'))
VULGAR FRACTION ONE HALF
0.5 30.0
>>> # 处理鬼符:按字节序将无法处理的字节序列依序替换成 \udc00 - \udcff 之间的码位
>>> x = 'digits-of-π'
>>> s = x.encode('gb2312')
>>> print(s)                                              # b'digits-of-\xa6\xd0'
>>> ascii_err = s.decode('ascii', 'surrogateescape')
>>> print(ascii_err)                                      # 'digits-of-\udca6\udcd0'
>>> print(ascii_err.encode('ascii', 'surrogateescape'))   # b'digits-of-\xa6\xd0'
b'digits-of-xa6xd0'
digits-of-��
b'digits-of-xa6xd0'