# 《流畅的Python》读书笔记
# 第4章 文本和字节序列
# 人类使用文本,计算机使用字节序列。
# 深入理解 Unicode 对你可能十分重要,也可能无关紧要,这取决于Python 编程的场景。
#本章将讨论下述话题:
#字符、码位和字节表述
#bytes、bytearray 和 memoryview 等二进制序列的独特特性
#全部 Unicode 和陈旧字符集的编解码器
#避免和处理编码错误
#处理文本文件的最佳实践
#默认编码的陷阱和标准 I/O 的问题
#规范化 Unicode 文本,进行安全的比较
#规范化、大小写折叠和暴力移除音调符号的实用函数
#使用 locale 模块和 PyUCA 库正确地排序 Unicode 文本
#Unicode 数据库中的字符元数据
#能处理字符串和字节序列的双模式 API
# 4.1 字符问题
# “字符串”是个相当简单的概念:一个字符串是一个字符序列。
# 如果想帮助自己记住.decode()和.encode()的区别,可以把字节序列想成晦涩难懂的机器磁芯转储,把Unicode字符串想成“人类可读”的文本。
# 那么,把字节序列变成人类可读的文本字符串就是解码,而把字符串变成用于存储或传输的字节序列就是编码。
# 示例 4-1 编码和解码
s = 'café'
print(len(s)) #4
b = s.encode('utf8')
print(b) #b'caf\xc3\xa9'
print(len(b)) #5
print(b.decode('utf8')) #café
# 4.2 字节概要
# bytes 或 bytearray 对象的各个元素是介于 0~255(含)之间的整数,而不像 Python 2 的 str 对象那样是单个的字符。
# 示例 4-2 包含 5 个字节的 bytes 和 bytearray 对象
# 示例 4-3 使用数组中的原始数据初始化bytes对象
import array
numbers=array.array('h',[-2,-1,0,1,2])
octets=bytes(numbers)
print(octets) #b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'
# 示例 4-4 使用memoryview和struct查看一个GIF图像的首部
import struct
fmt='<3s3sHH'
with open('2.gif','rb') as fp:
img=memoryview(fp.read())
header=img[:10]
print(bytes(header)) #b'\xff\xd8\xff\xe0\x00\x10JFIF'
print(struct.unpack(fmt, header)) #(b'\xff\xd8\xff', b'\xe0\x00\x10', 17994, 17993)
del header
del img
# 4.3 基本的编解码器
# Python 自带了超过 100 种编解码器(codec, encoder/decoder),用于在文本和字节之间相互转换。
# 示例 4-5 使用3个编解码器编码字符串“El Niño”,得到的字节序列差异很大
for code in ['latin_1', 'utf_8', 'utf_16']:
print(codec, 'El Niño'.encode(codec), sep='\t')
# 4.4 了解编解码问题
# 虽然有个一般性的 UnicodeError 异常,但是报告错误时几乎都会指明具体的异常:UnicodeEncodeError(把字符串转换成二进制序列时)或 UnicodeDecodeError(把二进制序列转换成字符串时)。
# 如果源码的编码与预期不符,加载 Python 模块时还可能抛出 SyntaxError。
# 4.4.1 处理UnicodeEncodeError
# 多数非 UTF 编解码器只能处理 Unicode 字符的一小部分子集。
# 把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出UnicodeEncodeError 异常,除非把 errors 参数传给编码方法或函数,对错误进行特殊处理。
# 示例 4-6 编码成字节序列:成功和错误处理
city = 'São Paulo'
print(city.encode('utf_8'))
print(city.encode('utf_16'))
print(city.encode('iso8859_1'))
# print(city.encode('cp437')) character maps to <undefined>
print(city.encode('cp437', errors='ignore'))
print(city.encode('cp437', errors='replace'))
print(city.encode('cp437', errors='xmlcharrefreplace'))
# 4.4.2 处理UnicodeDecodeError
# 不是每一个字节都包含有效的 ASCII 字符,也不是每一个字符序列都是有效的 UTF-8 或 UTF-16。
# 因此,把二进制序列转换成文本时,如果假设是这两个编码中的一个,遇到无法转换的字节序列时会抛出UnicodeDecodeError。
# 示例 4-7 把字节序列解码成字符串:成功和错误处理
octets = b'Montr\xe9al'
print(octets.decode('cp1252'))
print(octets.decode('iso8859_7'))
print(octets.decode('koi8_r'))
# print(octets.decode('utf_8')) invalid continuation byte
print(octets.decode('utf_8', errors='replace'))
# 4.4.3 使用预期之外的编码加载模块时抛出的SyntaxError
# GNU/Linux 和 OS X 系统大都使用 UTF-8,因此打开在 Windows 系统中使用 cp1252 编码的 .py 文件时可能发生这种情况。
# 注意,这个错误在Windows 版 Python 中也可能会发生,因为 Python 3 为所有平台设置的默认编码都是 UTF-8。
示例 4-8 ola.py:“你好,世界!”的葡萄牙语版
# coding: cp1252
print('Olá, Mundo!')
# 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:有用的鬼符
# 4.5 处理文本文件
# 处理文本的最佳实践是“Unicode 三明治”(如图 4-2 所示)。 意思是,要尽早把输入(例如读取文件时)的字节序列解码成字符串。
# 示例 4-9 一个平台上的编码问题
print(open('cafe.txt', 'w', encoding='utf_8').write('café'))
print(open('cafe.txt').read())
# 示例 4-10 仔细分析在 Windows 中运行的示例 4-9,找出并修正问题
# 示例 4-11 探索编码默认值
# 示例 4-12 在Windows 7(SP1)巴西版中的 cmd.exe 中输出的默认编码;PowerShell 输出的结果相同
# 4.6 为了正确比较而规范化Unicode字符串
# 因为 Unicode 有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。
# 4.6.1 大小写折叠
# 大小写折叠其实就是把所有文本变成小写,再做些其他转换。这个功能由 str.casefold() 方法(Python 3.3 新增)支持。
# 4.6.2 规范化文本匹配实用函数
# 由前文可知,NFC 和 NFD 可以放心使用,而且能合理比较 Unicode 字符串。对大多数应用来说,NFC 是最好的规范化形式。
# 不区分大小写的比较应该使用 str.casefold()。
# 示例 4-13 normeq.py:比较规范化 Unicode 字符串
# 4.6.3 极端“规范化”:去掉变音符号
# 示例 4-14 去掉全部组合记号的函数(在 sanitize.py 模块中)
# 示例 4-15 示例 4-14 中 shave_marks 函数的两个使用示例
# 示例 4-16 删除拉丁字母中组合记号的函数
# 示例 4-17 把一些西文印刷字符转换成 ASCII 字符
# 示例 4-18 示例 4-17 中 asciize 函数的使用示例
# 4.7 Unicode文本排序
# Python 比较任何类型的序列时,会一一比较序列里的各个元素。对字符串来说,比较的是码位。可是在比较非 ASCII 字符时,得到的结果不尽如人意。
# 下面对一个生长在巴西的水果的列表进行排序:
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
print(sorted(fruits))
# 示例 4-19 使用locale.strxfrm函数做排序键
import locale
locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=locale.strxfrm)
print(sorted_fruits)
# 使用Unicode排序算法排序
# 示例 4-20 使用 pyuca.Collator.sort_key 方法
import pyuca
coll = pyuca.Collator()
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=coll.sort_key)
print(sorted_fruits)
# 4.8 Unicode数据库
# unicodedata 模块中有几个函数用于获取字符的元数据。
# 例如,字符在标准中的官方名称是不是组合字符(如结合波形符构成的变音符号等),以及符号对应的人类可读数值(不是码位)。
# 示例 4-21 展示了unicodedata.name() 和 unicodedata.numeric() 函数,以及字符串的 .isdecimal() 和 .isnumeric() 方法的用法。
# 示例 4-21 Unicode 数据库中数值字符的元数据示例
# 4.9 支持字符串和字节序列的双模式API
# 标准库中的一些函数能接受字符串或字节序列为参数,然后根据类型展现不同的行为。re 和 os 模块中就有这样的函数。
# 4.9.1 正则表达式中的字符串和字节序列
# 如果使用字节序列构建正则表达式,\d 和 \w 等模式只能匹配 ASCII 字符;相比之下,如果是字符串模式,就能匹配 ASCII 之外的 Unicode 数字或字母。
# 示例 4-22 ramanujan.py:比较简单的字符串正则表达式和字节序列正则表达式的行为
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 \u0be7\u0bed\u0be8\u0bef" " as 1729 = 1³ + 12³ = 9³ + 10³.")
text_bytes = text_str.encode('utf_8')
print('Text', repr(text_str), sep='\n ')
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))
# 4.9.2 os函数中的字符串和字节序列
# 示例 4-23 把字符串和字节序列参数传给listdir函数得到的结果
os.listdir('.')
os.listdir(b'.')
# 示例 4-24 使用 surrogateescape 错误处理方式
os.listdir('.')
os.listdir(b'.')
pi_name_bytes = os.listdir(b'.')[1]
pi_name_str = pi_name_bytes.decode('ascii', 'surrogateescape')
pi_name_str
pi_name_str.encode('ascii', 'surrogateescape')
# 4.10 本章小结
# 本章首先澄清了人们对一个字符等于一个字节的误解。随着 Unicode 的广泛使用(80% 的网站已经使用 UTF-8),我们必须把文本字符串与它们在文件中的二进制序列表述区分开,而 Python 3 中这个区分是强制的。
# 4.11 延伸阅读