3.3 使用 Unicode 进行文字处理
我们的程序经常需要处理不同的语言和不同的字符集。“纯文本”的概念是虚构的。如果你住在讲英语国家,你可能在使用 ASCII 码而没有意识到这一点。如果你住在欧洲,你可能使用一种扩展拉丁字符集,包含丹麦语和挪威语中的“ø”,匈牙利语中的“ő”,西班牙和布列塔尼语中的“ñ”,捷克语和斯洛伐克语中的“ň”。在本节中,我们将概述如何使用 Unicode 处理使用非 ASCII 字符集的文本。
什么是 Unicode?
Unicode 支持超过一百万种字符。每个字符分配一个编号,称为编码点。在 Python 中,编码点写作\u
XXXX 的形式,其中 XXXX 是四位十六进制形式数。
在一个程序中,我们可以像普通字符串那样操纵 Unicode 字符串。然而,当 Unicode 字符被存储在文件或在终端上显示,它们必须被编码为字节流。一些编码(如 ASCII 和 Latin-2)中每个编码点使用单字节,所以它们可以只支持 Unicode 的一个小的子集,足够单个语言使用了。其它的编码(如 UTF-8)使用多个字节,可以表示全部的 Unicode 字符。
文件中的文本都是有特定编码的,所以我们需要一些机制来将文本翻译成 Unicode——翻译成 Unicode 叫做解码。相对的,要将 Unicode 写入一个文件或终端,我们首先需要将 Unicode 转化为合适的编码——这种将 Unicode 转化为其它编码的过程叫做编码,如3.3所示。
图 3.3:Unicode 解码和编码
从 Unicode 的角度来看,字符是可以实现一个或多个字形的抽象的实体。只有字形可以出现在屏幕上或被打印在纸上。一个字体是一个字符到字形映射。
从文件中提取已编码文本
假设我们有一个小的文本文件,我们知道它是如何编码的。例如,polish-lat2.txt
顾名思义是波兰语的文本片段(来源波兰语 Wikipedia;可以在http://pl.wikipedia.org/wiki/Biblioteka_Pruska
中看到)。此文件是 Latin-2 编码的,也称为 ISO-8859-2。nltk.data.find()
函数为我们定位文件。
>>> path = nltk.data.find('corpora/unicode_samples/polish-lat2.txt')
Python 的open()
函数可以读取编码的数据为 Unicode 字符串,并写出 Unicode 字符串的编码形式。它采用一个参数来指定正在读取或写入的文件的编码。因此,让我们使用编码 'latin2'
打开我们波兰语文件,并检查该文件的内容︰
>>> f = open(path, encoding='latin2')
>>> for line in f:
... line = line.strip()
... print(line)
Pruska Biblioteka Państwowa. Jej dawne zbiory znane pod nazwą
"Berlinka" to skarb kultury i sztuki niemieckiej. Przewiezione przez
Niemców pod koniec II wojny światowej na Dolny Śląsk, zostały
odnalezione po 1945 r. na terytorium Polski. Trafiły do Biblioteki
Jagiellońskiej w Krakowie, obejmują ponad 500 tys. zabytkowych
archiwaliów, m.in. manuskrypty Goethego, Mozarta, Beethovena, Bacha.
如果这不能在你的终端正确显示,或者我们想要看到字符的底层数值(或”代码点”),那么我们可以将所有的非 ASCII 字符转换成它们两位数\x
XX 和四位数 \u
XXXX 表示法︰
>>> f = open(path, encoding='latin2')
>>> for line in f:
... line = line.strip()
... print(line.encode('unicode_escape'))
b'Pruska Biblioteka Pa\\u0144stwowa. Jej dawne zbiory znane pod nazw\\u0105'
b'"Berlinka" to skarb kultury i sztuki niemieckiej. Przewiezione przez'
b'Niemc\\xf3w pod koniec II wojny \\u015bwiatowej na Dolny \\u015al\\u0105sk, zosta\\u0142y'
b'odnalezione po 1945 r. na terytorium Polski. Trafi\\u0142y do Biblioteki'
b'Jagiello\\u0144skiej w Krakowie, obejmuj\\u0105 ponad 500 tys. zabytkowych'
b'archiwali\\xf3w, m.in. manuskrypty Goethego, Mozarta, Beethovena, Bacha.'
上面输出的第一行有一个以\u
转义字符串开始的 Unicode 转义字符串,即\u0144
。相关的 Unicode 字符在屏幕上将显示为字形ń。在前面例子中的第三行中,我们看到\xf3
,对应字形为ó,在 128-255 的范围内。
在 Python 3 中,源代码默认使用 UTF-8 编码,如果你使用的 IDLE 或另一个支持 Unicode 的程序编辑器,你可以在字符串中包含 Unicode 字符。可以使用\u
XXXX 转义序列包含任意的 Unicode 字符。我们使用ord()
找到一个字符的整数序数。例如︰
>>> ord('ń')
324
324 的 4 位十六进制数字的形式是 0144(输入 hex(324)
可以发现这点),我们可以定义一个具有适当转义序列的字符串。
>>> nacute = '\u0144'
>>> nacute
'ń'
注意
决定屏幕上显示的字形的因素很多。如果你确定你的编码正确但你的 Python 代码仍然未能显示出你预期的字形,你应该检查你的系统上是否安装了所需的字体。可能需要配置你的区域设置来渲染 UTF-8 编码的字符,然后使用print(nacute.encode('utf8'))
才能在你的终端看到ń显示。
我们还可以看到这个字符在一个文本文件内是如何表示为字节序列的︰
>>> nacute.encode('utf8')
b'\xc5\x84'
unicodedata
模块使我们可以检查 Unicode 字符的属性。在下面的例子中,我们选择超出 ASCII 范围的波兰语文本的第三行中的所有字符,输出它们的 UTF-8 转义值,然后是使用标准 Unicode 约定的它们的编码点整数(即以U+
为前缀的十六进制数字),随后是它们的 Unicode 名称。
>>> import unicodedata
>>> lines = open(path, encoding='latin2').readlines()
>>> line = lines[2]
>>> print(line.encode('unicode_escape'))
b'Niemc\\xf3w pod koniec II wojny \\u015bwiatowej na Dolny \\u015al\\u0105sk, zosta\\u0142y\\n'
>>> for c in line: ![[1]](/projects/nlp-py-2e-zh/Images/7e6ea96aad77f3e523494b3972b5a989.jpg)
... if ord(c) > 127:
... print('{} U+{:04x} {}'.format(c.encode('utf8'), ord(c), unicodedata.name(c)))
b'\xc3\xb3' U+00f3 LATIN SMALL LETTER O WITH ACUTE
b'\xc5\x9b' U+015b LATIN SMALL LETTER S WITH ACUTE
b'\xc5\x9a' U+015a LATIN CAPITAL LETTER S WITH ACUTE
b'\xc4\x85' U+0105 LATIN SMALL LETTER A WITH OGONEK
b'\xc5\x82' U+0142 LATIN SMALL LETTER L WITH STROKE
如果你使用c
替换掉中的c.encode('utf8')
,如果你的系统支持 UTF-8,你应该看到类似下面的输出:
ó U+00f3 LATIN SMALL LETTER O WITH ACUTEś U+015b LATIN SMALL LETTER S WITH ACUTEŚ U+015a LATIN CAPITAL LETTER S WITH ACUTEą U+0105 LATIN SMALL LETTER A WITH OGONEKł U+0142 LATIN SMALL LETTER L WITH STROKE
另外,根据你的系统的具体情况,你可能需要用'latin2'
替换示例中的编码'utf8'
。
下一个例子展示 Python 字符串函数和re
模块是如何能够与 Unicode 字符一起工作的。(我们会在下面一节中仔细看看re
模块。\w
匹配一个”单词字符”,参见3.4)。
>>> line.find('zosta\u0142y')
54
>>> line = line.lower()
>>> line
'niemców pod koniec ii wojny światowej na dolny śląsk, zostały\n'
>>> line.encode('unicode_escape')
b'niemc\\xf3w pod koniec ii wojny \\u015bwiatowej na dolny \\u015bl\\u0105sk, zosta\\u0142y\\n'
>>> import re
>>> m = re.search('\u015b\w*', line)
>>> m.group()
'\u015bwiatowej'
NLTK 分词器允许 Unicode 字符串作为输入,并输出相应地 Unicode 字符串。
>>> word_tokenize(line)
['niemców', 'pod', 'koniec', 'ii', 'wojny', 'światowej', 'na', 'dolny', 'śląsk', ',', 'zostały']
在 Python 中使用本地编码
如果你习惯了使用特定的本地编码字符,你可能希望能够在一个 Python 文件中使用你的字符串输入及编辑的标准方法。为了做到这一点,你需要在你的文件的第一行或第二行中包含字符串:'# -*- coding: <coding> -*-'
。请注意 <coding> 必须是像'latin-1'
, 'big5'
或'utf-8'
这样的字符串 (见 3.4)。
图 3.4:Unicode 与 IDLE:IDLE 编辑器中 UTF-8 编码的字符串字面值;这需要在 IDLE 属性中设置了相应的字体;这里我们选择 Courier CE。
上面的例子还说明了正规表达式是如何可以使用编码的字符串的。