Play Open
Loading Please wait Loading Please wait Loading Please wait Loading Please wait Loading Please wait Loading Please wait

Unicode 指南

Unicode 数据的读写¶

既然处理 Unicode 数据的代码写好了,下一个问题就是输入/输出了。如何将 Unicode 字符串读入程序,如何将 Unicode 转换为适于存储或传输的形式呢?

根据输入源和输出目标的不同,或许什么都不用干;请检查一下应用程序用到的库是否原生支持 Unicode。例如,XML 解析器往往会返回 Unicode 数据。许多关系数据库的字段也支持 Unicode 值,并且 SQL 查询也能返回 Unicode 值。

在写入磁盘或通过套接字发送之前,Unicode 数据通常要转换为特定的编码。可以自己完成所有工作:打开一个文件,从中读取一个 8 位字节对象,然后用 bytes.decode(encoding) 对字节串进行转换。但是,不推荐采用这种全人工的方案。

编码的多字节特性就是一个难题; 一个 Unicode 字符可以用几个字节表示。 如果要以任意大小的块(例如 1024 或 4096 字节)读取文件,那么在块的末尾可能只读到某个 Unicode 字符的部分字节,这就需要编写错误处理代码。 有一种解决方案是将整个文件读入内存,然后进行解码,但这样就没法处理很大的文件了;若要读取 2 GB 的文件,就需要 2 GB 的 RAM。(其实需要的内存会更多些,因为至少有一段时间需要在内存中同时存放已编码字符串及其 Unicode 版本。)

解决方案是利用底层解码接口去捕获编码序列不完整的情况。这部分代码已经是现成的:内置函数 open() 可以返回一个文件类的对象,该对象认为文件的内容采用指定的编码,read() 和 write() 等方法接受 Unicode 参数。只要用 open() 的 encoding 和 errors 参数即可,参数释义同 str.encode() 和 bytes.decode() 。

因此从文件读取 Unicode 就比较简单了:

with open('unicode.txt', encoding='utf-8') as f:

for line in f:

print(repr(line))

也可以在更新模式下打开文件,以便同时读取和写入:

with open('test', encoding='utf-8', mode='w+') as f:

f.write('\u4500 blah blah blah\n')

f.seek(0)

print(repr(f.readline()[:1]))

Unicode 字符 U+FEFF 用作字节顺序标记(BOM),通常作为文件的第一个字符写入,以帮助自动检测文件的字节顺序。某些编码(例如 UTF-16)期望在文件开头出现 BOM;当采用这种编码时,BOM 将自动作为第一个字符写入,并在读取文件时会静默删除。这些编码有多种变体,例如用于 little-endian 和 big-endian 编码的 “utf-16-le” 和 “utf-16-be”,会指定一种特定的字节顺序并且不会忽略 BOM。

在某些地区,习惯在 UTF-8 编码文件的开头用上“BOM”;此名称具有误导性,因为 UTF-8 与字节顺序无关。此标记只是声明该文件以 UTF-8 编码。要读取此类文件,请使用“utf-8-sig”编解码器自动忽略此标记。

Unicode 文件名¶

当今大多数操作系统都支持包含任意 Unicode 字符的文件名。 通常这是通过将 Unicode 字符串转换为某种根据具体系统而定的编码格式来实现的。 如今的 Python 倾向于使用 UTF-8:MacOS 上的 Python 已经在多个版本中使用了 UTF-8,而 Python 3.6 也已在 Windows 上改用了 UTF-8。 在 Unix 系统中,将只有一个 文件系统编码格式。 如果你已设置了 LANG 或 LC_CTYPE 环境变量的话;如果未设置,则默认编码格式还是 UTF-8。

sys.getfilesystemencoding() 函数将返回要在当前系统采用的编码,若想手动进行编码时即可用到,但无需多虑。在打开文件进行读写时,通常只需提供 Unicode 字符串作为文件名,会自动转换为合适的编码格式:

filename = 'filename\u4500abc'

with open(filename, 'w') as f:

f.write('blah\n')

os 模块中的函数也能接受 Unicode 文件名,如 os.stat() 。

