• 流畅的python——4 文本和字节序列


    四、文本和字节序列

    Unicode 字符串、二进制序列

    一个字符串是一个字符序列。从python3 的str对象中获取的元素是 Unicode字符,这相当于从Python2的 Unicode对象中获取的元素,而不是从Python2的str对象中获取的原始字节序列。

    字符的标识,即码位,是0-1114111的十进制数,在Unicode标准中以4-6个十六进制数表示,而且增加前缀 'U+',例如,A 的码位:U+0041,欧元符号的码位:U+20AC,高音符号的码位:U+1D11E。在 Unicode 6.3 中(这是 Python 3.4使用的标准),约 10% 的有效码位有对应的字符。

    字符的具体表述取决于所用的编码。编码是 码位 和 字节序列 之间转换时使用的算法。

    在 UTF-8 编码中,A(U+0041)的码位编码成单个字节 x41,而在 UTF-16LE编码中编码成两个字节 x41x00。再举个例子,欧元符号(U+20AC)在 UTF-8 编码中是三个字节xe2x82xac,而在 UTF-16LE 中编码成两个字节:xacx20

    把 Unicode 码位 转成 字节序列 的过程是编码;把 字节序列 转为 Unicode 码位 的过程是解码。

    字节序列:bytes 对象,字面量以 b 开头。

    虽然 Python 3 的 str 类型基本相当于 Python 2 的 unicode 类型,只不过是换了个新名称,但是 Python 3 的 bytes 类型却不是把 str 类型换个名称那么简单,而且还有关系紧密的 bytearray 类型。因此,在讨论编码和解码的问题之前,有必要先来介绍一下二进制序列类型。

    字节概要

    新的二进制序列类型在很多方面与 Python 2 的 str 类型不同。首先要知道,Python 内置了两种基本的二进制序列类型:Python 3 引入的不可变 bytes 类型和 Python 2.6 添加的可变bytearray 类型。(Python 2.6 也引入了 bytes 类型,但那只不过是 str 类型的别名,与Python 3 的 bytes 类型不同。)

    python3 str 是 Unicode字符,python2 str 是 字节序列

    bytes 或 bytearray 对象的各个元素是介于 0~255(含)之间的整数,而不像 Python 2的 str 对象那样是单个的字符。然而,二进制序列的切片始终是同一类型的二进制序列,包括长度为 1 的切片,如示例 4-2 所示。

    In [1]: c = bytes('cafe',encoding='utf-8')  # 构建 bytes 字节序列
    
    In [2]: c
    Out[2]: b'cafe'
    
    In [3]: c[0]  # 各个元素是 range(256) 的数字
    Out[3]: 99
    
    In [4]: c[:1]  # 切片是 bytes对象,即使切片只有一个字节
    Out[4]: b'c'
    
    In [5]: c1 = bytearray(c)
    
    In [6]: c1  # bytearray 没有字面量句法
    Out[6]: bytearray(b'cafe')
    
    In [8]: c1[0]  # 各个元素是 range(256) 的数字
    Out[8]: 99
    
    In [7]: c1[-1:]  # 切片是 bytearray 对象
    Out[7]: bytearray(b'e')
    

    my_bytes[0] 获取的是一个整数,而 my_bytes[:1] 返回的是一个长度为 1的 bytes 对象——这一点应该不会让人意外。s[0] == s[:1] 只对 str 这个序列类型成立。不过,str 类型的这个行为十分罕见。对其他各个序列类型来说,s[i] 返回一个元素,而 s[i:i+1] 返回一个相同类型的序列,里面是 s[i] 元素。

    In [10]: c[0] == c[:1]  # 序列类型 bytes 不成立
    Out[10]: False
    
    In [9]: c1[0] == c1[:1]  # 可变序列类型 bytearray 不成立
    Out[9]: False
    
    In [11]: cc = 'cafe'
    
    In [12]: cc[0] == cc[:1]  # 字符串类型 str 成立
    Out[12]: True
    

    虽然二进制序列其实是整数序列,但是它们的字面量表示法表明其中有 ASCII 文本。因此,各个字节的值可能会使用下列三种不同的方式显示。

    • 可打印的 ASCII 范围内的字节(从空格到 ~),使用 ASCII 字符本身。

    • 制表符、换行符、回车符和 对应的字节,使用转义序列 、 、 和 。

    • 其他字节的值,使用十六进制转义序列(例如,x00 是空字节)。

    除了格式化方法(format 和 format_map)和几个处理 Unicode 数据的方法(包括casefold、isdecimal、isidentifier、isnumeric、isprintable 和 encode)之外,str 类型的其他方法都支持 bytes 和 bytearray 类型。这意味着,我们可以使用熟悉的字符串方法处理二进制序列,如 endswith、replace、strip、translate、upper等,只有少数几个其他方法的参数是 bytes 对象,而不是 str 对象。此外,如果正则表达式编译自二进制序列而不是字符串,re 模块中的正则表达式函数也能处理二进制序列。Python 3.0~3.4 不能使用 % 运算符处理二进制序列,但是根据“PEP 461—Adding %formatting to bytes and bytearray”(https://www.python.org/dev/peps/pep-0461/),Python 3.5应该会支持。

    二进制序列有个类方法是 str 没有的,名为 fromhex,它的作用是解析十六进制数字对(数字对之间的空格是可选的),构建二进制序列:

    >>> bytes.fromhex('31 4B CE A9')
    b'1Kxcexa9'
    

    构建 bytes 或 bytearray 实例还可以调用各自的构造方法,传入下述参数

    • 一个 str 对象和一个 encoding 关键字参数。

    • 一个可迭代对象,提供 0~255 之间的数值。

    • 一个整数,使用空字节创建对应长度的二进制序列。[Python 3.5 会把这个构造方法标记为“过时的”,Python 3.6 会将其删除。参见“PEP 467—Minor API improvements for binary sequences”(https://www.python.org/dev/peps/pep-0467/)。]

    • 一个实现了缓冲协议的对象(如bytes、bytearray、memoryview、array.array);此时,把源对象中的字节序列复制到新建的二进制序列中。

    使用缓冲类对象构建二进制序列是一种低层操作,可能涉及类型转换。

    In [13]: import array
    
    In [14]: n = array.array('h',[-2,-1,0])  # 指定类型代码 h,创建一个短整数(16 位)数组。
    
    In [15]: n
    Out[15]: array('h', [-2, -1, 0])
    
    In [16]: o = bytes(n)
    
    In [17]: o  # 这些是表示那 3 个短整数的 6 个字节。
    Out[17]: b'xfexffxffxffx00x00'
    

    使用缓冲类对象创建 bytes 或 bytearray 对象时,始终复制源对象中的字节序列。与之相反,memoryview 对象允许在二进制数据结构之间共享内存。如果想从二进制序列中提取结构化信息,struct 模块是重要的工具。下一节会使用这个模块处理 bytes 和 memoryview 对象。

    结构体和内存视图

    struct 模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转换成打包的字节序列。struct 模块能处理bytes、bytearray 和 memoryview 对象。

    memoryview 类不是用于创建或存储字节序列的,而是共享内存,让你访问其他二进制序列、打包的数组和缓冲中的数据切片,而无需复制字节序列,例如Python Imaging Library(PIL)2 就是这样处理图像的。

    >>> import struct
    >>> fmt = '<3s3sHH'
    >>> with open('filter.gif', 'rb') as fp:
    ... img = memoryview(fp.read())
    ...
    >>> header = img[:10]
    >>> bytes(header)
    b'GIF89a+x02xe6x00'
    >>> struct.unpack(fmt, header)
    (b'GIF', b'89a', 555, 230)
    >>> del header
    >>> del img
    

    基本的编解码器

    Python 自带了超过 100 种编解码器(codec, encoder/decoder),用于在文本和字节之间相互转换。每个编解码器都有一个名称,如 'utf_8',而且经常有几个别名,如'utf8'、'utf-8' 和 'U8'。这些名称可以传给open()、str.encode()、bytes.decode() 等函数的 encoding 参数。示例 4-5 使用 3个编解码器把相同的文本编码成不同的字节序列。

    编码表

    某些编码(如 ASCII 和多字节的 GB2312)不能表示所有 Unicode字符。然而,UTF 编码的设计目的就是处理每一个 Unicode 码位。

    各种编码:

    latin1(即 iso8859_1)  

    一种重要的编码,是其他编码的基础,例如 cp1252 和 Unicode(注意,latin1 与cp1252 的字节值是一样的,甚至连码位也相同)。

    cp1252

      Microsoft 制定的 latin1 超集,添加了有用的符号,例如弯引号和€(欧元);有些Windows 应用把它称为“ANSI”,但它并不是 ANSI 标准。

    cp437

      IBM PC 最初的字符集,包含框图符号。与后来出现的 latin1 不兼容。

    gb2312

      用于编码简体中文的陈旧标准;这是亚洲语言中使用较广泛的多字节编码之一。

    utf-8

      目前 Web 中最常见的 8 位编码;3 与 ASCII 兼容(纯 ASCII 文本是有效的 UTF-8 文本)。

    3W3Techs 发布的“Usage of character encodings for websites”(https://w3techs.com/technologies/overview/character_encoding/all)报告指出,截至 2014 年 9 月,81.4% 的网站使用 UTF-8;而 Built With 发布的“Encoding Usage Statistics”(http://trends.builtwith.com/encoding)估计的比例则是79.4%。

    utf-16le

      UTF-16 的 16 位编码方案的一种形式;所有 UTF-16 支持通过转义序列(称为“代理对”,surrogate pair)表示超过 U+FFFF 的码位。

    UTF-16 取代了 1996 年发布的 Unicode 1.0 编码(UCS-2)。这个编码在很多系统中仍在使用,但是支持的最大码位是 U+FFFF。从 Unicode 6.3 起,分配的码位中有超过 50% 在 U+10000 以上,包括逐渐流行的表情符号(emoji pictograph)。

    了解编解码问题

    虽然有个一般性的 UnicodeError 异常,但是报告错误时几乎都会指明具体的异常:UnicodeEncodeError(把字符串转换成二进制序列时)或UnicodeDecodeError(把二进制序列转换成字符串时)。如果源码的编码与预期不符,加载 Python 模块时还可能抛出 SyntaxError。接下来的几节说明如何处理这些错误。

    出现与 Unicode 有关的错误时,首先要明确异常的类型。导致编码问题的是UnicodeEncodeError、UnicodeDecodeError,还是如 SyntaxError 的其他错误?解决问题之前必须清楚这一点。

    处理 UnicodeEncodeError

    In [30]:  city = 'São Paulo'
    
    In [31]: city.encode('utf_8')  # utf_? 覆盖任何字符
    Out[31]: b'Sxc3xa3o Paulo'
    
    In [33]: city.encode('utf16')
    Out[33]: b'xffxfeSx00xe3x00ox00 x00Px00ax00ux00lx00ox00'
    
    In [34]:  city.encode('iso8859_1')
    Out[34]: b'Sxe3o Paulo'
    
    In [35]: city.encode('cp437')  # cp437 不能编码,报错
    ---------------------------------------------------------------------------
    UnicodeEncodeError                        Traceback (most recent call last)
    <ipython-input-35-064a572fd5b6> in <module>
    ----> 1 city.encode('cp437')
    
    c:userswanglinappdatalocalprogramspythonpython36libencodingscp437.py in encode(self, input, errors)
         10
         11     def encode(self,input,errors='strict'):
    ---> 12         return codecs.charmap_encode(input,errors,encoding_map)
         13
         14     def decode(self,input,errors='strict'):
    
    UnicodeEncodeError: 'charmap' codec can't encode character 'xe3' in position 1: character maps to <undefined>
    
    In [36]: city.encode('cp437',errors='ignore')  # 跳过 不能编码的字符
    Out[36]: b'So Paulo'
    
    In [37]: city.encode('cp437',errors='replace')  # 用 ? 替换 不能编码的字符,可以得知不能编码的问题
    Out[37]: b'S?o Paulo'
    
    In [38]: city.encode('cp437',errors='xmlcharrefreplace')  # 替换为 XML实体 不能编码的字符
    Out[38]: b'S&#227;o Paulo'
    

    编解码器的错误处理方式是可扩展的。你可以为 errors 参数注册额外的字符串,方法是把一个名称和一个错误处理函数传给 codecs.register_error 函数。参见 codecs.register_error 函数的文档

    https://docs.python.org/3/library/codecs.html#codecs.register_error)。

    处理 UnicodeDecodeError

    In [39]:  octets = b'Montrxe9al'  # 这些字节序列是使用 latin1 编码的 “Montréal”;'xe9' 字节对应“é”。
    
    In [40]: octets.decode('cp1252')  # 可以使用 'cp1252'(Windows 1252)解码,因为它是 latin1 的有效超集。
    Out[40]: 'Montréal'
    
    In [41]:  octets.decode('iso8859_7')  # ISO-8859-7 用于编码希腊文,因此无法正确解释 'xe9' 字节,而且没有抛出错误。
    Out[41]: 'Montrιal'
    
    In [42]:  octets.decode('koi8_r')  #  KOI8-R 用于编码俄文;这里,'xe9' 表示西里尔字母“И”。
    Out[42]: 'MontrИal'
    
    In [43]:  octets.decode('utf_8')  # 'utf_8' 编解码器检测到 octets 不是有效的 UTF-8 字符串,抛出 UnicodeDecodeError。
    ---------------------------------------------------------------------------
    UnicodeDecodeError                        Traceback (most recent call last)
    <ipython-input-43-afaa3d3916c5> in <module>
    ----> 1 octets.decode('utf_8')
    
    UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte
    
    In [44]:  octets.decode('utf_8', errors='replace')  # 使用 'replace' 错误处理方式,xe9 替换成了“ ”(码位是 U+FFFD),这是官方指定的 REPLACEMENT CHARACTER(替换字符),表示未知字符。
    Out[44]: 'Montr�al'
        
    In [44]:  octets.decode('utf_8', errors='replace')
    Out[44]: 'Montr�al'
    
    In [45]:  octets.decode('utf_8', errors='ignore')
    Out[45]: 'Montral'
    
    In [46]:  octets.decode('utf_8', errors='xmlcharrefreplace')
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-46-b0e050422a60> in <module>
    ----> 1 octets.decode('utf_8', errors='xmlcharrefreplace')
    
    TypeError: don't know how to handle UnicodeDecodeError in error callback
    

    使用预期之外的编码加载模块时抛出的 SyntaxError

    Python 3 默认使用 UTF-8 编码源码,Python 2(从 2.5 开始)则默认使用 ASCII。如果加载的 .py 模块中包含 UTF-8 之外的数据,而且没有声明编码,会得到类似下面的消息:

    SyntaxError: Non-UTF-8 code starting with 'xe1' in file ola.py on line
     1, but no encoding declared; see http://python.org/dev/peps/pep-0263/
     for details
    

    GNU/Linux 和 OS X 系统大都使用 UTF-8,因此打开在 Windows 系统中使用 cp1252 编码的 .py 文件时可能发生这种情况。注意,这个错误在 Windows 版 Python 中也可能会发生,因为 Python 3 为所有平台设置的默认编码都是 UTF-8。

    用 coding 解决

    示例 4-8 ola.py:“你好,世界!”的葡萄牙语版

    # coding: cp1252
    
    print('Olá, Mundo!')
    

    现在,Python 3 的源码不再限于使用 ASCII,而是默认使用优秀的 UTF-8 编码,因此要修正源码的陈旧编码(如 'cp1252')问题,最好将其转换成 UTF-8,别去麻烦 coding 注释。如果你用的编辑器不支持 UTF-8,那么是时候换一个了。

    源码中能不能使用非 ASCII 名称

    Python 3 允许在源码中使用非 ASCII 标识符:

    >>> ação = 'PBR' # ação = stock
    >>> ε = 10**-6 # ε = epsilon
    

    选择不同的标识符,要因地域而异,选择最适合的标识符,不同地方键盘不同,可以打出一些本地的字符,代码可能更容易阅读和编写。

    如何找出字节序列的编码

    如何找出字节序列的编码?简单来说,不能。必须有人告诉你。

    有些通信协议和文件格式,如 HTTP 和 XML,包含明确指明内容编码的首部。可以肯定的是,某些字节流不是 ASCII,因为其中包含大于 127 的字节值,而且制定 UTF-8 和UTF-16 的方式也限制了可用的字节序列。不过即便如此,我们也不能根据特定的位模式来 100% 确定二进制文件的编码是 ASCII 或 UTF-8。

    然而,就像人类语言也有规则和限制一样,只要假定字节流是人类可读的纯文本,就可能通过试探和分析找出编码。例如,如果 b'x00' 字节经常出现,那么可能是 16 位或 32位编码,而不是 8 位编码方案,因为纯文本中不能包含空字符;如果字节序列b'x20x00' 经常出现,那么可能是 UTF-16LE 编码中的空格字符(U+0020),而不是鲜为人知的 U+2000 EN QUAD 字符——谁知道这是什么呢!

    统一字符编码侦测包 Chardet(https://pypi.python.org/pypi/chardet)就是这样工作的,它能识别所支持的 30 种编码。Chardet 是一个 Python 库,可以在程序中使用,不过它也提供了命令行工具 chardetect。下面是它对本章书稿文件的检测报告:

    $ chardetect 04-text-byte.asciidoc
    04-text-byte.asciidoc: utf-8 with confidence 0.99
    

    二进制序列编码文本通常不会明确指明自己的编码,但是 UTF 格式可以在文本内容的开头添加一个字节序标记。

    BOM:有用的鬼符

    UTF-16 编码的序列开头有几个额外的字节,如下所示:

    >>> u16 = 'El Niño'.encode('utf_16')
    >>> u16
    b'xffxfeEx00lx00 x00Nx00ix00xf1x00ox00'
    
    In [53]: u161 = 'E'.encode('utf_16')
    
    In [54]: u16 = 'l'.encode('utf_16')
    
    In [55]: u161
    Out[55]: b'xffxfeEx00'
    
    In [56]: u16
    Out[56]: b'xffxfelx00'
    

    BOM 是 b'xffxfe' ,即字节序标记(byte-order mark),指明编码时使用 Intel CPU 的小字节序。

    在小字节序设备中,各个码位的最低有效字节在前面:字母 'E' 的码位是 U+0045(十进制数 69),在字节偏移的第 2 位和第 3 位编码为 69 和 0。

    >>> list(u16)
    [255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
    

    在大字节序 CPU 中,编码顺序是相反的;'E' 编码为 0 和 69。

    为了避免混淆,UTF-16 编码在要编码的文本前面加上特殊的不可见字符 ZERO WIDTH NO-BREAK SPACE(U+FEFF)。在小字节序系统中,这个字符编码为 b'xffxfe'(十进制数 255, 254)。因为按照设计,U+FFFE 字符不存在,在小字节序编码中,字节序列 b'xffxfe' 必定是 ZERO WIDTH NO-BREAK SPACE,所以编解码器知道该用哪个字节序。

    UTF-16 有两个变种:UTF-16LE,显式指明使用小字节序;UTF-16BE,显式指明使用大字节序。如果使用这两个变种,不会生成 BOM:

    >>> u16le = 'El Niño'.encode('utf_16le')
    >>> list(u16le)
    [69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
    >>> u16be = 'El Niño'.encode('utf_16be')
    >>> list(u16be)
    [0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]
    

    如果有 BOM,UTF-16 编解码器会将其过滤掉,为你提供没有前导 ZERO WIDTH NO BREAK SPACE 字符的真正文本。根据标准,如果文件使用 UTF-16 编码,而且没有BOM,那么应该假定它使用的是 UTF-16BE(大字节序)编码。然而,Intel x86 架构用的是小字节序,因此有很多文件用的是不带 BOM 的小字节序 UTF-16 编码。

    与字节序有关的问题只对一个字(word)占多个字节的编码(如 UTF-16 和 UTF-32)有影响。UTF-8 的一大优势是,不管设备使用哪种字节序,生成的字节序列始终一致,因此不需要 BOM。尽管如此,某些 Windows 应用(尤其是 Notepad)依然会在 UTF-8 编码的文件中添加 BOM;而且,Excel 会根据有没有 BOM 确定文件是不是 UTF-8 编码,否则,它假设内容使用 Windows 代码页(codepage)编码。UTF-8 编码的 U+FEFF 字符是一个三字节序列:b'xefxbbxbf'。因此,如果文件以这三个字节开头,有可能是带有 BOM的 UTF-8 文件。然而,Python 不会因为文件以 b'xefxbbxbf' 开头就自动假定它是UTF-8 编码的。

    In [64]: u8 = 'El Niño'.encode('utf_8')
    
    In [65]: u8
    Out[65]: b'El Nixc3xb1o'
    

    处理文本文件

    处理文本的最佳实践是“Unicode 三明治”。

    意思是,要尽早把输入(例如读取文件时)的字节序列解码成字符串。

    这种三明治中的“肉片”是程序的业务逻辑,在这里只能处理字符串对象。在其他处理过程中,一定不能编码或解码。

    对输出来说,则要尽量晚地把字符串编码成字节序列。

    多数 Web 框架都是这样做的,使用框架时很少接触字节序列。例如,在 Django 中,视图应该输出 Unicode 字符串;Django 会负责把响应编码成字节序列,而且默认使用 UTF-8 编码。

    Unicode三明治

    在 Python 3 中能轻松地采纳 Unicode 三明治的建议,因为内置的 open 函数会在读取文件时做必要的解码,以文本模式写入文件时还会做必要的编码,所以调用 my_file.read() 方法得到的以及传给 my_file.write(text) 方法的都是字符串对象。

    Python 2.6 或 Python 2.7 用户要使用 io.open() 函数才能得到读写文件时自动执行的解码和编码操作。

    可以看出,处理文本文件很简单。但是,如果依赖默认编码,你会遇到麻烦。

    没有指定编码参数,会使用区域设置中的默认编码

    In [67]: open(r'D:cafe.txt', 'w', encoding='utf_8').write('café')  # 注意 不是 e
    Out[67]: 4
    
    In [68]: open(r'D:cafe.txt').read()
    Out[68]: 'caf茅'
    

    需要在多台设备中或多种场合下运行的代码,一定不能依赖默认编码。打开文件时始终应该明确传入 encoding= 参数,因为不同的设备使用的默认编码可能不同,有时隔一天也会发生变化。

    >>> fp = open('cafe.txt', 'w', encoding='utf_8')
    >>> fp  # 默认情况下,open 函数采用文本模式,返回一个 TextIOWrapper 对象。
    <_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
    >>> fp.write('café')  # 写入的 Unicode 字符数,注意不是 e
    4
    >>> fp.close()
    >>> import os
    >>> os.stat('cafe.txt').st_size  # os.stat 报告文件中有 5 个字节;UTF-8 编码的 'é' 占两个字节,0xc3 和 0xa9。
    5
    >>> fp2 = open('cafe.txt')
    >>> fp2
    <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
    >>> fp2.encoding
    'cp1252'
    >>> fp2.read()
    'café'
    >>> fp3 = open('cafe.txt', encoding='utf_8')
    >>> fp3
    <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
    >>> fp3.read()
    'café'
    >>> fp4 = open('cafe.txt', 'rb')
    >>> fp4  # rb 模式,返回的是 BufferedReader 对象,而不是 TextIOWrapper 对象。
    <_io.BufferedReader name='cafe.txt'>
    >>> fp4.read()  # 这是 5个字节,c, a, f, xc3, xa9
    b'cafxc3xa9'
    

    除非想判断编码,否则不要在二进制模式中打开文本文件;即便如此,也应该使用 Chardet,而不是重新发明轮子。常规代码只应该使用二进制模式打开二进制文件,如光栅图像。

    Unicode 字符串 规范化

    因为 Unicode 有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。

    例如,“café”这个词可以使用两种方式构成,分别有 4 个和 5 个码位,但是结果完全一样:

    U+0301 是 COMBINING ACUTE ACCENT,加在“e”后面得到“é”。在 Unicode 标准中,'é'和 'eu0301' 这样的序列叫“标准等价物”(canonical equivalent),应用程序应该把它们视作相同的字符。但是,Python 看到的是不同的码位序列,因此判定二者不相等。

    这个问题的解决方案是使用 unicodedata.normalize 函数提供的 Unicode 规范化。这个函数的第一个参数是这 4 个字符串中的一个:'NFC'、'NFD'、'NFKC' 和 'NFKD'。下面先说明前两个。

    NFC(Normalization Form C)使用最少的码位构成等价的字符串,而 NFD 把组合字符分解成基字符和单独的组合字符。这两种规范化方式都能让比较行为符合预期:

    In [95]: s1 = 'café'
    
    In [96]: s2 = 'cafeu0301'
    
    In [97]: len(s1)
    Out[97]: 4
    
    In [98]: len(s2)
    Out[98]: 5
    
    In [82]: from unicodedata import normalize
    
    In [83]: s1 == s2
    Out[83]: False
    
    In [84]: normalize('NFC',s1) == normalize('NFC',s2)
    Out[84]: True
        
    In [89]: len(normalize('NFC',s1))
    Out[89]: 4
    
    In [90]: len(normalize('NFD',s1))
    Out[90]: 5
    

    西方键盘通常能输出组合字符,因此用户输入的文本默认是 NFC 形式。不过,安全起见,保存文本之前,最好使用 normalize('NFC', user_text) 清洗字符串。NFC 也是W3C 的“Character Model for the World Wide Web: String Matching and Searching”规范(https://www.w3.org/TR/charmod-norm/)推荐的规范化形式。

    使用 NFC 时,有些单字符会被规范成另一个单字符。例如,电阻的单位欧姆(Ω)会被规范成希腊字母大写的欧米加。这两个字符在视觉上是一样的,但是比较时并不相等,因此要规范化,防止出现意外:

    In [99]: from unicodedata import normalize,name
    
    In [100]: ohm = 'u2126'
    
    In [101]: ohm
    Out[101]: 'Ω'
    
    In [102]: name(ohm)
    Out[102]: 'OHM SIGN'
    
    In [103]: ohm_c = normalize('NFC',ohm)
    
    In [104]: ohm_c
    Out[104]: 'Ω'
    
    In [105]: name(ohm_c)
    Out[105]: 'GREEK CAPITAL LETTER OMEGA'
    
    In [106]: ohm == ohm_c
    Out[106]: False
    
    In [107]: normalize('NFC',ohm_c)
    Out[107]: 'Ω'
    
    In [108]: name(normalize('NFC',ohm_c))
    Out[108]: 'GREEK CAPITAL LETTER OMEGA'
    
    In [109]: normalize('NFC',ohm) == normalize('NFC',ohm_c)
    Out[109]: True
    

    在另外两个规范化形式(NFKC 和 NFKD)的首字母缩略词中,字母 K 表 示“compatibility”(兼容性)。这两种是较严格的规范化形式,对“兼容字符”有影响。虽然 Unicode 的目标是为各个字符提供“规范的”码位,但是为了兼容现有的标准,有些字符会出现多次。例如,虽然希腊字母表中有“μ”这个字母(码位是 U+03BC,GREEK SMALL LETTER MU),但是 Unicode 还是加入了微符号 'µ'(U+00B5),以便与 latin1 相互转换。因此,微符号是一个“兼容字符”。

    在 NFKC 和 NFKD 形式中,各个兼容字符会被替换成一个或多个“兼容分解”字符,即便这样有些格式损失,但仍是“首选”表述——理想情况下,格式化是外部标记的职责,不应该由 Unicode 处理。下面举个例子。二分之一 '½'(U+00BD)经过兼容分解后得到的是三个字符序列 '1/2';微符号 'µ'(U+00B5)经过兼容分解后得到的是小写字母'μ'(U+03BC)。

    微符号是“兼容字符”,而欧姆符号不是,这还真是奇怪。因此,NFC 不会改动微符号,但是会把欧姆符号改成大写的欧米加;而 NFKC 和 NFKD 会把欧姆和微符号都改成其他字符。

    下面是 NFKC 的具体应用:

    >>> from unicodedata import normalize, name
    >>> half = '½'
    >>> normalize('NFKC', half)
    '1⁄2'
    >>> four_squared = '4²'
    >>> normalize('NFKC', four_squared)
    '42'
    >>> micro = 'μ'
    >>> micro_kc = normalize('NFKC', micro)
    >>> micro, micro_kc
    ('μ', 'μ')
    >>> ord(micro), ord(micro_kc)
    (181, 956)
    >>> name(micro), name(micro_kc)
    ('MICRO SIGN', 'GREEK SMALL LETTER MU')
    

    使用 '1/2' 替代 '½' 可以接受,微符号也确实是小写的希腊字母 'µ',但是把 '4²' 转换成 '42' 就改变原意了。某些应用程序可以把 '4²' 保存为 '42',但是normalize 函数对格式一无所知。因此,NFKC 或 NFKD 可能会损失或曲解信息,但是可以为搜索和索引提供便利的中间表述:用户搜索 '1 / 2 inch' 时,如果还能找到包含 '½ inch' 的文档,那么用户会感到满意。

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

    为搜索或索引准备文本时,还有一个有用的操作,即下一节讨论的大小写折叠。

    大小写折叠

    大小写折叠其实就是把所有文本变成小写,再做些其他转换。这个功能由 str.casefold() 方法(Python 3.3 新增)支持。

    对于只包含 latin1 字符的字符串 s,s.casefold() 得到的结果与 s.lower() 一样,唯有两个例外:微符号 'µ' 会变成小写的希腊字母“μ”(在多数字体中二者看起来一样);德语 Eszett(“sharp s”,ß)会变成“ss”。

    自 Python 3.4 起,str.casefold() 和 str.lower() 得到不同结果的有 116 个码位。Unicode 6.3 命名了 110 122 个字符,这只占 0.11%。

    IIn [118]: m1 = 'Μ'
    
    In [119]: m1.casefold()
    Out[119]: 'μ'
    
    In [121]: m1.casefold().casefold()
    Out[121]: 'μ'
    
    In [122]: name(m1)
    Out[122]: 'GREEK CAPITAL LETTER MU'
        
    In [123]: m1.lower()
    Out[123]: 'μ'
    

    规范化文本匹配实用函数

    由前文可知,NFC 和 NFD 可以放心使用,而且能合理比较 Unicode 字符串。对大多数应用来说,NFC 是最好的规范化形式。不区分大小写的比较应该使用 str.casefold()。

    如果要处理多语言文本,工具箱中应该有示例中的 nfc_equal 和 fold_equal 函数。

    from unicodedata import normalize
    
    def nfc_equal(str1, str2):
        return normalize('NFC', str1) == normalize('NFC', str2)
    def fold_equal(str1, str2):
        return (normalize('NFC', str1).casefold() == 
                normalize('NFC', str2).casefold())
    

    极端规范化:去掉变音符号

    Google 搜索涉及很多技术,其中一个显然是忽略变音符号(如重音符、下加符等),至少在某些情况下会这么做。去掉变音符号不是正确的规范化方式,因为这往往会改变词的意思,而且可能误判搜索结果。但是对现实生活却有所帮助:人们有时很懒,或者不知道怎么正确使用变音符号,而且拼写规则会随时间变化,因此实际语言中的重音经常变来变去。

    除了搜索,去掉变音符号还能让 URL 更易于阅读,至少对拉丁语系语言是如此。下面是维基百科中介绍圣保罗市(São Paulo)的文章的 URL:

    http://en.wikipedia.org/wiki/S%C3%A3o_Paulo
    

    其中,“%C3%A3”是 UTF-8 编码“ã”字母(带有波形符的“a”)转义后得到的结果。下述形式更友好,尽管拼写是错误的:

    http://en.wikipedia.org/wiki/Sao_Paulo
    

    如果想把字符串中的所有变音符号都去掉,可以使用示例中的函数

    import unicodedata
    import string
    def shave_marks(txt):
        """去掉全部组合符号"""
        norm_txt = unicodedata.normalize('NFD', txt)  # 分解为:基字符 和 组合符号
    	# 过滤 所有组合符号
        shaved = ''.join(c for c in norm_txt if not unicodedata.combining(c))
        return unicodedata.normalize('NFC', shaved)  # 重组所有字符
    

    删除拉丁字母中组合记号的函数(import 语句省略了,因为这是示例中定义的 sanitize.py 模块的一部分)

    def shave_marks_latin(txt):
        """把拉丁基字符中所有的变音符号删除"""
        norm_txt = unicodedata.normalize('NFD', txt)
        latin_base = False
        keepers = []
        for c in norm_txt:
            if unicodedata.combining(c) and latin_base:
                continue # 忽略拉丁基字符上的变音符号
                keepers.append(c)
                # 如果不是组合字符,那就是新的基字符
                if not unicodedata.combining(c):
                    latin_base = c in string.ascii_letters
                    shaved = ''.join(keepers)
                    return unicodedata.normalize('NFC', shaved)
    
    single_map = str.maketrans(""",ƒ,,†ˆ‹‘’“”•––˜›""",  """'f"*^<''""---~>""")
    

    str.maketrans 方法

    In [4]: single_map = str.maketrans('aaa','123')
    
    In [5]: single_map
    Out[5]: {97: 51}
    
    In [6]: chr(51)
    Out[6]: '3'
    
    In [7]: single_map = str.maketrans({'a':'1'})
    
    In [8]: single_map
    Out[8]: {97: '1'}
    
    In [9]: single_map = str.maketrans({'aaa':'231'})
    ---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    <ipython-input-9-3c86e2fcdb8e> in <module>
    ----> 1 single_map = str.maketrans({'aaa':'231'})
    
    ValueError: string keys in translate table must be of length 1
    
    # 使用
    In [10]: txt = 'abc'
    
    In [11]: single_map
    Out[11]: {97: '1'}
    
    In [12]: txt.translate(single_map)
    Out[12]: '1bc'
    

    示例 把一些西文印刷字符转换成 ASCII 字符

    single_map = str.maketrans(""",ƒ,,†ˆ‹‘’“”•––˜›""",
                               """'f"*^<''""---~>""")
    multi_map = str.maketrans({
        '€': '<euro>',
        '…': '...',
        'OE': 'OE',
        '™': '(TM)',
        'oe': 'oe',
        '‰': '<per mille>',
        '‡': '**',
    })
    multi_map.update(single_map)
    def dewinize(txt):
        """把Win1252符号替换成ASCII字符或序列"""
        return txt.translate(multi_map)
    def asciize(txt):
        no_marks = shave_marks_latin(dewinize(txt))
        no_marks = no_marks.replace('ß', 'ss')
        return unicodedata.normalize('NFKC', no_marks)
    

    Unicode 文本排序

    python 比较任何类型的序列时,会一一比较序列中的各个元素。对字符串来说,比较的是码位。但是在比较 非ASCII 字符时,得到的结果不尽如人意。

    In [13]: fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
    
    In [14]: sorted(fruits)
    Out[14]: ['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
    
    In [15]: fruits
    Out[15]: ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
    

    不同的区域采用的排序规则有所不同,葡萄牙语等很多语言按照拉丁字母表排序,重音符号和下加符对排序几乎没什么影响。9 因此,排序时“cajá”视作“caja”,必定排在“caju”前面。

    变音符号对排序有影响的情况很少发生,只有两个词之间唯有变音符号不同时才有影响。此时,带有变音符号的词排在常规词的后面。

    然而,python3.6

    In [19]: fruits = ['cajuf', 'atemoia', 'cajáe', 'açaí', 'acerola']
    
    In [20]: sorted(fruits)
    Out[20]: ['acerola', 'atemoia', 'açaí', 'cajuf', 'cajáe']
    

    在 Python 中,非 ASCII 文本的标准排序方式是使用 locale.strxfrm 函数,根据 locale模块的文档(https://docs.python.org/3/library/locale.html?highlight=strxfrm#locale.strxfrm),这 个函数会“把字符串转换成适合所在区域进行比较的形式”。

    使用 locale.strxfrm 函数之前,必须先为应用设定合适的区域设置,还要祈祷操作系统支持这项设置。

    >>> import locale
    >>> locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
    'pt_BR.UTF-8'
    >>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
    >>> sorted_fruits = sorted(fruits, key=locale.strxfrm)
    >>> sorted_fruits
    ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
    

    因此,使用 locale.strxfrm 函数做排序键之前,要调用 setlocale(LC_COLLATE, «your_locale»)。

    不过,有几点要注意。

    • 区域设置是全局的,因此不推荐在库中调用 setlocale 函数。应用或框架应该在进程启动时设定区域设置,而且此后不要再修改。

    • 操作系统必须支持区域设置,否则 setlocale 函数会抛出 locale.Error: unsupported locale setting 异常。

    • 必须知道如何拼写区域名称。它在 Unix 衍生系统中几乎已经形成标准,要通过'language_code.encoding' 获取。10 但是在 Windows 中,句法复杂一些:Language Name-Language Variant_Region Name.codepage。注意,“Language Name”(语言名称)、“Language Variant”(语言变体)和“RegionName”(区域名)中可以包含空格;除了第一部分之外,其他部分的前面是不同的字符:一个连字符、一个下划线和一个点号。除了语言名称之外,其他部分好像都是可选的。例如,English_United States.850,它的语言名称是“English”,区域是“United States”,代码页是“850”。Windows 能理解的语言名称和区域名见于 MSDN中的文章“Language Identifier Constants and Strings”(https://msdn.microsoft.com/en-us/library/dd318693.aspx),还有“Code Page Identifiers”(https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756(v=vs.85).aspx)一文列出了最后一部分的代码页数字。

    • 操作系统的制作者必须正确实现了所设的区域。我在 Ubuntu 14.04 中成功了,但在OS X(Mavericks 10.9)中却失败了。在两台 Mac 中,调用setlocale(LC_COLLATE, 'pt_BR.UTF-8') 返回的都是字符串 'pt_BR.UTF-8',没有任何问题。但是,sorted(fruits, key=locale.strxfrm) 得到的结果与sorted(fruits) 一样,是错误的。我还在 OS X 中尝试了 fr_FR、es_ES 和de_DE,但是 locale.strxfrm 并未起作用。

    在 Linux 操作系统中,中国大陆的读者可以使用 zh_CN.UTF-8,简体中文会按照汉语拼音顺序进行排序,它也能对葡萄牙语进行正确排序。——编者注

    使用Unicode排序算法排序

    James Tauber,一位高产的 Django 贡献者,他一定是感受到了这一痛点,因此开发了PyUCA 库https://pypi.python.org/pypi/pyuca/),这是 Unicode 排序算法(UnicodeCollation Algorithm,UCA)的纯 Python 实现

    In [1]: fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
    
    In [2]: sorted(fruits)
    Out[2]: ['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
    
    In [3]: import pyuca
    
    In [4]: coll = pyuca.Collator()
    
    In [5]: sorted(fruits,key=coll.sort_key)
    Out[5]: ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
    

    PyUCA 没有考虑区域设置。如果想定制排序方式,可以把自定义的排序表路径传给 Collator() 构造方法。PyUCA 默认使用项目自带的 allkeys.txt(https://github.com/jtauber/pyuca),这就是 Unicode 6.3.0 的“Default Unicode Collation Element Table”(http://www.unicode.org/Public/UCA/6.3.0/allkeys.txt)的副本。

    Unicode 数据库

    Unicode 标准提供了一个完整的数据库(许多格式化的文本文件),不仅包括码位与字符名称之间的映射,还有各个字符的元数据,以及字符之间的关系。例如,Unicode 数据库记录了字符是否可以打印、是不是字母、是不是数字,或者是不是其他数值符号。字符串的 isidentifier、isprintable、isdecimal 和 isnumeric 等方法就是靠这些信息作判断的。 str.casefold 方法也用到了 Unicode 表中的信息。

    import unicodedata
    import re
    re_digit = re.compile(r'd')
    sample = '1xbcxb2u0969u136bu216bu2466u2480u3285'
    for char in sample:
        print('U+%04x' % ord(char),
              char.center(6),
              're_dig' if re_digit.match(char) else '-',
              'isdig' if char.isdigit() else '-',
              'isnum' if char.isnumeric() else '-',
              format(unicodedata.numeric(char), '5.2f'),
              unicodedata.name(char),
              sep='	')
    

    结果:

    U+0031    1     re_dig  isdig   isnum    1.00   DIGIT ONE
    U+00bc    ¼     -       -       isnum    0.25   VULGAR FRACTION ONE QUARTER
    U+00b2    ²     -       isdig   isnum    2.00   SUPERSCRIPT TWO
    U+0969    ३     re_dig  isdig   isnum    3.00   DEVANAGARI DIGIT THREE
    U+136b    ፫     -       isdig   isnum    3.00   ETHIOPIC DIGIT THREE
    U+216b    Ⅻ    -       -       isnum   12.00   ROMAN NUMERAL TWELVE
    U+2466    ⑦    -       isdig   isnum    7.00   CIRCLED DIGIT SEVEN
    U+2480    ⒀    -       -       isnum   13.00   PARENTHESIZED NUMBER THIRTEEN
    U+3285    ㊅    -       -       isnum    6.00   CIRCLED IDEOGRAPH SIX
    

    表明,正则表达式 r'd' 能匹配数字“1”和梵文数字 3,但是不能匹配 isdigit 方法判断为数字的其他字符。re 模块对 Unicode 的支持并不充分。PyPI 中有个新开发的 regex 模块,它的最终目的是取代 re 模块,以提供更好的 Unicode 支持。

    支持字符串和字节序列的双模式API

    1 正则表达式中的字符串和字节序列

    使用字节序列构建正则表达式,d和w等模式只能匹配ASCII字符。

    使用字符串模式,就能匹配ASCII之外的Unicode数字或字母。

    text_str = ("Ramanujan saw u0be7u0bedu0be8u0bef" " as 1729 = 1³ + 12³ = 9³ + 10³.")
    
    import re
    re_numbers_str = re.compile(r'd+')  # 字符串
    re_words_str = re.compile(r'w+')
    re_numbers_bytes = re.compile(rb'd+')  # 字节序列
    re_words_bytes = re.compile(rb'w+')
    text_str = ("Ramanujan saw u0be7u0bedu0be8u0bef"  # 1729
                " as 1729 = 1³ + 12³ = 9³ + 10³.")  # 编译时,两个字符串拼接起来
    text_bytes = text_str.encode('utf_8')
    print('Text', repr(text_str), sep='
     ')
    print('Bytes', repr(text_bytes), sep='
     ')
    print('Numbers')
    print(' str :', re_numbers_str.findall(text_str))
    print(' bytes:', re_numbers_bytes.findall(text_bytes))
    print('Words')
    print(' str :', re_words_str.findall(text_str))
    print(' bytes:', re_words_bytes.findall(text_bytes))
    
    Text
     'Ramanujan saw ௧௭௨௯ as 1729 = 1³ + 12³ = 9³ + 10³.'
    Bytes
     b'Ramanujan saw xe0xafxa7xe0xafxadxe0xafxa8xe0xafxaf as 1729 = 1xc2xb3 + 12xc2xb3 = 9xc2xb3 + 10xc2xb3.'
    Numbers
     str : ['௧௭௨௯', '1729', '1', '12', '9', '10']
     bytes: [b'1729', b'1', b'12', b'9', b'10']
    Words
     str : ['Ramanujan', 'saw', '௧௭௨௯', 'as', '1729', '1³', '12³', '9³', '10³']
     bytes: [b'Ramanujan', b'saw', b'as', b'1729', b'1', b'12', b'9', b'10']
        
    print(' str match bytes :', re_numbers_str.findall(text_bytes))
    
    Traceback (most recent call last):
      File ".aaa.py", line 17, in <module>
        print(' str match bytes :', re_numbers_str.findall(text_bytes))
    TypeError: cannot use a string pattern on a bytes-like object
        
        
    print(' bytes match str :', re_numbers_bytes.findall(text_str))
    
    Traceback (most recent call last):
      File ".aaa.py", line 18, in <module>
        print(' bytes match str :', re_numbers_bytes.findall(text_str))
    TypeError: cannot use a bytes pattern on a string-like object
    
    结论:

    字节序列:不能匹配泰米尔数字,也不能匹配上标。只能匹配 ASCII 字符。

    不能混合搜索。

    字符串模式 r'd+' 能匹配泰米尔数字和 ASCII 数字。

    字节序列模式 rb'd+'只能匹配 ASCII 字节中的数字。

    字符串模式 r'w+' 能匹配字母、上标、泰米尔数字和 ASCII 数字。

    字节序列模式 rb'w+' 只能匹配 ASCII 字节中的字母和数字。

    ASCII 范围外的字节不会当成数字和组成单词的字母。

    字符串正则表达式有个 re.ASCII 标志,它让 w、W、、B、d、D、s 和 S 只匹配 ASCII 字符。详情参阅 re 模块的文档(https://docs.python.org/3/library/re.html)。

    2 os 函数中的字符串和字节序列

    GNU/Linux 内核不理解 Unicode,因此你可能发现了,对任何合理的编码方案来说,在文件名中使用字节序列都是无效的,无法解码成字符串。在不同操作系统中使用各种客户端的文件服务器,在遇到这个问题时尤其容易出错。

    为了规避这个问题,os 模块中的所有函数、文件名或路径名参数既能使用字符串,也能使用字节序列。如果这样的函数使用字符串参数调用,该参数会使用 sys.getfilesystemencoding() 得到的编解码器自动编码,然后操作系统会使用相同的编解码器解码。这几乎就是我们想要的行为,与 Unicode 三明治最佳实践一致。

    >>> os.listdir('.')
    ['abc.txt', 'digits-of-π.txt']
    >>> os.listdir(b'.')
    [b'abc.txt', b'digits-of-xcfx80.txt']
    

    为了便于手动处理字符串或字节序列形式的文件名或路径名,os 模块提供了特殊的编码和解码函数。

    fsencode(filename)

      如果 filename 是 str 类型(此外还可能是 bytes 类型),使用 sys.getfilesystemencoding() 返回的编解码器把 filename 编码成字节序列;否则,返回未经修改的 filename 字节序列。

    fsdecode(filename)

      如果 filename 是 bytes 类型(此外还可能是 str 类型),使用 sys.getfilesystemencoding() 返回的编解码器把 filename 解码成字符串;否则,返回未经修改的 filename 字符串。

    处理 解码 时的 鬼符:乱码的符号

    在 Unix 衍生平台中,这些函数使用 surrogateescape 错误处理方式(参见下述附注栏)以避免遇到意外字节序列时卡住。Windows 使用的错误处理方式是 strict。

    使用 surrogateescape 处理鬼符

    Python 3.1 引入的 surrogateescape 编解码器错误处理方式是处理意外字节序列或未知编码的一种方式,它的说明参见“PEP 383 — Non-decodable Bytes in SystemCharacter Interfaces”(https://www.python.org/dev/peps/pep-0383/)。

    这种错误处理方式会把每个**无法解码的字节 ** 替换成 Unicode 中 U+DC00 到 U+DCFF 之间的码位(Unicode 标准把这些码位称为“Low Surrogate Area”),这些码位是保留的,没有分配字符,供应用程序内部使用。编码时,这些码位会转换成被替换的字节值。

    >>> os.listdir('.')
    ['abc.txt', 'digits-of-π.txt']
    >>> os.listdir(b'.')
    [b'abc.txt', b'digits-of-xcfx80.txt']
    >>> pi_name_bytes = os.listdir(b'.')[1]
    >>> pi_name_str = pi_name_bytes.decode('ascii', 'surrogateescape')
    >>> pi_name_str
    'digits-of-udccfudc80.txt'
    >>> pi_name_str.encode('ascii', 'surrogateescape')
    b'digits-of-xcfx80.txt
    
    In [43]: b_bytes.decode('ascii','strict')
    ---------------------------------------------------------------------------
    UnicodeDecodeError                        Traceback (most recent call last)
    <ipython-input-43-356ed8582aca> in <module>
    ----> 1 b_bytes.decode('ascii','strict')
    
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xcf in position 10: ordinal not in range(128)
    

    使用'ascii' 编解码器和 'surrogateescape' 错误处理方式把它解码成字符串。

    各个非 ASCII 字节替换成代替码位:'xcfx80' 变成了'udccfudc80'。

    编码成 ASCII 字节序列:各个代替码位还原成被替换的字节。

    本章小结

    我们必须把文本字符串与它们在文件中的二进制序列表述区分开,而python3中这个区分是强制的。

    文本比较是个异常复杂的任务,因为 Unicode为某些字符提供了不同的表示,所以匹配文本之前一定要先规范化。尤其是有特殊字符的时候。

    还有字符串的排序, locale 和 Unicode排序算法 PyUCA 包。

    人类使用文本,计算机使用字节序列。

    ——Esther Nam 和 Travis Fischer “Character Encoding and Unicode in Python”

    纯文本是什么

    对于经常处理非英语文本的人来说,“纯文本”并不是指“ASCII”。Unicode 词汇表(http://www.unicode.org/glossary/#plain_text)是这样定义纯文本的:

    只由特定标准的码位序列组成的计算机编码文本,其中不含其他格式化或结构化信息。

    这个定义的前半句说得很好,但是我不同意后半句。HTML 就是包含格式化和结构化信息的纯文本格式,但它依然是纯文本,因为 HTML 文件中的每个字节都表示文本字符(通常使用 UTF-8 编码),没有任何字节表示文本之外的信息。.png 或 .xsl 文档则不同,其中多数字节表示打包的二进制值,例如 RGB 值和浮点数。在纯文本中,数字使用数字符号序列表示。

    RAM 中如何表示字符串

    Python 官方文档对字符串的码位在内存中如何存储避而不谈。毕竟,这是实现细节。理论上,怎么存储都没关系:不管内部表述如何,输出时每个字符串都要编码成字节序列。

    在内存中,Python 3 使用固定数量的字节存储字符串的各个码位,以便高效访问各个字符或切片。

    在 Python 3.3 之前,编译 CPython 时可以配置在内存中使用 16 位或 32 位存储各个码位。16 位是“窄构建”(narrow build),32 位是“宽构建”(wide build)。如果想知道用的是哪个,要查看 sys.maxunicode 的值:65535 表示“窄构建”,不能透明地处理 U+FFFF 以上的码位。“宽构建”没有这个限制,但是消耗的内存更多:每个字符占 4个字节,就算是中文象形文字的码位大多数也只占 2 个字节。这两种构建没有高下之分,应该根据自己的需求选择。

    从 Python 3.3 起,创建 str 对象时,解释器会检查里面的字符,然后为该字符串选择最经济的内存布局:如果字符都在 latin1 字符集中,那就使用 1 个字节存储每个码位;否则,根据字符串中的具体字符,选择 2 个或 4 个字节存储每个码位。这是简述,完整细节参阅“PEP 393—Flexible String Representation”(https://www.python.org/dev/peps/pep-0393/)。

    灵活的字符串表述类似于 Python 3 对 int 类型的处理方式:如果一个整数在一个机器字中放得下,那就存储在一个机器字中;否则解释器切换成变长表述,类似于Python 2 中的 long 类型。这种聪明的做法得到推广,真是让人欢喜!

  • 相关阅读:
    UVa 291 The House Of Santa Claus——回溯dfs
    (优先)队列简单总结
    POJ 2255 Tree Recovery——二叉树的前序遍历、后序遍历、中序遍历规则(递归)
    博弈论入门 Bash 、Nim 、Wythoff's Game结论及c++代码实现
    UVa 167(八皇后)、POJ2258 The Settlers of Catan——记两个简单回溯搜索
    欧几里得(辗转相除gcd)、扩欧(exgcd)、中国剩余定理(crt)、扩展中国剩余定理(excrt)简要介绍
    51nod 1135 原根 (数论)
    「学习笔记」扩展KMP (简)
    「解题报告」[luoguP6594]换寝室 (二分答案 树形DP)
    「解题报告」[luoguP6585]中子衰变 (交互题 分类讨论)
  • 原文地址:https://www.cnblogs.com/pythonwl/p/15147705.html
Copyright © 2020-2023  润新知