流畅的 Python 第二版(GPT 重译)(二)(3)https://developer.aliyun.com/article/1484411
另外两种规范化形式是 NFKC 和 NFKD,其中字母 K 代表“兼容性”。这些是更强的规范化形式,影响所谓的“兼容性字符”。尽管 Unicode 的一个目标是为每个字符有一个单一的“规范”代码点,但一些字符出现多次是为了与现有标准兼容。例如,MICRO SIGN
,µ
(U+00B5
),被添加到 Unicode 以支持与包括它在内的latin1
的往返转换,即使相同的字符是希腊字母表的一部分,具有代码点U+03BC
(GREEK SMALL LETTER MU
)。因此,微符号被视为“兼容性字符”。
在 NFKC 和 NFKD 形式中,每个兼容字符都被一个或多个字符的“兼容分解”替换,这些字符被认为是“首选”表示,即使存在一些格式损失——理想情况下,格式应该由外部标记负责,而不是 Unicode 的一部分。举例来说,一个半分数'½'
(U+00BD
)的兼容分解是三个字符的序列'1/2'
,而微符号'µ'
(U+00B5
)的兼容分解是小写的希腊字母 mu'μ'
(U+03BC
)。⁷
下面是 NFKC 在实践中的工作方式:
>>> from unicodedata import normalize, name >>> half = '\N{VULGAR FRACTION ONE HALF}' >>> print(half) ½ >>> normalize('NFKC', half) '1⁄2' >>> for char in normalize('NFKC', half): ... print(char, name(char), sep='\t') ... 1 DIGIT ONE ⁄ FRACTION SLASH 2 DIGIT TWO >>> 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'
是'½'
的一个合理替代品,而微符号实际上是一个小写希腊字母 mu,但将'4²'
转换为'42'
会改变含义。一个应用程序可以将'4²'
存储为'42'
,但normalize
函数对格式一无所知。因此,NFKC 或 NFKD 可能会丢失或扭曲信息,但它们可以生成方便的中间表示形式用于搜索和索引。
不幸的是,对于 Unicode 来说,一切总是比起初看起来更加复杂。对于VULGAR FRACTION ONE HALF
,NFKC 规范化产生了用FRACTION SLASH
连接的 1 和 2,而不是SOLIDUS
,即“斜杠”—ASCII 代码十进制 47 的熟悉字符。因此,搜索三字符 ASCII 序列'1/2'
将找不到规范化的 Unicode 序列。
警告
NFKC 和 NFKD 规范会导致数据丢失,应仅在特殊情况下如搜索和索引中应用,而不是用于文本的永久存储。
当准备文本进行搜索或索引时,另一个有用的操作是大小写折叠,我们的下一个主题。
大小写折叠
大小写折叠基本上是将所有文本转换为小写,还有一些额外的转换。它由str.casefold()
方法支持。
对于只包含 latin1
字符的任何字符串 s
,s.casefold()
产生与 s.lower()
相同的结果,只有两个例外——微符号 'µ'
被更改为希腊小写 mu(在大多数字体中看起来相同),德语 Eszett 或 “sharp s”(ß)变为 “ss”:
>>> micro = 'µ' >>> name(micro) 'MICRO SIGN' >>> micro_cf = micro.casefold() >>> name(micro_cf) 'GREEK SMALL LETTER MU' >>> micro, micro_cf ('µ', 'μ') >>> eszett = 'ß' >>> name(eszett) 'LATIN SMALL LETTER SHARP S' >>> eszett_cf = eszett.casefold() >>> eszett, eszett_cf ('ß', 'ss')
有将近 300 个代码点,str.casefold()
和 str.lower()
返回不同的结果。
和 Unicode 相关的任何事物一样,大小写折叠是一个困难的问题,有很多语言特殊情况,但 Python 核心团队努力提供了一个解决方案,希望能适用于大多数用户。
在接下来的几节中,我们将利用我们的规范化知识开发实用函数。
用于规范化文本匹配的实用函数
正如我们所见,NFC 和 NFD 是安全的,并允许在 Unicode 字符串之间进行明智的比较。对于大多数应用程序,NFC 是最佳的规范化形式。str.casefold()
是进行不区分大小写比较的方法。
如果您使用多种语言的文本,像 示例 4-13 中的 nfc_equal
和 fold_equal
这样的一对函数对您的工具箱是有用的补充。
示例 4-13. normeq.py: 规范化的 Unicode 字符串比较
""" 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())
超出 Unicode 标准中的规范化和大小写折叠之外,有时候进行更深层次的转换是有意义的,比如将 'café'
改为 'cafe'
。我们将在下一节看到何时以及如何进行。
极端的“规范化”:去除变音符号
谷歌搜索的秘密酱包含许多技巧,但其中一个显然是忽略变音符号(例如,重音符号、锐音符等),至少在某些情况下是这样。去除变音符号并不是一种适当的规范化形式,因为它经常改变单词的含义,并且在搜索时可能产生误报。但它有助于应对生活中的一些事实:人们有时懒惰或无知于正确使用变音符号,拼写规则随时间变化,这意味着重音符号在活语言中来来去去。
除了搜索之外,去除变音符号还可以使 URL 更易读,至少在基于拉丁语言的语言中是这样。看看关于圣保罗市的维基百科文章的 URL:
https://en.wikipedia.org/wiki/S%C3%A3o_Paulo
%C3%A3
部分是 URL 转义的,UTF-8 渲染的单个字母 “ã”(带有波浪符的 “a”)。即使拼写不正确,以下内容也更容易识别:
https://en.wikipedia.org/wiki/Sao_Paulo
要从 str
中移除所有变音符号,可以使用类似 示例 4-14 的函数。
示例 4-14. simplify.py: 用于移除所有组合标记的函数
import unicodedata import string def shave_marks(txt): """Remove all diacritic marks""" norm_txt = unicodedata.normalize('NFD', txt) # ① shaved = ''.join(c for c in norm_txt if not unicodedata.combining(c)) # ② return unicodedata.normalize('NFC', shaved) # ③
①
将所有字符分解为基本字符和组合标记。
②
过滤掉所有组合标记。
③
重新组合所有字符。
示例 4-15 展示了几种使用 shave_marks
的方法。
示例 4-15. 使用 shave_marks
的两个示例,来自 示例 4-14
>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”' >>> shave_marks(order) '“Herr Voß: • ½ cup of Œtker™ caffe latte • bowl of acai.”' # ① >>> Greek = 'Ζέφυρος, Zéfiro' >>> shave_marks(Greek) 'Ζεφυρος, Zefiro' # ②
①
仅字母 “è”、“ç” 和 “í” 被替换。
②
“έ” 和 “é” 都被替换了。
来自 示例 4-14 的函数 shave_marks
运行良好,但也许它做得太过了。通常移除变音符号的原因是将拉丁文本更改为纯 ASCII,但 shave_marks
也会改变非拉丁字符,比如希腊字母,这些字母仅仅通过失去重音就不会变成 ASCII。因此,有必要分析每个基本字符,并仅在基本字符是拉丁字母时才移除附加标记。这就是 示例 4-16 的作用。
示例 4-16. 从拉丁字符中移除组合标记的函数(省略了导入语句,因为这是来自 示例 4-14 的 simplify.py 模块的一部分)
def shave_marks_latin(txt): """Remove all diacritic marks from Latin base characters""" norm_txt = unicodedata.normalize('NFD', txt) # ① latin_base = False preserve = [] for c in norm_txt: if unicodedata.combining(c) and latin_base: # ② continue # ignore diacritic on Latin base char preserve.append(c) # ③ # if it isn't a combining char, it's a new base char if not unicodedata.combining(c): # ④ latin_base = c in string.ascii_letters shaved = ''.join(preserve) return unicodedata.normalize('NFC', shaved) # ⑤
①
将所有字符分解为基本字符和组合标记。
②
当基本字符为拉丁字符时,跳过组合标记。
③
否则,保留当前字符。
④
检测新的基本字符,并确定它是否为拉丁字符。
⑤
重新组合所有字符。
更激进的一步是将西方文本中的常见符号(例如,卷曲引号、破折号、项目符号等)替换为ASCII
等效符号。这就是示例 4-17 中的asciize
函数所做的。
示例 4-17. 将一些西方排版符号转换为 ASCII(此片段也是示例 4-14 中simplify.py
的一部分)
single_map = str.maketrans("""‚ƒ„ˆ‹‘’“”•–—˜›""", # ① """'f"^<''""---~>""") multi_map = str.maketrans({ # ② '€': 'EUR', '…': '...', 'Æ': 'AE', 'æ': 'ae', 'Œ': 'OE', 'œ': 'oe', '™': '(TM)', '‰': '<per mille>', '†': '**', '‡': '***', }) multi_map.update(single_map) # ③ def dewinize(txt): """Replace Win1252 symbols with ASCII chars or sequences""" 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) # ⑦
①
为字符替换构建映射表。
②
为字符到字符串替换构建映射表。
③
合并映射表。
④
dewinize
不影响ASCII
或latin1
文本,只影响cp1252
中的 Microsoft 附加内容。
⑤
应用dewinize
并移除变音符号。
⑥
用“ss”替换 Eszett(我们这里不使用大小写折叠,因为我们想保留大小写)。
⑦
对具有其兼容性代码点的字符进行 NFKC 规范化以组合字符。
示例 4-18 展示了asciize
的使用。
示例 4-18. 使用示例 4-17 中的asciize
的两个示例
>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”' >>> dewinize(order) '"Herr Voß: - ½ cup of OEtker(TM) caffè latte - bowl of açaí."' # ① >>> asciize(order) '"Herr Voss: - 1⁄2 cup of OEtker(TM) caffe latte - bowl of acai."' # ②
①
dewinize
替换卷曲引号、项目符号和™(商标符号)。
②
asciize
应用dewinize
,删除变音符号,并替换'ß'
。
警告
不同语言有自己的去除变音符号的规则。例如,德语将'ü'
改为'ue'
。我们的asciize
函数不够精细,因此可能不适合您的语言。但对葡萄牙语来说,它的效果还可以接受。
总结一下,在simplify.py
中的函数远远超出了标准规范化,并对文本进行了深度处理,有可能改变其含义。只有您可以决定是否走得这么远,了解目标语言、您的用户以及转换后的文本将如何使用。
这就结束了我们对规范化 Unicode 文本的讨论。
现在让我们来解决 Unicode 排序问题。
对 Unicode 文本进行排序
Python 通过逐个比较每个序列中的项目来对任何类型的序列进行排序。对于字符串,这意味着比较代码点。不幸的是,这对于使用非 ASCII 字符的人来说产生了无法接受的结果。
考虑对在巴西种植的水果列表进行排序:
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] >>> sorted(fruits) ['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
不同区域设置的排序规则不同,但在葡萄牙语和许多使用拉丁字母表的语言中,重音符号和塞迪利亚很少在排序时产生差异。⁸ 因此,“cajá”被排序为“caja”,并且必须位于“caju”之前。
排序后的fruits
列表应为:
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
在 Python 中对非 ASCII 文本进行排序的标准方法是使用locale.strxfrm
函数,根据locale
模块文档,“将一个字符串转换为可用于区域设置感知比较的字符串”。
要启用locale.strxfrm
,您必须首先为您的应用程序设置一个合适的区域设置,并祈祷操作系统支持它。示例 4-19 中的命令序列可能适用于您。
示例 4-19. locale_sort.py:使用locale.strxfrm
函数作为排序键
import locale my_locale = locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8') print(my_locale) fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] sorted_fruits = sorted(fruits, key=locale.strxfrm) print(sorted_fruits)
在 GNU/Linux(Ubuntu 19.10)上运行 示例 4-19,安装了 pt_BR.UTF-8
区域设置,我得到了正确的结果:
'pt_BR.UTF-8' ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
因此,在排序时需要在使用 locale.strxfrm
作为键之前调用 setlocale(LC_COLLATE, «your_locale»)
。
不过,还有一些注意事项:
- 因为区域设置是全局的,不建议在库中调用
setlocale
。您的应用程序或框架应该在进程启动时设置区域设置,并且不应该在之后更改它。 - 操作系统必须安装区域设置,否则
setlocale
会引发locale.Error: unsupported locale setting
异常。 - 您必须知道如何拼写区域设置名称。
- 区域设置必须由操作系统的制造商正确实现。我在 Ubuntu 19.10 上成功了,但在 macOS 10.14 上没有成功。在 macOS 上,调用
setlocale(LC_COLLATE, 'pt_BR.UTF-8')
返回字符串'pt_BR.UTF-8'
而没有任何投诉。但sorted(fruits, key=locale.strxfrm)
产生了与sorted(fruits)
相同的不正确结果。我还在 macOS 上尝试了fr_FR
、es_ES
和de_DE
区域设置,但locale.strxfrm
从未起作用。⁹
因此,标准库提供的国际化排序解决方案有效,但似乎只在 GNU/Linux 上得到很好的支持(也许在 Windows 上也是如此,如果您是专家的话)。即使在那里,它也依赖于区域设置,会带来部署上的麻烦。
幸运的是,有一个更简单的解决方案:pyuca 库,可以在 PyPI 上找到。
使用 Unicode Collation Algorithm 进行排序
James Tauber,多产的 Django 贡献者,一定感受到了痛苦,并创建了 pyuca,这是 Unicode Collation Algorithm(UCA)的纯 Python 实现。示例 4-20 展示了它的易用性。
示例 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) >>> sorted_fruits ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
这个方法简单易行,在 GNU/Linux、macOS 和 Windows 上都可以运行,至少在我的小样本中是这样的。
pyuca
不考虑区域设置。如果需要自定义排序,可以向 Collator()
构造函数提供自定义排序表的路径。默认情况下,它使用 allkeys.txt,这是项目捆绑的。这只是 来自 Unicode.org 的默认 Unicode Collation Element Table 的副本。
PyICU:Miro 的 Unicode 排序推荐
(技术审阅员 Miroslav Šedivý 是一位多语言使用者,也是 Unicode 方面的专家。这是他对 pyuca 的评价。)
pyuca 有一个排序算法,不考虑各个语言中的排序顺序。例如,在德语中 Ä 在 A 和 B 之间,而在瑞典语中它在 Z 之后。看看 PyICU,它像区域设置一样工作,而不会更改进程的区域设置。如果您想要在土耳其语中更改 iİ/ıI 的大小写,也需要它。PyICU 包含一个必须编译的扩展,因此在某些系统中安装可能比只是 Python 的 pyuca 更困难。
顺便说一句,那个排序表是组成 Unicode 数据库的许多数据文件之一,我们下一个主题。
Unicode 数据库
Unicode 标准提供了一个完整的数据库,以几个结构化的文本文件的形式存在,其中不仅包括将代码点映射到字符名称的表,还包括有关各个字符及其相关性的元数据。例如,Unicode 数据库记录了字符是否可打印、是否为字母、是否为十进制数字,或者是否为其他数字符号。这就是 str
方法 isalpha
、isprintable
、isdecimal
和 isnumeric
的工作原理。str.casefold
也使用了来自 Unicode 表的信息。
注意
unicodedata.category(char)
函数从 Unicode 数据库返回 char
的两个字母类别。更高级别的 str
方法更容易使用。例如,label.isalpha()
如果 label
中的每个字符属于以下类别之一,则返回 True
:Lm
、Lt
、Lu
、Ll
或 Lo
。要了解这些代码的含义,请参阅英文维基百科的 “Unicode 字符属性”文章 中的 “通用类别”。
按名称查找字符
unicodedata
模块包括检索字符元数据的函数,包括 unicodedata.name()
,它返回标准中字符的官方名称。图 4-5 展示了该函数的使用。¹⁰
图 4-5. 在 Python 控制台中探索 unicodedata.name()
。
您可以使用 name()
函数构建应用程序,让用户可以按名称搜索字符。图 4-6 展示了 cf.py 命令行脚本,它接受一个或多个单词作为参数,并列出具有这些单词在官方 Unicode 名称中的字符。cf.py 的完整源代码在 示例 4-21 中。
图 4-6. 使用 cf.py 查找微笑的猫。
警告
表情符号在各种操作系统和应用程序中的支持差异很大。近年来,macOS 终端提供了最好的表情符号支持,其次是现代 GNU/Linux 图形终端。Windows cmd.exe 和 PowerShell 现在支持 Unicode 输出,但截至我在 2020 年 1 月撰写本节时,它们仍然不显示表情符号——至少不是“开箱即用”。技术评论员莱昂纳多·罗查尔告诉我有一个新的、由微软推出的开源 Windows 终端,它可能比旧的微软控制台具有更好的 Unicode 支持。我还没有时间尝试。
在 示例 4-21 中,请注意 find
函数中的 if
语句,使用 .issubset()
方法快速测试 query
集合中的所有单词是否出现在从字符名称构建的单词列表中。由于 Python 丰富的集合 API,我们不需要嵌套的 for
循环和另一个 if
来实现此检查。
示例 4-21. cf.py:字符查找实用程序
#!/usr/bin/env python3 import sys import unicodedata START, END = ord(' '), sys.maxunicode + 1 # ① def find(*query_words, start=START, end=END): # ② query = {w.upper() for w in query_words} # ③ for code in range(start, end): char = chr(code) # ④ name = unicodedata.name(char, None) # ⑤ if name and query.issubset(name.split()): # ⑥ print(f'U+{code:04X}\t{char}\t{name}') # ⑦ def main(words): if words: find(*words) else: print('Please provide words to find.') if __name__ == '__main__': main(sys.argv[1:])
①
设置搜索的代码点范围的默认值。
②
find
接受 query_words
和可选的关键字参数来限制搜索范围,以便进行测试。
③
将 query_words
转换为大写字符串集合。
④
获取 code
的 Unicode 字符。
⑤
获取字符的名称,如果代码点未分配,则返回 None
。
⑥
如果有名称,将其拆分为单词列表,然后检查 query
集合是否是该列表的子集。
⑦
打印出以 U+9999
格式的代码点、字符和其名称的行。
unicodedata
模块还有其他有趣的函数。接下来,我们将看到一些与获取具有数字含义的字符信息相关的函数。
字符的数字含义
unicodedata
模块包括函数,用于检查 Unicode 字符是否表示数字,如果是,则返回其人类的数值,而不是其代码点数。示例 4-22 展示了 unicodedata.name()
和 unicodedata.numeric()
的使用,以及 str
的 .isdecimal()
和 .isnumeric()
方法。
示例 4-22. Unicode 数据库数字字符元数据演示(标注描述输出中的每列)
import unicodedata import re re_digit = re.compile(r'\d') sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285' for char in sample: print(f'U+{ord(char):04x}', # ① char.center(6), # ② 're_dig' if re_digit.match(char) else '-', # ③ 'isdig' if char.isdigit() else '-', # ④ 'isnum' if char.isnumeric() else '-', # ⑤ f'{unicodedata.numeric(char):5.2f}', # ⑥ unicodedata.name(char), # ⑦ sep='\t')
①
以U+0000
格式的代码点。
②
字符在长度为 6 的str
中居中。
③
如果字符匹配r'\d'
正则表达式,则显示re_dig
。
④
如果char.isdigit()
为True
,则显示isdig
。
⑤
如果char.isnumeric()
为True
,则显示isnum
。
⑥
数值格式化为宽度为 5 和 2 位小数。
⑦
Unicode 字符名称。
运行示例 4-22 会给你图 4-7,如果你的终端字体有所有这些字形。
图 4-7. macOS 终端显示数字字符及其元数据;re_dig
表示字符匹配正则表达式r'\d'
。
图 4-7 的第六列是在字符上调用unicodedata.numeric(char)
的结果。它显示 Unicode 知道代表数字的符号的数值。因此,如果你想创建支持泰米尔数字或罗马数字的电子表格应用程序,就去做吧!
图 4-7 显示正则表达式r'\d'
匹配数字“1”和梵文数字 3,但不匹配一些其他被isdigit
函数视为数字的字符。re
模块对 Unicode 的了解不如它本应该的那样深入。PyPI 上提供的新regex
模块旨在最终取代re
,并提供更好的 Unicode 支持。¹¹我们将在下一节回到re
模块。
在本章中,我们使用了几个unicodedata
函数,但还有许多我们没有涉及的函数。查看标准库文档中的unicodedata
模块。
接下来我们将快速查看双模式 API,提供接受str
或bytes
参数的函数,并根据类型进行特殊处理。
双模式 str 和 bytes API
Python 标准库有接受str
或bytes
参数并根据类型表现不同的函数。一些示例可以在re
和os
模块中找到。
正则表达式中的 str 与 bytes
如果用bytes
构建正则表达式,模式如\d
和\w
只匹配 ASCII 字符;相反,如果这些模式给定为str
,它们将匹配 ASCII 之外的 Unicode 数字或字母。示例 4-23 和图 4-8 比较了str
和bytes
模式如何匹配字母、ASCII 数字、上标和泰米尔数字。
示例 4-23. ramanujan.py:比较简单str
和bytes
正则表达式的行为
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(f'Text\n {text_str!r}') 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)) # ⑨
①
前两个正则表达式是str
类型。
②
最后两个是bytes
类型。
③
Unicode 文本搜索,包含泰米尔数字1729
(逻辑行一直延续到右括号标记)。
④
此字符串在编译时与前一个字符串连接(参见“2.4.2. 字符串文字连接”中的Python 语言参考)。
⑤
需要使用bytes
正则表达式来搜索bytes
字符串。
⑥](#co_unicode_text_versus_bytes_CO15-6)
str
模式r'\d+'
匹配泰米尔和 ASCII 数字。
⑦
bytes
模式rb'\d+'
仅匹配数字的 ASCII 字节。
⑧
str
模式r'\w+'
匹配字母、上标、泰米尔语和 ASCII 数字。
⑨
bytes
模式rb'\w+'
仅匹配字母和数字的 ASCII 字节。
图 4-8。从示例 4-23 运行 ramanujan.py 的屏幕截图。
示例 4-23 是一个简单的例子,用来说明一个观点:你可以在str
和bytes
上使用正则表达式,但在第二种情况下,ASCII 范围之外的字节被视为非数字和非单词字符。
对于str
正则表达式,有一个re.ASCII
标志,使得\w
、\W
、\b
、\B
、\d
、\D
、\s
和\S
只执行 ASCII 匹配。详细信息请参阅re 模块的文档。
另一个重要的双模块是os
。
os
函数中的 str 与 bytes
GNU/Linux 内核不支持 Unicode,因此在现实世界中,您可能会发现由字节序列组成的文件名,这些文件名在任何明智的编码方案中都无效,并且无法解码为str
。使用各种操作系统的客户端的文件服务器特别容易出现这个问题。
为了解决这个问题,所有接受文件名或路径名的os
模块函数都以str
或bytes
形式接受参数。如果调用这样的函数时使用str
参数,参数将自动使用sys.getfilesystemencoding()
命名的编解码器进行转换,并且 OS 响应将使用相同的编解码器进行解码。这几乎总是您想要的,符合 Unicode 三明治最佳实践。
但是,如果您必须处理(或者可能修复)无法以这种方式处理的文件名,您可以将bytes
参数传递给os
函数以获得bytes
返回值。这个功能让您可以处理任何文件或路径名,无论您可能遇到多少小精灵。请参阅示例 4-24。
示例 4-24。listdir
使用str
和bytes
参数和结果
>>> os.listdir('.') # ① ['abc.txt', 'digits-of-π.txt'] >>> os.listdir(b'.') # ② [b'abc.txt', b'digits-of-\xcf\x80.txt']
①
第二个文件名是“digits-of-π.txt”(带有希腊字母π)。
②
给定一个byte
参数,listdir
以字节形式返回文件名:b'\xcf\x80'
是希腊字母π的 UTF-8 编码。
为了帮助处理作为文件名或路径名的str
或bytes
序列,os
模块提供了特殊的编码和解码函数os.fsencode(name_or_path)
和os.fsdecode(name_or_path)
。自 Python 3.6 起,这两个函数都接受str
、bytes
或实现os.PathLike
接口的对象作为参数。
Unicode 是一个深奥的领域。是时候结束我们对str
和bytes
的探索了。
章节总结
我们在本章开始时否定了1 个字符 == 1 个字节
的概念。随着世界采用 Unicode,我们需要将文本字符串的概念与文件中表示它们的二进制序列分开,而 Python 3 强制执行这种分离。
在简要概述二进制序列数据类型——bytes
、bytearray
和memoryview
后,我们开始了编码和解码,列举了一些重要的编解码器,然后介绍了如何防止或处理由 Python 源文件中错误编码引起的臭名昭著的UnicodeEncodeError
、UnicodeDecodeError
和SyntaxError
。
在没有元数据的情况下考虑编码检测的理论和实践:理论上是不可能的,但实际上 Chardet 软件包对一些流行的编码做得相当不错。然后介绍了字节顺序标记作为 UTF-16 和 UTF-32 文件中唯一常见的编码提示,有时也会在 UTF-8 文件中找到。
在下一节中,我们演示了如何打开文本文件,这是一个简单的任务,除了一个陷阱:当你打开文本文件时,encoding=
关键字参数不是强制的,但应该是。如果你未指定编码,你最终会得到一个在不同平台上不兼容的“纯文本”生成程序,这是由于冲突的默认编码。然后我们揭示了 Python 使用的不同编码设置作为默认值以及如何检测它们。对于 Windows 用户来说,一个令人沮丧的认识是这些设置在同一台机器内往往具有不同的值,并且这些值是相互不兼容的;相比之下,GNU/Linux 和 macOS 用户生活在一个更幸福的地方,UTF-8 几乎是默认编码。
Unicode 提供了多种表示某些字符的方式,因此规范化是文本匹配的先决条件。除了解释规范化和大小写折叠外,我们还提供了一些实用函数,您可以根据自己的需求进行调整,包括像删除所有重音这样的彻底转换。然后我们看到如何通过利用标准的 locale
模块正确对 Unicode 文本进行排序——带有一些注意事项——以及一个不依赖于棘手的 locale 配置的替代方案:外部的 pyuca 包。
我们利用 Unicode 数据库编写了一个命令行实用程序,通过名称搜索字符——感谢 Python 的强大功能,只需 28 行代码。我们还简要介绍了其他 Unicode 元数据,并对一些双模式 API 进行了概述,其中一些函数可以使用 str
或 bytes
参数调用,产生不同的结果。
进一步阅读
Ned Batchelder 在 2012 年 PyCon US 的演讲“实用 Unicode,或者,我如何停止痛苦?”非常出色。Ned 是如此专业,他提供了演讲的完整文本以及幻灯片和视频。
“Python 中的字符编码和 Unicode:如何(╯°□°)╯︵ ┻━┻ 有尊严地处理”(幻灯片,视频)是 Esther Nam 和 Travis Fischer 在 PyCon 2014 上的出色演讲,我在这个章节中找到了这个简洁的题记:“人类使用文本。计算机使用字节。”
Lennart Regebro——本书第一版的技术审查者之一——在短文“澄清 Unicode:什么是 Unicode?”中分享了他的“Unicode 有用的心智模型(UMMU)”。Unicode 是一个复杂的标准,所以 Lennart 的 UMMU 是一个非常有用的起点。
Python 文档中官方的“Unicode HOWTO”从多个不同角度探讨了这个主题,从一个很好的历史介绍,到语法细节,编解码器,正则表达式,文件名,以及 Unicode-aware I/O 的最佳实践(即 Unicode 三明治),每个部分都有大量额外的参考链接。Mark Pilgrim 的精彩书籍Dive into Python 3(Apress)的第四章,“字符串”也提供了 Python 3 中 Unicode 支持的很好介绍。在同一本书中,第十五章描述了 Chardet 库是如何从 Python 2 移植到 Python 3 的,这是一个有价值的案例研究,因为从旧的 str
到新的 bytes
的转换是大多数迁移痛点的原因,这也是一个设计用于检测编码的库的核心关注点。
如果你了解 Python 2 但是对 Python 3 感到陌生,Guido van Rossum 的“Python 3.0 有什么新特性”列出了 15 个要点,总结了发生的变化,并提供了许多链接。Guido 以直率的话语开始:“你所知道的关于二进制数据和 Unicode 的一切都发生了变化。”Armin Ronacher 的博客文章“Python 中 Unicode 的更新指南”深入探讨了 Python 3 中 Unicode 的一些陷阱(Armin 不是 Python 3 的铁粉)。
第三版的Python Cookbook(O’Reilly)中的第二章“字符串和文本”,由大卫·比兹利和布莱恩·K·琼斯编写,包含了几个处理 Unicode 标准化、文本清理以及在字节序列上执行面向文本操作的示例。第五章涵盖了文件和 I/O,并包括“第 5.17 节 写入字节到文本文件”,展示了在任何文本文件下始终存在一个可以在需要时直接访问的二进制流。在后续的食谱中,struct
模块被用于“第 6.11 节 读取和写入二进制结构数组”。
尼克·科格兰的“Python 笔记”博客有两篇与本章非常相关的文章:“Python 3 和 ASCII 兼容的二进制协议”和“在 Python 3 中处理文本文件”。强烈推荐。
Python 支持的编码列表可在codecs
模块文档中的“标准编码”中找到。如果需要以编程方式获取该列表,请查看随 CPython 源代码提供的/Tools/unicode/listcodecs.py脚本。
书籍Unicode Explained由尤卡·K·科尔佩拉(O’Reilly)和Unicode Demystified由理查德·吉拉姆(Addison-Wesley)撰写,虽然不是针对 Python 的,但在我学习 Unicode 概念时非常有帮助。Programming with Unicode由维克多·斯汀纳自由出版,涵盖了 Unicode 的一般概念,以及主要操作系统和几种编程语言的工具和 API。
W3C 页面“大小写折叠:简介”和“全球网络字符模型:字符串匹配”涵盖了标准化概念,前者是一个简单的介绍,后者是一个以干燥标准语言撰写的工作组说明书—与“Unicode 标准附录#15—Unicode 标准化形式”相同的语调。Unicode.org的“常见问题,标准化”部分更易读,马克·戴维斯的“NFC FAQ”也是如此—他是几个 Unicode 算法的作者,也是本文撰写时 Unicode 联盟的主席。
2016 年,纽约现代艺术博物馆(MoMA)将 1999 年由 NTT DOCOMO 的栗田茂高设计的原始表情符号加入了其收藏品。回顾历史,Emojipedia发表了“关于第一个表情符号集的纠正”,将日本的 SoftBank 归功于已知最早的表情符号集,于 1997 年在手机上部署。SoftBank 的集合是 Unicode 中 90 个表情符号的来源,包括 U+1F4A9(PILE OF POO
)。马修·罗森伯格的emojitracker.com是一个实时更新的 Twitter 表情符号使用计数仪表板。在我写这篇文章时,FACE WITH TEARS OF JOY
(U+1F602)是 Twitter 上最受欢迎的表情符号,记录的出现次数超过 3,313,667,315 次。
¹ PyCon 2014 演讲“Python 中的字符编码和 Unicode”(幻灯片,视频)的第 12 张幻灯片。
² Python 2.6 和 2.7 也有bytes
,但它只是str
类型的别名。
³ 小知识:Python 默认使用的 ASCII“单引号”字符实际上在 Unicode 标准中被命名为 APOSTROPHE。真正的单引号是不对称的:左边是 U+2018,右边是 U+2019。
⁴ 在 Python 3.0 到 3.4 中不起作用,给处理二进制数据的开发人员带来了很多痛苦。这种逆转在PEP 461—为 bytes 和 bytearray 添加%格式化中有记录。
⁵ 我第一次看到“Unicode 三明治”这个术语是在 Ned Batchelder 在 2012 年美国 PyCon 大会上的优秀“务实的 Unicode”演讲中。
⁶ 来源:“Windows 命令行:Unicode 和 UTF-8 输出文本缓冲区”。
⁷ 有趣的是,微符号被认为是一个“兼容字符”,但欧姆符号不是。最终结果是 NFC 不会触及微符号,但会将欧姆符号更改为大写希腊字母 omega,而 NFKC 和 NFKD 会将欧姆符号和微符号都更改为希腊字符。
⁸ 重音符号只在两个单词之间唯一的区别是它们时才会影响排序—在这种情况下,带有重音符号的单词会在普通单词之后排序。
⁹ 再次,我找不到解决方案,但发现其他人报告了相同的问题。其中一位技术审阅者 Alex Martelli 在他的 Macintosh 上使用setlocale
和locale.strxfrm
没有问题,他的 macOS 版本是 10.9。总结:结果可能有所不同。
¹⁰ 那是一张图片—而不是代码清单—因为在我写这篇文章时,O’Reilly 的数字出版工具链对表情符号的支持不佳。
¹¹ 尽管在这个特定样本中,它并不比re
更擅长识别数字。