详解Python中的文本处理
字符串--不可改变的序列
如同大多数高级编程语言一样,变长字符串是Python中的基本类型。Python在“后台”分配内存以保存字符串(或其它值),程序员不必为此操心。Python还有一些其它高级语言没有的字符串处理功能。
在Python中,字符串是“不可改变的序列”。尽管不能“按位置”修改字符串(如字节组),但程序可以引用字符串的元素或子序列,就象使用任何序列一样。Python使用灵活的“分片”操作来引用子序列,字符片段的格式类似于电子表格中一定范围的行或列。以下交互式会话说明了字符串和字符片段的的用法:
字符串和分片
>>>s= "maryhadalittlelamb" >>>s[0] #indexiszero-based 'm' >>>s[3]= 'x' #changingelementin-placefails Traceback(innermostlast): File "<stdin>",line1, in ? TypeError:objectdoesn'tsupportitemassignment >>>s[11:18] #'slice'asubsequence 'little' >>>s[:4] #emptyslice-beginassumeszero 'mary' >>>s[4] #index4isnotincludedinslice[:4] '' >>>s[5:-5] #canuse"fromend"indexwithnegatives 'hadalittle' >>>s[:5]+s[5:] #slice-begin&slice-endarecomplimentary 'maryhadalittlelamb'
另一个功能强大的字符串操作就是简单的in关键字。它提供了两个直观有效的构造:
in关键字
>>>s= "maryhadalittlelamb" >>> for c in s[11:18]: print c, #printeachcharinslice ... little >>> if 'x' in s: print 'gotx' #testforcharoccurrence ... >>> if 'y' in s: print 'goty' #testforcharoccurrence ... goty
在Python中,有几种方法可以构成字符串文字。可以使用单引号或双引号,只要左引号和右引号匹配,常用的还有其它引号的变化形式。如果字符串包含换行符或嵌入引号,三重引号可以很方便地定义这样的字符串,如下例所示:
三重引号的使用
>>>s2= """Maryhadalittlelamb ...itsfleecewaswhiteassnow ...andeverywherethatMarywent ...thelambwassuretogo""" >>> print s2 Maryhadalittlelamb itsfleecewaswhiteassnow and everywherethatMarywent thelambwassuretogo
使用单引号或三重引号的字符串前面可以加一个字母"r"以表示Python不应该解释规则表达式特殊字符。例如:
使用"r-strings"
>>>s3= "this\nand\nthat" >>> print s3 this and that >>>s4=r "this\nand\nthat" >>> print s4 this\n and \nthat
在"r-strings"中,可能另外组成换码符的反斜杠被当作是常规反斜杠。在以后的规则表达式讨论中会进一步说明这个话题。
文件和字符串变量
我们谈到“文本处理”时,我们通常是指处理的内容。Python将文本文件的内容读入可以操作的字符串变量非常容易。文件对象提供了三个“读”方法:.read()、.readline()和.readlines()。每种方法可以接受一个变量以限制每次读取的数据量,但它们通常不使用变量。.read()每次读取整个文件,它通常用于将文件内容放到一个字符串变量中。然而.read()生成文件内容最直接的字符串表示,但对于连续的面向行的处理,它却是不必要的,并且如果文件大于可用内存,则不可能实现这种处理。
.readline()和.readlines()非常相似。它们都在类似于以下的结构中使用:
Python.readlines()示例
fh=open( 'c:\\autoexec.bat') for line in fh.readlines(): print line
.readline()和.readlines()之间的差异是后者一次读取整个文件,象.read()一样。.readlines()自动将文件内容分析成一个行的列表,该列表可以由Python的for...in...结构进行处理。另一方面,.readline()每次只读取一行,通常比.readlines()慢得多。仅当没有足够内存可以一次读取整个文件时,才应该使用.readline()。
如果正在使用处理文件的标准模块,可以使用cStringIO模块将字符串转换成“虚拟文件”(如果需要生成模块的子类,可以使用StringIO模块,初学者未必要这样做)。例如:
cStringIO模块
>>> import cStringIO >>>fh=cStringIO.StringIO() >>>fh.write( "maryhadalittlelamb") >>>fh.getvalue() 'maryhadalittlelamb' >>>fh.seek(5) >>>fh.write( 'ATE') >>>fh.getvalue() 'maryATEalittlelamb'
但是,请记住,cStringIO“虚拟文件”不是永久的,这一点与真正的文件不同。如果不保存它(如将它写入一个真正的文件,或者使用shelve模块或数据库),则程序结束时,它将消失。
标准模块:string
string模块也许是Python1.5.*标准发行版中最常用的模块。实际上,在Python1.6或更高版本中,string模块中的功能将作为内置字符串方法(在撰写本文时,详细信息尚未发布)。当然,任何执行文本处理任务的程序也许应该用以下这行开头:
开始使用string的方法
importstring
一般经验法则告诉我们,如果可以使用string模块完成任务,那么那就是正确的方法。与re(规则表达式)相比,string函数通常更快速,大多数情况下他们更易于理解和维护。第三方Python模块,包括某些用C编写的快速模块,适用于专门的任务,但可移植性和熟悉性都建议只要可能就使用string。如果您习惯于使用其它语言,也会有例外,但不如您想像的那样多。
string模块包含了几种类型的事物,如函数、方法和类;它还包含了公共常量的字符串。例如:
string用法例1
>>> import string >>>string.whitespace '\011\012\013\014\015' >>>string.uppercase 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
虽然可以用手写出这些常量,string版本或多或少确保了常量对于运行Python脚本的国家语言和平台将是正确的。
string还包括了以常见方式(可以结合这些方式来构成几种罕见的转换)转换字符串的函数。例如:
string用法例2
>>> import string >>>s= "maryhadalittlelamb" >>>string.capwords(s) 'MaryHadALittleLamb' >>>string.replace(s, 'little', 'ferocious') 'maryhadaferociouslamb'
还有许多没有在这里具体说明的其它转换;可以在Python手册中查找详细信息。
还可以使用string函数来报告字符串属性,如子串的长度或位置,例如:
string用法例3
>>> import string >>>s= "maryhadalittlelamb" >>>string.find(s, 'had')5>>>string.count(s, 'a')4
最后,string提供了非常Python化的奇特事物。.split()和.join()对提供了在字符串和字节组之间转换的迅捷方法,您会发现它们非常有用。用法很简单:
string用法例4
>>> import string>>>s= "maryhadalittlelamb" >>>L=string.split(s) >>>L [ 'mary', 'had', 'a', 'little', 'lamb'] >>>string.join(L, "-") 'mary-had-a-little-lamb'
当然,除了.join()之外,也许会利用列表来做其它事(如某些涉及我们熟悉的for...in...结构的事情)。
标准模块:re
re模块废弃了在老的Python代码中使用的regex和regsub模块。虽然相对于regex仍然有几个有限的优点,不过这些优点微不足道,不值得在新代码中使用。过时的模块可能会从未来的Python发行版中删除,并且1.6版可能有一个改进的接口兼容的re模块。所以,规则表达式仍将使用re模块。
规则表达式很复杂。也许有人会撰写关于这个主题的书,但实际上,已经有许多人这样做了!本文尝试捕捉规则表达式的“完全形态”,让读者可以掌握它。
规则表达式是一种很简练方法,用于描述可能在文本中出现的模式。是否会出现某些字符?是否按特定顺序出现?子模式是否会重复一定次数?其它子模式是否会排除在匹配之外?从概念上说,似乎不能用自然语言了直观地描述模式。诀窍是使用规则表达式的简洁语法来编码这种描述。
当处理规则表达式时,将它作为它自己的编程问题来处理,即使只涉及一或两行代码;这些行有效地构成了一个小程序。
从最小处着手。从最基本上看,任何规则表达式都涉及匹配特定的“字符类”。最简单的字符类就是单个字符,它在模式中只是一个字。通常,您希望匹配一类字符。可以通过将类括在方括号内来表明这是一个类;在括号中,可以有一组字符或者用破折号指定的字符范围。还可以使用许多命名字符类来确定您的平台和国家语言。以下是一些示例:
字符类
>>> import re >>>s= "maryhadalittlelamb" >>> if re.search( "m",s): print "Match!" #charliteral Match! >>> if re.search( "[@A-Z]",s): print "Match!" #charclass ... #matcheitherat-signorcapitalletter ... >>> if re.search( "\d",s): print "Match!" #digitsclass ...
可以将字符类看作是规则表达式的“原子”,通常会将那些原子组合成“分子”。可以结合使用分组和循环来完成此操作。由括号表示分组:括号中包含的任何子表达式都被看作是用于以后分组或循环的原子。循环则由以下几个运算符中的某一个来表示:"*"表示“零或多”;"+"表示“一或多”;"?"表示“零或一”。例如,请看以下示例:
样本规则表达式
ABC([d-w]*\d\d?)+XYZ
对于要匹配这个表达式的字符串,它必须以"ABC"开头、以"XYZ"结尾--但它的中间必须要有什么呢?中间子表达式是([d-w]*\d\d?),而且后面跟了“一或多”运算符。所以,字符串的中间必须包括一个(或者两个,或者一千个)与括号中的子表达式匹配的字符或字符串。字符串"ABCXYZ"不匹配,因为它的中间没有必要的字符。
不过这个内部子表达式是什么呢?它以d-w范围内的零或多个字母开头。一定要注意:零字母是有效匹配,虽然使用英语单词"some"(一些)来描述它,可能会感到很别扭。接着,字符串必须恰好有一个数字;然后有零或一个附加数字。(第一个数字字符类没有循环运算符,所以它只出现一次。第二个数字字符类有"?"运算符。)总而言之,这将翻译成“一个或两个数字”。以下是一些与规则表达式匹配的字符串:
匹配样本表达式的字符串
ABC1234567890XYZ ABCd12e1f37g3XYZ ABC1XYZ
还有一些表达式与规则表达式不匹配(想一想,它们为什么不匹配):
不匹配样本表达式的字符串
ABC123456789dXYZ ABCdefghijklmnopqrstuvwXYZ ABcd12e1f37g3XYZ ABC12345%67890XYZ ABCD12E1F37G3XYZ
需要一些练习才能习惯创建和理解规则表达式。但是,一旦掌握了规则表达式,您就具有了强大的表达能力。也就是说,转而使用规则表达式解决问题通常会很容易,而这类问题实际上可以使用更简单(而且更快速)的工具,如string,来解决。