Ned Batchelder 在 2012 年的 PyCon US 所做的演讲“Pragmatic Unicode—or—How Do I Stop the Pain?”(http://nedbatchelder.com/text/unipain.html)非常出色。Ned 很专业,除了幻灯片和视频之外,他还提供了完整的文字记录。Esther Nam 和 Travis Fischer 在 PyCon 2014 做了一场精彩的演讲:“Character encoding and Unicode in Python: How to ( ╯ ° □ °) ╯︵ ┻━┻ with dignity”[ 幻灯片(http://www.slideshare.net/fischertrav/character-encoding-unicode-how-to-with-dignity-33352863),视频(http://pyvideo.org/pycon-us-2014/character-encoding-and-unicode-in-python.html)]。本章开头那句简短有力的话就是出自这次演讲:“人类使用文本,计算机使用字节序列。”本书的技术审校之一 Lennart Regebro 在“Unconfusing Unicode: What Is Unicode?”(https://regebro.wordpress.com/2011/03/23/unconfusing-unicode-what-is-unicode/)这篇短文中提出了“Useful Mental Model of Unicode(UMMU)”。Unicode 是个复杂的标准,Lennart 提出的 UMMU 是个很好的切入点。
Python 文档中的“Unicode HOWTO”一文(https://docs.python.org/3/howto/unicode.html)从几个不同的角度对本章所涉及的话题做了讨论,从编码历史到句法细节、编解码器、正则表达式、文件名和 Unicode 的 I/O 最佳实践(即 Unicode 三明治),而且各节都给出了大量参考资料链接。Dive into Python 3 是一本非常优秀的书(Mark Pilgrim 著,http://www.diveintopython3.net),其中第 4 章“Strings”(http://www.diveintopython3.net/strings.html)对 Python 3 对 Unicode 的支持做了很好的介绍。此外,该书的第 15 章(http://getpython3.com/diveintopython3/case-study-porting-chardet-to-python-3.html)阐述了 Chardet 库从 Python 2 移植到 Python 3 的过程,这是一个宝贵的案例分析,从中可以看出,从旧的 str 类型转到新的 bytes 类型是造成迁移如此痛苦的主要原因,也是检测编码的库应该关注的重点。
如果你用过 Python 2,但是刚接触 Python 3,可以阅读 Guido van Rossum 写的“What's New in Python 3.0”(https://docs.python.org/3.0/whatsnew/3.0.html#text-vs-data-instead-of-unicode-vs-8-bit),这篇文章简要列出了新版的 15 点变化,而且附有很多链接。Guido 开门见山地说道:“你自以为知道的二进制数据和 Unicode 知识全都变了。”Armin Ronacher 的博客文章“The Updated Guide to Unicode on Python”(http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/)深入分析了 Python 3 中 Unicode 的一些陷阱(Armin 不是很热衷于 Python 3)。
《Python Cookbook(第 3 版)中文版》(David Beazley 和 Brian K. Jones 著)的第 2 章“字符串和文本”中有几个诀窍谈到了 Unicode 规范化、清洗文本,以及在字节序列上执行面向文本的操作。第 5 章涵盖文件和 I/O,“5.17 将字节数据写入文本文件”指出,任何文本文件的底层都有一个二进制流,如果需要可以直接访问。之后的“6.11 读写二进制结构的数组”用到了 struct 模块。
Nick Coghlan 的“Python Notes”博客中有两篇文章与本章的话题十分相关:“Python 3 and ASCII Compatible Binary Protocols”(http://python-notes.curiousefficiency.org/en/latest/python3/binary_protocols.html)和“Processing Text Files in Python 3”(http://python-notes.curiousefficiency.org/en/latest/python3/text_file_processing.html)。强烈推荐阅读。
Python 3.5 将为二进制序列引入新的构造方法和方法,而且会废弃目前使用的构造方法签名(参见“PEP 467—Minor API improvements for binary sequences”,https://www.python.org/dev/peps/pep-0467/)。此外,Python 3.5 还会实现“PEP 461—Adding % formatting to bytes and bytearray”(https://www.python.org/dev/peps/pep-0461/)。
Python 支持的编码列表参见 codecs 模块文档的“Standard Encodings”一节(https://docs.python.org/3/library/codecs.html#standard-encodings)。如果需要通过编程的方式获得那个列表,看看 CPython 源码中 /Tools/unicode/listcodecs.py 脚本(https://hg.python.org/cpython/file/6dcc96fa3970/Tools/unicode/listcodecs.py)是怎么做的。
Martijn Faassen 的文章“Changing the Python Default Encoding Considered Harmful”(http://blog.startifact.com/posts/older/changing-the-python-default-encoding-considered-harmful.html)和 Tarek Ziadé的文章“sys.setdefaultencoding Is Evil”(http://blog.ziade.org/2008/01/08/syssetdefaultencoding-is-evil/)解释了为什么一定不能修改 sys.getdefaultencoding() 获取的编码,即便知道怎么做也不能改。
Unicode Explained(Jukka K. Korpela 著,O'Reilly 出版社,http://shop.oreilly.com/product/9780596101213.do)和 Unicode Demystified(Richard Gillam 著,Addison-Wesley 出版社,http://www.informit.com/store/unicode-demystified-a-practical-programmers-guide-to-9780201700527)这两本书不是针对 Python 的,但在我学习 Unicode 相关概念时给了我很大的帮助。Victor Stinner 的著作 Programming with Unicode(http://unicodebook.readthedocs.org/index.html)是一本免费的自出版图书(遵守 CC BY-SA 协议),其中讨论了一般的 Unicode 话题,以及主流操作系统和几门编程语言(包括 Python)中的相关工具和 API。
W3C 网站中的“Case Folding: An Introduction”(https://www.w3.org/International/wiki/Case_folding)和“Character Model for the World Wide Web: String Matching and Searching”(https://www.w3.org/TR/charmod-norm/)讨论了规范化相关的概念,前者是介绍性文章,后者则是以枯燥的标准用语写就的工作草案——“Unicode Standard Annex #15—Unicode Normalization Forms”(http://unicode.org/reports/tr15/)也是这种风格。Unicode.org 网站中的“Frequently Asked Questions / Normalization”(http://www.unicode.org/faq/normalization.html)更容易理解, Mark Davis 写的“NFC FAQ”(http://www.macchiato.com/unicode/nfc-faq)也是如此。Mark 是多个 Unicode 算法的作者,在我写作本书时,他还担任 Unicode 联盟的主席。
杂谈
“纯文本”是什么
对于经常处理非英语文本的人来说,“纯文本”并不是指“ASCII”。Unicode 词汇表(http://www.unicode.org/glossary/#plain_text)是这样定义纯文本的:
只由特定标准的码位序列组成的计算机编码文本,其中不含其他格式化或结构化信息。
这个定义的前半句说得很好,但是我不同意后半句。HTML 就是包含格式化和结构化信息的纯文本格式,但它依然是纯文本,因为 HTML 文件中的每个字节都表示文本字符(通常使用 UTF-8 编码),没有任何字节表示文本之外的信息。.png 或 .xsl 文档则不同,其中多数字节表示打包的二进制值,例如 RGB 值和浮点数。在纯文本中,数字使用数字符号序列表示。
这本书是我用一种名为 AsciiDoc(http://www.methods.co.nz/asciidoc/,很讽刺)的纯文本格式撰写的,它是 O'Reilly 优秀的图书出版平台 Atlas(https://atlas.oreilly.com/)的工具链中的一部分。AsciiDoc 的源文件是纯文本,但用的是 UTF-8 编码,而不是 ASCII。如果不这样做的话,撰写本章必定痛苦不堪。姑且不管名称,AsciiDoc 是个很棒的工具。
Unicode 的世界正在不断扩张,但是有些边缘场景缺少支持工具。因此图 4-1、图 4-3 和图 4-4 中的内容要使用图像,因为渲染本书的字体中缺少一些我想展示的字符。不过,Ubuntu 14.04 和 OS X 10.9 的终端能正确显示,包括“mojibake”(文字化け)这个日文的词。
捉摸不透的 Unicode
讨论 Unicode 规范化时,我经常使用“往往”“多数”和“通常”等不确定的修饰语。很遗憾,我不能提供更可靠的建议,因为 Unicode 规则有很多例外,很难百分之百确定。
例如,μ(微符号)是“兼容字符”,而 Ω(欧姆)和 Å(埃)符号却不是。这种差别是有真实影响的:NFC 规范化形式(推荐用于文本匹配)会把 Ω(欧姆)替换成 Ω(大写希腊字母欧米加),把 Å(埃)替换成 Å(上有圆圈的大写字母 A)。但是,作为“兼容字符”的 μ(微符号)不会替换成视觉等效的 μ(小写希腊字母 μ);不过在使用更极端的 NFKC 或 NFKD 规范化形式时会替换,但这是有损转换。
我能理解为什么把 μ(微符号)纳入 Unicode,因为
latin1编码中有它,如果换成希腊字母 μ,会破坏两种编码之间的转换。说到底,这就是微符号是“兼容字符”的原因。但是,如果是由于兼容原因而没把欧姆和埃符号纳入 Unicode,那为什么这两个符号要存在? Unicode 已经为GREEK CAPITAL LETTER OMEGA和LATIN CAPITAL LETTER A WITH RING ABOVE分配了码位,它们的外观一样,而且 NFC 规范化形式会替换它们。想想看吧。研究 Unicode 几小时之后,我猜测的原因是:Unicode 异常复杂,充满特殊情况,而且要覆盖各种人类语言和产业标准策略。
在 RAM 中如何表示字符串
Python 官方文档对字符串的码位在内存中如何存储避而不谈。毕竟,这是实现细节。理论上,怎么存储都没关系:不管内部表述如何,输出时每个字符串都要编码成字节序列。
在内存中,Python 3 使用固定数量的字节存储字符串的各个码位,以便高效访问各个字符或切片。
在 Python 3.3 之前,编译 CPython 时可以配置在内存中使用 16 位或 32 位存储各个码位。16 位是“窄构建”(narrow build),32 位是“宽构建”(wide build)。如果想知道用的是哪个,要查看
sys.maxunicode的值:65535 表示“窄构建”,不能透明地处理 U+FFFF 以上的码位。“宽构建”没有这个限制,但是消耗的内存更多:每个字符占 4 个字节,就算是中文象形文字的码位大多数也只占 2 个字节。这两种构建没有高下之分,应该根据自己的需求选择。从 Python 3.3 起,创建
str对象时,解释器会检查里面的字符,然后为该字符串选择最经济的内存布局:如果字符都在latin1字符集中,那就使用 1 个字节存储每个码位;否则,根据字符串中的具体字符,选择 2 个或 4 个字节存储每个码位。这是简述,完整细节参阅“PEP 393—Flexible String Representation”(https://www.python.org/dev/peps/pep-0393/)。灵活的字符串表述类似于 Python 3 对
int类型的处理方式:如果一个整数在一个机器字中放得下,那就存储在一个机器字中;否则解释器切换成变长表述,类似于 Python 2 中的long类型。这种聪明的做法得到推广,真是让人欢喜!