了解编码问题
如何找出字节序列的编码
如果我们不知道一个文件使用的是什么编码格式,可以使用统一字符编码侦测包 Chardet 来帮助我们判断文件的编码:
1 2
| $ chardetect 04-text-byte.asciidoc 04-text-byte.asciidoc: utf-8 with confidence 0.99
|
处理文本文件
处理文本你文件的最佳实践是 “Unicode Sandwich”。即:要尽早的把输入(例如读取文本文件时)的字节序列解码成字符串,而三明治中的 “肉片” 是程序的业务逻辑,在这里只能处理字符串对象。最后,要尽可能晚的把字符串编码成字节序列。
同时,在进行文本文件的读取和写入时,最好显式地指定编解码方式,不要依赖当前环境的默认值:
1 2 3
| >>> open('cafe.txt', 'w', encoding='utf-8').write('café') >>> open('cafe.txt', encoding='utf-8').read() café
|
为了正确比较二规范化 Unicode 字符串
因为 Unicode 有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。
例如,“café”
这个词可以使用两种方式构成,分别有 4 个和 5 个码位,但是结果完全一样:
1 2 3 4 5 6 7 8 9 10 11 12
| In [26]: s1 = 'café'
In [27]: s2 = 'cafe\u0301'
In [28]: s1, s2 Out[28]: ('café', 'café')
In [29]: len(s1), len(s2) Out[29]: (4, 5)
In [30]: s1 == s2 Out[30]: False
|
在 Unicode 标准中,'é'
和 'e\u0301'
这样的序列叫“标准等价物”(canonical equivalent),应用程序应该把它们视作相同的字符。但是,Python 看到的是不同的码位序列,因此判定二者不相等。
这个问题的解决方案是使用 unicodedata.normalize
函数提供的 Unicode 规范化。这个函数的第一个参数是这 4 个字符串中的一个:
'NFC'
:使用最少的码位构成等价的字符串。
'NFD'
:把组合字符分解成基字符和单独的组合字符。
'NFKC'
:使用最少的码位构成等价的字符串,对兼容字符有影响。
'NFKD'
:把组合字符分解成基字符和单独的组合字符,对兼容字符有影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| In [18]: from unicodedata import normalize
In [19]: s1 = 'café'
In [20]: s2 = 'cafe\u0301'
In [21]: len(s1), len(s2) Out[21]: (4, 5)
In [22]: len(normalize('NFC', s1)), len(normalize('NFC', s2)) Out[22]: (4, 4)
In [23]: len(normalize('NFD', s1)), len(normalize('NFD', s2)) Out[23]: (5, 5)
In [24]: normalize('NFC', s1) == normalize('NFC', s2) Out[24]: True
In [25]: normalize('NFD', s1) == normalize('NFD', s2) Out[25]: True
|
西方键盘通常能输出组合字符,因此用户输入的文本默认是 NFC 形式。不过,安全起见,保存文本之前,最好使用 normalize('NFC', user_text)
清洗字符串。
兼容字符
虽然 Unicode 的目标是为各个字符提供“规范的”码位,但是为了兼容现有的标准,有些字符会出现多次。例如,虽然希腊字母表中有 “μ”
这个字母(码位是 U+03BC,GREEK SMALL LETTER MU),但是 Unicode 还是加入了微符号 'µ'
(U+00B5),以便与 latin1 相互 转换。因此,微符号是一个“兼容字符”。
在 NFKC 和 NFKD 形式中,各个兼容字符会被替换成一个或多个“兼容分解”字符。二分之一 '½'(U+00BD)
经过兼容分解后得到的是三个字符序列 '1/2'
;微符号 'µ'(U+00B5)
经过兼容分解后得到的是小写字母 'μ'(U+03BC)
。
1 2 3 4 5 6 7 8 9 10 11
| In [31]: from unicodedata import normalize, name
In [32]: half = '½'
In [33]: normalize('NFKC', half) Out[33]: '1⁄2'
In [34]: four_squared = '4²'
In [36]: normalize('NFKC', four_squared) Out[36]: '42'
|
从上面的例子中能够看出,NFKC 或 NFKD 可能会损失或曲解信息,在使用这两种规范化形式时要格外注意。
大小写折叠
大小写折叠其实就是把所有文本变成小写,再做些其他转换。这个功能由 str.casefold()
方法(Python 3.3 新增)支持。
自 Python 3.4 起,str.casefold()
和 str.lower()
得到不同结果的有 116 个码位。Unicode 6.3 命名了 110 122 个字符,这只占 0.11%。
规范化文本匹配实用函数
下面两个工具函数能够帮助我们对 Unicode 字符进行比较:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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())
In [38]: s1 = 'café'
In [39]: s2 = 'cafe\u0301'
In [40]: s1 == s2 Out[40]: False
In [41]: nfc_equal(s1, s2) Out[41]: True
In [42]: nfc_equal('A', 'a') Out[42]: False
In [43]: s3 = 'Straße'
In [44]: s4 = 'strasse'
In [46]: s3 == s4 Out[46]: False
In [47]: nfc_equal(s3, s4) Out[47]: False
In [48]: fold_equal(s3, s4) Out[48]: True
|
去除变音符号
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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)
In [56]: order = '“Herr Voß: • ½ cup of OEtker™ caffè latte • bowl of açaí.”'
In [57]: shave_marks(order) Out[57]: '“Herr Voß: • ½ cup of OEtker™ caffe latte • bowl of acai.”'
|
Unicode 文本排序
在 Python 中,非 ASCII 文本的标准排序方式是使用 locale.strxfrm
函数,根据 locale 模块的文档 (https://docs.python.org/3/library/locale.html?highlight=strxfrm#locale.strxfrm),这个函数会“把字符串转换成适合所在区域进行比较的形式”。
但是上述的排序方式需要更改当前环境下的全局区域设置,而且,还需要操作系统支持这一功能,所以,推荐使用 PyUCA
库来进行 Unicode 字符的排序:
1 2 3 4 5 6 7 8 9 10
| In [1]: import pyuca
In [2]: coll = pyuca.Collator()
In [3]: fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
In [4]: sorted_fruits = sorted(fruits, key=coll.sort_key)
In [5]: sorted_fruits Out[5]: ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
|
Last updated:
能否,用你今天的微笑,補影一次合照