《Fluent Python》 CH.04_数据结构-文本和字节序列 (码位字符、Unicode、文本编解码异常等)

本章主要内容

  • 字符、码位和字节表述
  • bytes、bytearray 和 memoryview 等二进制序列的独特特性
  • 全部 Unicode 和陈旧字符集的编解码器
  • 避免和处理编码错误
  • 处理文本文件的最佳实践
  • 默认编码的陷阱和标准 I/O 的问题
  • 规范化 Unicode 文本,进行安全的比较
  • 规范化、大小写折叠和暴力移除音调符号的实用函数
  • 使用 locale 模块和 PyUCA 库正确地排序 Unicode 文本
  • Unicode 数据库中的字符元数据
  • 能处理字符串和字节序列的双模式 API

其他

  • jupyter nbconvert --to markdown E:PycharmProjectsTianChiProject 0_山枫叶纷飞competitions 13_fluent_pythonCH.04_数据结构-文本和字节序列.ipynb

4.1 字符问题

"字符"的最佳定义就是Unicode字符,因此从Python3的str对象中获取的元素是Unicode字符。
区别

  • python2 unicode 类似于 python3 str
  • python2 str 类似于 python3 bytes

Unicode 标准把字符的标识和具体的字节表述进行了如下的明确区分:

  • 字符的标示,即码位,在 Unicode 标准中以 4~6 个十六进制数字表示,而且加前缀“U+”。
  • 字符的具体表述取决于所用的编码。编码是在码位和字节序列之间 转换时使用的算法。如utf-8算法、utf-16算法等。
  • 百度百科=》在Unicode中:汉字“字”对应的数字是23383(十进制),十六进制表示为5B57。在Unicode中,我们有很多方式将数字23383表示成程序中的数据,包括:UTF-8、UTF-16、UTF-32。UTF是“Unicode Transformation Format”的缩写,可以翻译成Unicode字符集转换格式,即怎样将Unicode定义的数字转换成程序数据。

把码位(可读文本)转换成字节序列(机器码)的过程是编码;把字节序列转换成码位的过程是 解码。

示例,编码和解码:

s = 'café' # python3中默认使用的是Unicode编码,'café' 字符串有 4 个 Unicode 字符。
len(s)
b = s.encode('utf8')
b
b'cafxc3xa9'
len(b)
5
b.decode('utf8')
'café'

4.2 字节概要

bytes 或 bytearray 对象的各个元素是介于 0~255(含)之间的整 数,而不像 Python 2 的 str 对象那样是单个的字符。
字节对象有以下特点:

  • bytes 对象可以从 str 对象使用给定的编码构建。
  • 各个元素是 range(256) 内的整数
  • bytes 对象的切片还是 bytes 对象,即使是只有一个字节的切片
  • bytearray 对象没有字面量句法,而是以 字节序列 字面量参数的形式显示
  • bytearray 对象的切片还是 bytearray 对象
cafe = bytes('café', encoding='utf8')
cafe
b'cafxc3xa9'
print(type(cafe[:2]))
cafe[:2]

<class 'bytes'>





b'ca'
cafe_arr = bytearray(cafe)
cafe_arr

bytearray(b'cafxc3xa9')
cafe_arr[-1:]

bytearray(b'xa9')

结构体和内存视图

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

4.3 基本的编解码器

示例 4-5 使用 3 个编解码器编码字符串“El Niño”,得到的字节序 列差异很大

for codec in ['latin_1', 'utf_8', 'utf_16']:
    print(codec, 'El Niño'.encode(codec), sep='	')
latin_1	b'El Nixf1o'
utf_8	b'El Nixc3xb1o'
utf_16	b'xffxfeEx00lx00 x00Nx00ix00xf1x00ox00'

4.4 了解编解码问题

  • UnicodeEncodeError(把字符串转换成二进制序列时)
  • 或 UnicodeDecodeError(把二进制序列转换成字符串时)

4.4.1 处理UnicodeEncodeError

方法

  • 使用encode的errors处理措施,可选忽略'ignore',替换replace(把无法编码的字符替换成 '?'), 或者使用xmlcharrefreplace('xmlcharrefreplace' 把无法编码的字符替换成 XML实体。)

示例 4-6 编码成字节序列, 成功和错误处理:

city = 'São Paulo'
city.encode('utf-8')
b'Sxc3xa3o Paulo'
city.encode('cp437')

---------------------------------------------------------------------------

UnicodeEncodeError                        Traceback (most recent call last)

<ipython-input-14-31f4f9909b38> in <module>
----> 1 city.encode('cp437')
      2 
      3 


D:Anaconda3libencodingscp437.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>
city.encode('cp437', errors='replace')

b'S?o Paulo'
city.encode('cp437', errors='xmlcharrefreplace')

b'S&#227;o Paulo'

4.4.2 处理UnicodeDecodeError

不是每一个字节都包含有效的 ASCII 字符,也不是每一个字符序列都是 有效的 UTF-8 或 UTF-16。因此,把二进制序列转换成文本时,如果假 设是这两个编码中的一个,遇到无法转换的字节序列时会抛出 UnicodeDecodeError。

另一方面,很多陈旧的 8 位编码——如 'cp1252'、'iso8859_1' 和 'koi8_r'——能解码任何字节序列流而不抛出错误,例如随机噪声。 因此,如果程序使用错误的 8 位编码,解码过程悄无声息,而得到的

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

