Chapter 4 Text versus Bytes
4.1 字符
- Python3 的
str对象中获取的元素时Unicode 字符,而非字节序列(Python2); - Unicode 标准中的码位和编码:
- 码位:字符的标识,用 U+xxxx(4 位到 6 位十六进制数字)表示(表示为\uxxxx);
- 字符序列:实际的表示,用两个十六进制数字表示一个字符(如\x41);
- 编码:字符的具体表述,在码位和字节序列间转换的算法,如 UTF-8 编码(A 编码为\x41)、UTF-16LE 编码(A 编码为\x41\x00)等(上述编码得到的结果为字节序列)。
- 将码位转为字符序列称为编码(
.encode(),将人类可读的str编码为bytes。注意str中存储的是 Unicode 字符,也即码位),反向称为解码(.decode(),将bytes解码为str);
bytes字面量以b开头。
s = 'café'
len(s) # 4
b = s.encode('utf8') # b'caf\xc3\xa9'
len(b) # 5
b.decode('utf8') # 'café'
4.2 字节
概述
- 内置的基本二进制序列类型:不可变的
bytes和可变的bytearray; - 元素均是 0-255 的整数(下标读取),切片仍然是同一类型的二进制序列;
- 显示方式:
- 可打印的 ASCII:字符本身;
- 空白符:使用转义序列;
- 其他字节:使用十六进制转义序列
\x00。
方法
- 可以使用大部分
str类的方法来处理bytes和bytearray(除了格式化方法和 Unicode 相关); - 特殊的类方法:
fromhex,解析十六进制数字对变成字符,如bytes.fromhex('31 4B CE A9'); - 构造方式:
- 对
str调用编码.encode()方法构造bytes; - 使用
bytes/bytearray(string, encoding=xxx)函数构造; - 接受包含 0-255 之间数值的可迭代对象;
- 一个实现了缓冲协议的对象(
bytes、bytearray、memoryview、array.array)等,直接复制其中的字节序列到新的二进制序列中。这种方式可能涉及强制类型转换;
- 对
结构体与内存视图
- 结构体模块
struct:将打包的字节序列转换为不同类型字段组成的元组,或反向打包; - 拆包
struct.unpack(format, data):结构体的格式format用来指定拆包的形式,如<3s3sHH中<表示小字节序,3s表示 3 字节序列,H表示 16 位二进制整数。
import struct
with open('filter.gif', 'rb') as fp:
img = memoryview(fp.read())
header = img[:10]
print(bytes(header)) # b'GIF89a+\x02\xe6\x00'
print(struct.unpack('<3s3sHH', header)) # (b'GIF', b'89a', 555, 230)
4.3 基本的编解码器
- 部分编码不能表示所有 Unicode 字符,UTF 编码的设计目的就是处理每一个 Unicode 码位;
- UTF-16LE 中的 LE 表示 Little Endian(小字节序)。
4.4 编解码问题
异常处理
UnicodeEncodeError:如果目标编码中没有定义该字符,就会抛出该异常。除非传入errors参数,对错误进行特殊处理。默认为strict,可以选择:ignore跳过无法编码的字符;replace把无法编码的字符替换为'?'字符;xmlcharrefreplace把无法编码的字符替换成 XML 实体(格式:&#xxx;);- 可以扩展其他的错误处理方式,使用
codecs.register_error函数;
- utf-? 可以处理任何字符串。
UnicodeDecodeError:如果字节不包含有效的 ASCII 字符,则抛出该异常。同样可设置errors;- 如果使用
replace进行错误处理,则会替换为“替换字符”(Replacement Charater)�; - 如果使用错误的编码,解码过程可能不会出现错误,而得到乱码字符(鬼符)。
- 如果使用
加载 Python 模块时的编码问题
- Python3 默认使用 UTF-8 编码源码,Python2 为 ASCII;
- 如果需要在源码中包括 UTF-8 以外的数据,可以在开头标注
# coding: xxx表示编码方式。 - 注意:要使用不同的编码保存(设置文件编码格式),这样才需要标注编码方式。否则,如果使用默认的 UTF-8 保存,会自动将其转换为 UTF-8,这样就不需要特殊编码。因此,要避免编码问题,将其保存为 UTF-8 编码格式即可。
判断字节序列的编码
- 经验:
b'\x00出现只可能为 16/32 位编码;b'\x20\x00很可能为 UTF-16LE 中的空格字符; - Python 库
chardet通过试探和分析来识别所支持的 30 种编码,并提供了命令行工具。
BOM:字节序标记
- 使用 UTF-16 编码的文本要在开头加上不可见字符
ZERO WIDTH NO-BREAK SPACE(U+FEFF),在小字节序中对应字符编码b'\xff\xfe',在大字节序中相反,可以以此判断。UTF-16 编解码器可以通过 BOM 判断字节序,并将其过滤掉,提供真正的文本内容; - 如果使用 UTF-16 的变种 LE、BE 显式指名字节序,则不会生成 BOM;
- 根据标准,如果文件使用 UTF-16 编码且没有 BOM,则应该假定大字节序。但也有例外;
- UTF-8 中也可能存在 BOM,U+FEFF 字符为为三字节序列
b'\xef\xbb\xbf'。
4.5 处理文本文件
文件编解码
- Unicode 三明治:解码输入的字节序列,只处理文本,最后编码输出;
- 打开文件时若不指定编解码方式,则会使用区域设置中的默认编码。因此不能依赖默认编码;
- 使用文本、二进制方式打开文件时,得到的文件描述符对象分别为
TextIOWrapper和BufferedReader。前者调用read()方法得到字符串,后者得到字符序列bytes。
默认编码
- 小技巧:
eval函数能够将字符串翻译为语句,而不仅仅是字面值的转换;str.rjust(width)可以用于右对齐显示;
locale.getpreferredencoding()返回的编码是打开文件的默认编码,也是重定向标准输出的默认编码。但它在不同系统中设置方式不同,返回值只是猜测的编码。
4.6 规范化 Unicode 字符串
规范化 unicodedata.normalize
- 规范化的原因:Unicode 中包含组合字符(变音符号、附加符号),它们可能构成“标准等价物”(canonical equivalent),看上去一样的字符串却不相等;
unicodedata.normalize(mode, string)可以用来规范化字符串,可用的模式:- NFC:最少的码位(看成整体),是W3C 推荐的规范化形式。有些看上去完全相同的单字符会被规范为另一个(如Ω),因此需要规范化;
- NFD:分解成基字符和单独的组合字符;
- NFKC、NFKD:(K-兼容性)严格的规范化形式,将“兼容字符”(用来兼容现有标准而添加的重复字符)替换为若干个字符,但会导致格式损失,如²、½等。因此只在搜索、索引等特殊情况中使用。
大小写折叠
s.casefold()与s.lower()一样,把所有文本变成小写。但有少量的字符是例外,如微符号、德语中的 Eszett 等。
规范化文本匹配
"""
Utility functions for normalized Unicode string comparison.
Using Normal Form C, case sensitive:
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1 == s2
False
>>> nfc_equal(s1, s2)
True
>>> nfc_equal('A', 'a')
False
Using Normal Form C with case folding:
>>> s3 = 'Straße'
>>> s4 = 'strasse'
>>> s3 == s4
False
>>> nfc_equal(s3, s4)
False
>>> fold_equal(s3, s4)
True
>>> fold_equal(s1, s2)
True
>>> fold_equal('A', 'a')
True
"""
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())
去掉变音符号
- 先使用 NFD 规范化分解,之后过滤掉所有组合符号(用
unicodedata.combining(c)可以判断组合符号):
import unicodedata
import string
def shave_marks(txt):
"""Remove all diacritic marks"""
norm_txt = unicodedata.normalize('NFD', txt) # <1>
shaved = ''.join(c for c in norm_txt
if not unicodedata.combining(c)) # <2>
return unicodedata.normalize('NFC', shaved) # <3>
- 对非拉丁字符的修改没有意义,无法将其改为 ASCII 字符。因此需要对前一基字符判断:
def shave_marks_latin(txt):
"""Remove all diacritic marks from Latin base characters"""
norm_txt = unicodedata.normalize('NFD', txt) # <1>
latin_base = False
keepers = []
for c in norm_txt:
if unicodedata.combining(c) and latin_base: # <2>
continue # ignore diacritic on Latin base char
keepers.append(c) # <3>
# if it isn't combining char, it's a new base char
if not unicodedata.combining(c): # <4>
latin_base = c in string.ascii_letters
shaved = ''.join(keepers)
return unicodedata.normalize('NFC', shaved) # <5>
- 还可以将常见特殊符号替换为对等的字符/字符串:
single_map = str.maketrans("""‚ƒ„†ˆ‹‘’“”•–—˜›""", # <1>
"""'f"*^<''""---~>""")
multi_map = str.maketrans({ # <2>
'€': '<euro>',
'…': '...',
'Œ': 'OE',
'™': '(TM)',
'œ': 'oe',
'‰': '<per mille>',
'‡': '**',
})
multi_map.update(single_map) # <3> 合并上面的两个映射表
def dewinize(txt):
"""Replace Win1252 symbols with ASCII chars or sequences"""
return txt.translate(multi_map) # <4>
def asciize(txt):
no_marks = shave_marks_latin(dewinize(txt)) # <5>
no_marks = no_marks.replace('ß', 'ss') # <6>
return unicodedata.normalize('NFKC', no_marks) # <7>
- 上述的规范化操作超出了标准的范围,很可能会改变原意,需要根据应用场景确定。
4.7 Unicode 文本排序
- 问题:在众多语言中,重音符号和下加符对排序没有影响,不应当直接按码位排序(默认);
- 非 ASCII 文本的标准排序方式:
key=locale.strxfrm将字符串转换为所在区域进行比较的形式。因此需要预先使用locale.setlocale(locale.LC_COLLATE, <<your_locale>>)设置合适的区域。不过这种方法由很多问题:区域设置是全局的,操作系统必须支持区域设置,需要区域名称,操作系统正确实现了该区域; - PyUCA:Unicode 排序算法(Unicode Collation Algorithm)的纯 Python 实现:
coll = pyuca.Collator()构造排序工具;key=coll.sort_key指定排序关键码函数;- 可以自行定制排序方式,传入构造函数。默认使用 Unicode 6.3.0 的 Default Unicode Collation Element Table。
4.8 Unicode 数据库
- Unicode 标准提供了完整的数据库,记录了字符是否可打印、是字母、是数字等性质。字符串的若干方法就是基于此实现的;
- 小技巧:
str.center(width)居中对齐;%04x表示十六进制、4 位、空缺填 0;- 可以使用
r = re.compile(expr)一次性设置匹配的规则,随后用r.match()即可匹配; unicodedata.numeric(char)能够将字符转化为其表示的数字。
4.9 支持字符串和字节序列的双模式 API
双模式 API:接受字符串或字符序列为参数,根据类型展现不同的行为。
正则表达式
- 使用字节序列构建:只能匹配 ASCII 字符(严格对应);
- 使用字符串构建:可以匹配其他 Unicode 数字或字母; ^34d285
- 字节序列只能用字节序列正则表达式搜索(类型必须对应);
- 可以使用
re.ASCII标志指定只能匹配 ASCII 字符; - 小技巧:
- 正则表达式中
\d表示数字,\w表示数字或字母; - 可以使用括号的方式拼接两行字符串:
- 正则表达式中
a = ("2321414"
"12312313")
print(a) # 232141412312313
os中的函数
os.listdir()返回路径中的文件名列表。参数是字节序列,则返回的文件名也是字节序列;- 手动处理编解码:
os.fsencode(filename)、os.fsdecode(filename)。它们的错误处理方式为surrogateescape(Linux)或strict(Windows):surrogateescape会将无法解码的字节替换成U+DC00到U+DCFF之间的码位(Low Surrogate Area),这些码位是保留的,没有分配字符。用同样的模式编码可以还原。