os.listdir() 函数返回文件名,这引发了一个问题:它应该返回文件名的 Unicode 版本,还是应该返回包含已编码版本的字节串? 这两者 os.listdir() 都能做到,具体取决于你给出的目录路径是字节串还是 Unicode 字符串形式的。 如果你传入一个 Unicode 字符串作为路径,文件名将使用文件系统的编码格式进行解码并返回一个 Unicode 字符串列表,而传入一个字节串形式的路径则将返回字节串形式的文件名。 例如,假定默认 文件系统编码 为 UTF-8,运行以下程序:

fn = 'filename\u4500abc'

f = open(fn, 'w')

f.close()

import os

print(os.listdir(b'.'))

print(os.listdir('.'))

将产生以下输出:

$ python listdir-test.py

[b'filename\xe4\x94\x80abc', ...]

['filename\u4500abc', ...]

第一个列表包含 UTF-8 编码的文件名,第二个列表则包含 Unicode 版本的。

请注意,大多时候应该坚持用这些 API 处理 Unicode。字节串 API 应该仅用于可能存在不可解码文件名的系统;现在几乎仅剩 Unix 系统了。

识别 Unicode 的编程技巧¶

本节提供了一些关于编写 Unicode 处理软件的建议。

最重要的技巧如下:

程序应只在内部处理 Unicode 字符串,尽快对输入数据进行解码,并只在最后对输出进行编码。

如果尝试编写的处理函数对 Unicode 和字节串形式的字符串都能接受,就会发现组合使用两种不同类型的字符串时,容易产生差错。没办法做到自动编码或解码:如果执行 str + bytes,则会触发 TypeError。

当要使用的数据来自 Web 浏览器或其他不受信来源时,常用技术是在用该字符串生成命令行之前,或要存入数据库之前,先检查字符串中是否包含非法字符。请仔细检查解码后的字符串,而不是编码格式的字节串数据;有些编码可能具备一些有趣的特性,例如与 ASCII 不是一一对应或不完全兼容。如果输入数据还指定了编码格式,则尤其如此,因为攻击者可以选择一种巧妙的方式将恶意文本隐藏在经过编码的字节流中。

在文件编码格式之间进行转换¶

StreamRecoder 类可以在两种编码之间透明地进行转换,参数为编码格式为 #1 的数据流,表现行为则是编码格式为 #2 的数据流。

假设输入文件 f 采用 Latin-1 编码格式,即可用 StreamRecoder 包装后返回 UTF-8 编码的字节串:

new_f = codecs.StreamRecoder(f,

# en/decoder: 被 read() 用来编码其结果

# 并被 write() 用来解码其输入。

codecs.getencoder('utf-8'), codecs.getdecoder('utf-8'),

# reader/writer: 用于读取和写入流。

codecs.getreader('latin-1'), codecs.getwriter('latin-1') )

编码格式未知的文件¶

若需对文件进行修改,但不知道文件的编码,那该怎么办呢?如果已知编码格式与 ASCII 兼容,并且只想查看或修改 ASCII 部分,则可利用 surrogateescape 错误处理 handler 打开文件:

with open(fname, 'r', encoding="ascii", errors="surrogateescape") as f:

data = f.read()

# 对字符串“data”进行更改

with open(fname + '.new', 'w',

encoding="ascii", errors="surrogateescape") as f:

f.write(data)

surrogateescape 错误处理 handler 会把所有非 ASCII 字节解码为 U+DC80 至 U+DCFF 这一特殊范围的码位。当 surrogateescape 错误处理 handler用于数据编码并回写时,这些码位将转换回原样。

参考文献¶

David Beazley 在 PyCon 2010 上的演讲 掌握 Python 3 输入/输出 中,有一节讨论了文本和二进制数据的处理。

Marc-André Lemburg 演示的PDF 幻灯片“在 Python 中编写支持 Unicode 的应用程序” ,讨论了字符编码问题以及如何国际化和本地化应用程序。这些幻灯片仅涵盖 Python 2.x。

Python Unicode 实质 是 Benjamin Peterson 在 PyCon 2013 上的演讲,讨论了 Unicode 在 Python 3.3 中的内部表示。

Posted in 图鉴收藏
Previous
All posts
Next