Chapter 1. Text
第1 章 文本处理
本章目录:
Introduction
介绍导言
Recipe 1.1. Processing a String One Character at a Time
Recipe 1.1. 逐个处理字符串中的各个字符
Recipe 1.2. Converting Between Characters and Numeric Codes
Recipe 1.2. 字符与其对应的数字编码之间的转换
Recipe 1.3. Testing Whether an Object Is String-like
Recipe 1.3. 测试一个 Object 是否为 String-like 对象
Recipe 1.4. Aligning Strings
Recipe 1.4. 对齐字符串
Recipe 1.5. Trimming Space from the Ends of a String
Recipe 1.5. 去除字符串末尾的空格
Recipe 1.6. Combining Strings
Recipe 1.6. 字符串的组合
Recipe 1.7. Reversing a String by Words or Characters
Recipe 1.7. 以单词或字符为单位对字符串进行反序排列
Recipe 1.8. Checking Whether a String Contains a Set of Characters
Recipe 1.8. 检测字符串是否包含特定的字符集合
Recipe 1.9. Simplifying Usage of Strings' translate Method
Recipe 1.9. 字符串 translate 方法的简化用法
Recipe 1.10. Filtering a String for a Set of Characters
Recipe 1.10. 以一组字符集合来过滤字符串
Recipe 1.11. Checking Whether a String Is Text or Binary
Recipe 1.11. 检查字符串包含的是文本还是二进制码
Recipe 1.12. Controlling Case
Recipe 1.12. 控制大小写
Recipe 1.13. Accessing Substrings
Recipe 1.13. 存取字符串中的子串
Recipe 1.14. Changing the Indentation of a Multiline String
Recipe 1.14. 改变多行字符串的缩进方式
Recipe 1.15. Expanding and Compressing Tabs
Recipe 1.15. Tabs 与 Spaces 互转
Recipe 1.16. Interpolating Variables in a String
Recipe 1.16. 替换字符串中的占位变量
Recipe 1.17. Interpolating Variables in a Stringin Python 2.4
Recipe 1.17. 在 Python 2.4 中替换字符串中的占位变量
Recipe 1.18. Replacing Multiple Patterns in a Single Pass
Recipe 1.18. 一次同时替换多种子串模式
Recipe 1.19. Checking a String for Any of Multiple Endings
Recipe 1.19. 检查字符串是否以多种特定的形式结尾
Recipe 1.20. Handling International Text with Unicode
Recipe 1.20. 利用 Unicode 处理国际化文本
Recipe 1.21. Converting Between Unicode and Plain Strings
Recipe 1.21. Unicode 字符串与普通字符串的相互转换
Recipe 1.22. Printing Unicode Charactersto Standard Output
Recipe 1.22. 在标准输出上打印 Unicode 字符
Recipe 1.23. Encoding Unicode Data for XML and HTML
Recipe 1.23. 为 XML 和 HTML 对 Unicode 数据进行编码
Recipe 1.24. Making Some Strings Case-Insensitive
Recipe 1.24. 让字符串区分大小写
Recipe 1.25. Converting HTML Documents to Text on a Unix Terminal
Recipe 1.25. 在 Unix 终端上将 HTML 文档转换为文本
Introduction
介绍导言
Credit: Fred L. Drake, Jr., PythonLabs
对任何脚本语言(scripting language)来说,文本处理是其主要应用域。人人都会同意:文本处理是有用处的。所有人都会需要通过各种方式对少许文本进行重新格式化或变换处理。当然,各种应用程序之间多少总有些许差异,因此若要找到恰好可供复用的代码来处理不同的文件格式(无论多么简单)之情形,那就有难度了。
1.1 What Is Text?
1.1 什么是文本(Text)?
简单问题,不是吗?毕竟我们看到的时候就知道是不是,对吧?文本(text)是由字符组成的序列,并因此而与二进制数据区别开来——二进制数据是由字节(byte)组成的序列。
不幸的是,所有进入应用程序的数据都是字节序列。尽管可以积累一些有用的经验之谈,供我们辨析数据是否能安全地(不一定是正确地)作为文本来处理,但没有哪个程序库提供的函数能够帮我们辨别某个特定的字节序列是否代表文本。
Python 中的字符串(strings)是字节或字符组成的不可变(immutable)序列。我们创建和处理字符串时,多数是将其作为字符序列来对待,其实许多情况下同样可以将其作为字节序列来对待。Unicode 字符串是 Unicode 字符组成的不可变序列:需要利用 codecs(coder-decoders)对象来进行 Unicode 字符串与普通字符串之间的相互转换;codecs 对象封装了“可将字符序列作为字节序列来对待”情形下的许多标准处理方法——也称为编码(encoding)及字符集(character sets)。需要注意的是,Unicode 字符串并不专门用来代表字节序列。Recipe 1.20、Recipe 1.21 及 Recipe 1.22 展示 Python 中 Unicode 的基础知识。
好吧,让我们假设应用程序根据上下文环境得知它要处理的是文本。这通常是最佳方案,因为此即外部输入所需的方案。我们能够辨识一个文件,要么是因为它具有众所周知的文件名以及定义好的格式(这种情形在 Unix 世界很常见),要么是因为它具有众所周知的扩展文件名从而暗示了其中内容的格式(这种情形在 Windows 上很常见)。但现在我们遇到问题了:我们不得不使用“字词间隔”这种“格式”来使文本变得有意义。文本不是很简单么?
让我们面对这一切吧:没有所谓“纯”文本,即使有的话,我们可能也对其无动于衷(可能的例外是计算语言学领域的应用中,纯文本确实可能就是研究对象)。我们想在应用程序中处理的东西,使包含在文本当中的信息。我们所关心的文本可能包含配置数据、用来控制或定义过程的命令、人类能够辨识的文档,甚至是表格数据。包含配置数据或一系列命令的文本通常具有颇为有限的语法结构,可以在处理之前检查之。将输入的文本中的错误告知用户,通常已经足够处理我们不希望发生的错误情形。
供人类辨识的文档在结构上趋于简单,但在细节上却差异繁多。鉴于此类文档通常以自然语言撰写,至少其语法和文法就可能难于检查。不同的文本可能使用不同的字符集或编码方式;如果除文本本身之外没有提供额外信息加以阐释,那么要辨识“创建该文本时采用了何种字符集或编码方式”就可能有难度甚至是不可能的。然而我们还是须要适当地支持某些自然语言文档的表示方案。自然语言文本还具有结构,但结构在文本中经常体现得不够明显,至少需要知晓撰写该文本所采用的语言才行。字符组成单词,单词组成句子,句子组成段落,还有可能有更大的文本结构存在。除非您知道文档中所采用的排版格式,否则光是段落就特别难以辨识:每行就是一个段落,还是多行组成一个段落呢?若是多行组成一个段落,那如何分辨哪些行组成一个段落呢?段落完全可能以空行、缩进或其他特殊标记来进行分隔。Recipe 19.10 的示例展示了读取由“以空行分隔的段落序列”组成的文本文件。
处理表格数据面临的许多问题与处理自然语言文本时遇到的问题类似,而且表格数据还增加了一个输入格式的维度:表格数据的文本不再是线性的字符序列,而是字符组成的二维矩阵,需要辨识和组织的东西是文本区块。
1.2 Basic Textual Operations
1.2 基本的文本操作
与处理其他数据格式一样,我们会在不同的时候对文本作不同的处理。然而还是有三个基本操作:
* 将数据进行分析后,存放到应用程序内部的结构中;
* 将输入转换成另一种类似的格式;
* 生成全新的数据。
数据分析能以多种方式进行。专用的分析器程序能够高效地处理某一特定的、极为有限的格式,许多种格式都能由这类专用分析器很好地处理。此类例子包括专门用于分析 RFC 2822 格式电子邮件头部信息的分析程序(详见 Python 标准库中的 rfc822 模块),以及能由 ConfigParser 模块进行处理的配置文件。专用于分析特定应用中文件格式的分析器的例子还有 netrc 模块,该模块基于 shlex 模块实现。shlex 模块是颇为典型的字元转换器(tokenizer),能够分析一些语法简单的语言,用于创建具有可读性的配置文件,以及支持用户借以输入命令的交互式提示符环境。诸如此类的专用分析器在 Python 标准库中非常丰富,第 2 章和第 13 章包含了运用这些分析器的 recipe 条目。也有供 Python 使用的更为专门的分析工具,这些工具内含于较大的 add-on packages 当中;第 16 章介绍导言描述了这些工具。
将文本从一种格转换为另一种格式,这件事情若作为文本处理来看待的话(在谈及文本时,我们通常首先想到的也就是文本处理),显得更为有趣。在本章当中我们将会看到用途各异的文本格式转换方法,既会讲如何处理存放在外部文件中的文本,也会讲如何处理内存中的字符串。
若要根据与应用相关的数据结构来生成文本数据,最简单的办法就是使用 Python 的 print 语句,也可以使用 file object 或 file-like object 的 write 方法。要完成此事,经常会使用应用对象所包含的方法或函数,将输出文件作为参数来进行调用。函数内部就可以使用形如下列的语句来完成实际工作:
print >>thefile, sometext
thefile.write(sometext)
上述语句将输出列印到相应的文件中去了。然而,这种做法通常不被认为是文本处理,因为其中并没有输入文本供处理。贯穿本书,还能找到同时使用 print 和 write 的例子。
1.3 Sources of Text
1.3 文本来源
在文本不太大的情形中,将文本作为字符串存放在内存中来进行处理就比较容易。搜索文本的操作可用几行代码完成,又快又容易,也不必担心在搜索过程中不慎超出缓冲区界限。将文本放在内存中作为简单字符串来对待,这样就非常容易充分地利用内建的字符串操作(即字符串对象的方法)。
基于文件的文本格式转换值得特别讨论,因为这可能涉及到实质性的负荷问题,包括与输入输出性能相关的负荷,以及必须真实存放在内存中的数据量。处理存放在磁盘中的数据时,由于数据大小的关系,我们经常希望避免将整个文件全数加载至内存:可不应该随随便便将 80 MB 的文件加载至内存呀!若我们的应用程序每一时刻只需要一部分数据,那么在采用“只将完整数据的一个小片段加载至内存进行处理”的方案时,仅因为我们留出了足够的内存空间供应用程序运行之用,就能使性能得到实质性提高。若我们关心的是内存缓冲区的管理,那么为了获取性能优势,我们可以对磁盘进行少量的读写操作,每次读写处理一大块数据。第 12 章中包含了文件相关的 recipe 条目。
一想到网络我们就不难想到另一种有趣的文本数据来源:文本通常是利用 socket 从网络取得的。我们总是可以将 socket 看作文件(使用 socket 对象的 makefile 方法),数据可能是通过 socket 一块一块地取得的,我们必须等待完整区块的数据获取完毕才能最终得到数据。在数据流结束之前,该文本数据所包含的数据可能还不完整,因此将“通过 makefile 创建的文件对象”传递给文本处理代码可能不完全合适。在处理通过网络连接到的数据时,我们经常须要在进一步处理数据之前,先将数据通过网络连接读取下来。若数据量很大,可以将其保存为文件,然后再对该文件进行文本处理操作。还可以构思更为复杂的方案,以便在获得所有数据之前就开始进行文本处理操作。适用于此种情形的分析器的例子可以在标准库的 htmllib 和 HTMLParser 模块中找到。
1.4 String Basics
1.4 字符串基础
Python 为文本处理提供的主要设施是字符串,即不可变的字符序列。其实共有两种字符串:1)普通字符串,包含 8-bit(ASCII)字符;2)Unicode 字符串,包含 Unicode 字符。这里我们不多累述 Unicode 字符串,因为其功能与普通字符串类似。Unicode 字符串与普通字符串的区别在于,Unicode 字符串中的每个字符占用 2 个(或 4 个)bytes ,共可以表示成千上万(甚至上亿)种不同的字符,而普通字符串的字符集只能表示 256 种不同的字符。若您要处理不同语种(特别是亚洲语系)的文本,那么 Unicode 字符串对您就相当重要了。普通字符串足以满足英语及有限的非亚洲语言集的处理要求。例如,所有西欧语系的字母都能用普通字符串进行编码,所采用的编码通常是称为 ISO-8859-1 的国际标准编码(若您需要能表示 Euro 货币符号的编码,可以使用 ISO-8859-15)。
在 Python 中,可以这样表示字面字符串(literal string,也经常被称为 string literal):
'this is a literal string'
"this is another string"
字符串的内容要用一对单引号或一对双引号包围起来。这两种引号用法相同,都允许在“用其中一种引号包围的字符串”中将另一种引号作为字符串的一部分,而且不需要使用反斜杠来插入 escape 字符:
'isn/'t that grand'
"isn't that grand"
要想让字面字符串跨占多行,可以在行尾追加反斜杠,表示该行与下一行是接续起来的:
big = "This is a long string/
that spans two lines."
若您希望输出字符串时也占用两行,就得在须要换行的位置插入新行符:
big = "This is a long string/n/
that prints on two lines."
另一种跨行表示方案是使用一对三引号(用三个单引号或三个双引号表示皆可)来包围字符串:
bigger = """
This is an even
bigger string that
spans three lines.
"""
使用了三引号,就不需要使用反斜杠表示换行接续,字符串中的断行会被作为 newline character 保留在 Python 的字符串对象中。您还可以在字面字符串前面加上“r”或“R”,以便将字符串声明为“raw string(原始字符串)”:
big = r"This is a long string/
with a backslash and a newline in it"
在 raw string 当中,带反斜杠 escape 字符不会生效,而是原样被作为普通字符序列来看待。您还可以在字符串之前追加“u”或“U”,以便将其声明为 Unicode 字符串:
hello = u'Hello/u0020World'
字符串是不可改变的,这意味着:无论您对字符串作了何种操作,操作结果总是经由产生新的字符串对象来体现,而不是在原有字符串的基础上进行更动。字符串是字符组成的序列,这意味着您可以通过索引来访问单个字符:
mystr = "my string"
mystr[0] # 'm'
mystr[-2] # 'n'
您还可以使用 slice(切割)操作来访问字符串中的某一部分子串:
mystr[1:4] # 'y s'
mystr[3:] # 'string'
mystr[-3:] # 'ing'
Slice 操作中还可以追加指定第三个参数,用来表示切割字符串时采集字符所用的步长:
mystr[:3:-1] # 'gnirt'
mystr[1::2] # 'ysrn'
您可以通过循环来访问字符串中的字符:
for c in mystr:
上面语句将 c 依次绑定为 mystr 字符串中的各个字符。您还可以通过 list 形成一个字符序列:
list(mystr) # returns ['m','y',' ','s','t','r','i','n','g']
您可以通过加号(“+”)来连接字符串:
mystr+'oid' # 'my stringoid'
您还可以通过乘号(“*”)来重复连接相同的字符串:
'xo'*3 # 'xoxoxo'
总体来说,您对任何其他序列型对象所做的操作,都可以对字符串施行——只要该操作不改变序列本身即可,因为字符串是不可改变的。
字符串对象包含许多有用的方法。例如,您可以通过调用 s.isdigit() 来检测字符串 s 的内容,如果 s 非空且其包含的所有字符都是数字,该方法就返回 true ,否则就返回 false 。您可以通过调用 s.toupper() 来产生一个新的字符串,其内容为 s 中所有小写字母变为大写之后的字符串。要在一个字符串 haystack 里搜索另一个字符串“needle”,您可以调用 haystack.count('needle'),该方法返回子串“needle”在字符串 haystack 中出现的次数。若您有一个跨占多行的大字符串,您可以使用 splitlines 方法将其分裂为单行字符串的 list :
list_of_lines = one_large_string.splitlines( )
您可以通过 join 方法来将多个字符串组合产生为单个大字符串:
one_large_string = '/n'.join(list_of_lines)
本章包含的 recipes 将展示许多字符串对象提供的方法(methods)。您可以在 Python 的 Library Reference 和 Python in a Nutshell 一书中找到字符串方法的完整文档。
Python 中的字符串还能通过 re 模块,以正则表达式(regular expression)方式进行操纵。正则表达式是强大(但复杂)的设施,或许您已经通过其他语言(比如 Perl)、vi 编辑器或诸如 grep 之类的文本模式的命令而对其有所熟悉。在本章后半部分的 recipes 当中,您会看到对正则表达式的运用。在 Library Reference 以及 Python in a Nutshell 一书中有对正则表达式的完整文档。若您需要精通正则表达式,推荐您阅读 J.E.F. Friedl 所著的 Mastering Regular Expressions(O'Reilly)——Python 中的正则表达式基本上与 Perl 中的一样,Friedl 的书对其进行了全面通透地讲解。
Python 的标准模块 string 提供了与字符串对象的方法几乎相同的功能,只不过 string 模块是以函数(function)来提供这些功能,而字符串对象是以方法(method)来提供这些功能。较之字符串对象的方法,string 模块还提供了:1)更多的函数,比如有用的 string.maketrans 函数,本章当中的几条 recipes 展示了其用法;2)字符串常量(例如 string.digits,其内容为 '0123456789');3)Python 2.4 中新追加的 Template 类,用于对包含内嵌变量的字符串进行简单但具适应性的格式化操作(您在本章的一条 recipe 中会看到对它的运用)。字符串格式化操作符“%”提供了便利的用法,用于将字符串摆放到一起,以及从诸如浮点数之类的对象中获取经过精确格式化的字符串。本章中的某些 recipes 向您展示了“%”的用法。Python 还有许多标准模块和扩展模块,用于进行各种字符串的特殊处理。本章不包含此类特殊处理的用法,但是在第 12 章会以整章专门讲述 XML 处理这个重要的议题。