Python 3 默认使用 UTF-8 编码源码,Python 2(从 2.5 开始)则默认使用 ASCII。

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

因为 Python 3 为所有平台设置的默 认编码都是 UTF-8。

4.4.4 如何找出字节序列的编码

统一字符编码侦测包

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

4.4.5 BOM:有用的鬼符

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

UTF-16 有两个变种:UTF-16LE,显式指明使用小字节序(Intel x86 架构);

UTF-16BE, 显式指明使用大字节序(ARM架构)。如果使用这两个变种,不会生成 BOM。

4.5 处理文本文件(最佳实践:Unicode 三明治)

Unicode 三明治

  • bytes->str,解码输入的字节序列
  • 100% str,只处理文本
  • str -> bytes, 编码输出的文本
open('/competitions/013_fluent_python/file/cafe.txt', 'w+', encoding='utf_8').write('café')

4
open('E:\PycharmProjects\TianChiProject\00_山枫叶纷飞competitions\013_fluent_python\file\cafe.txt').read()

'caf茅'
open('E:\PycharmProjects\TianChiProject\00_山枫叶纷飞competitions\013_fluent_python\file\cafe.txt',  encoding='utf_8').read()

'café'

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

编码默认值:一团糟

在 GNU/Linux 和 OS X 中,这些编码的默认值都是 UTF-8,而 且多年来都是如此,因此 I/O 能处理所有 Unicode 字符。

在 Windows 中,不仅同一个系统中使用不同的编码,还有只支持 ASCII 和 127 个额外的字符的代码页(如 'cp850' 或 'cp1252'),而且不同的代码页之间增加的字符也有所不同。因 此,若不多加小心,Windows 用户更容易遇到编码问题。

综上,locale.getpreferredencoding() 返回的编码是最重要的:这 是打开文件的默认编码,也是重定向到文件的 sys.stdout/stdin/stderr 的默认编码。

目前win10默认编码为cp936':

import locale
locale.getpreferredencoding(do_setlocale=True)

'cp936'

关于编码默认值的最佳建议是:别依赖默认值。

4.7 Unicode文本排序

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

解决办法:

  • 在 Python 中,非 ASCII 文本的标准排序方式是使用 locale.strxfrm 函数,根据 locale 模块的文档 (https://docs.python.org/3/library/locale.html? highlight=strxfrm#locale.strxfrm),这 个函数会“把字符串转换成适合所 在区域进行比较的形式”。 使用 locale.strxfrm 函数之前,必须先为应用设定合适的区域设置, 还要祈祷操作系统支持这项设置。

4.8 Unicode数据库

Unicode 标准提供了一个完整的数据库(许多格式化的文本文件),不仅包括码位与字符名称之间的映射,还有各个字符的元数据,以及字符 之间的关系。

例如,Unicode 数据库记录了字符是否可以打印、是不是 字母、是不是数字,或者是不是其他数值符号。字符串的 isidentifier、isprintable、isdecimal 和 isnumeric 等方法就 是靠这些信息作判断的。

str.casefold 方法也用到了 Unicode 表中的 信息。

示例 4-21 Unicode 数据库中数值字符的元数据示例(各个标号说 明输出中的各列):

  • ('U+%04x' % ord(char),U+0000 格式的码位
  • char.center(6),在长度为 6 的字符串中居中显示字符
  • char.isdigit() : 如果字符匹配正则表达式 r'd',显示 re_dig
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

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

  • 标准库中的一些函数能接受字符串或字节序列为参数,然后根据类型展 现不同的行为。

re 和 os 模块中就有这样的函数。

4.9.1 正则表达式(re)中的字符串和字节序列

  • 字节序列只能用字节序列正则表达式搜索。
  • 字符串模式 r'd+' 能匹配泰米尔数字和 ASCII 数字。
  • 字节序列模式 rb'd+' 只能匹配 ASCII 字节中的数字。
  • 字符串模式 r'w+' 能匹配字母、上标、泰米尔数字和 ASCII 数字。
  • 字节序列模式 rb'w+' 只能匹配 ASCII 字节中的字母和数字。

示例:

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"
" as 1729 = 1³ + 12³ = 9³ + 10³.")
text_bytes = text_str.encode('utf_8')
print('Text', repr(text_str), 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³.'
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']

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

GNU/Linux 内核(UTF-8编码格式)不理解 Unicode(包括字符集、编码方案),因此你可能发现了,对任何合理的编 码方案来说,在文件名中使用字节序列都是无效的,无法解码成字符串。

为了规避这个问题,os 模块中的所有函数、文件名或路径名参数既能 使用字符串,也能使用字节序列。

如果这样的函数使用字符串参数调用,该参数会使用 sys.getfilesystemencoding() 得到的编解码器 自动编码,然后操作系统会使用相同的编解码器解码。这几乎就是我们 想要的行为,与 Unicode 三明治最佳实践一致。

示例 4-23 把字符串和字节序列参数传给 listdir 函数得到的结 果

import os
os.listdir('.')

['.cache',
 'correlation.png',
 'FeatureBagging.png',
 'HBOS.png',
 'KNN.png',
 'LOF.png',
 'PCA.png']
os.listdir(b'.')

[b'.cache',
 b'correlation.png',
 b'FeatureBagging.png',
 b'HBOS.png',
 b'KNN.png',
 b'LOF.png',
 b'PCA.png']

使用 surrogateescape 处理鬼符

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

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