- 15.3 正则表达式和Python语言
- 15.3.1 re模块:核心函数和方法
- 15.3.2 使用compile()编译正则表达式
- 15.3.3 匹配对象和group()、groups()方法
- 15.3.4 用match()匹配字符串
- 15.3.5 search()在一个字符串中查找一个模式(搜索与匹配的比较)
- 15.3.6 匹配多个字符串(|)
- 15.3.7 匹配任意单个字符(.)
- 15.3.8 创建字符集合([])
- 15.3.9 重复、特殊字符和子组
- 15.3.10 从字符串的开头或结尾匹配及在单词边界上的匹配
- 15.3.11 用findall()找到每个出现的匹配部分
- 15.3.12 用sub()(和subn())进行搜索和替换
- 15.3.13 用split()分割(分隔模式)
15.3 正则表达式和Python语言
既然我们已知道了有关正则表达式本身的所有知识,那让我们来详细研究当前Python的默认正则表达式模块re模块吧。re模块在Pythonl.5版本被引入。如果你正在使用Python的早期版本,你将只能使用已过时的regex、regsub模块。这些模块具有Emacs风格,功能不丰富,而且与现在的re模块也不兼容。regex和regsub这两个模块已在Python 2.5版本时被移除了,在Python2.5及其后续版本,引入这两个模块中的任何一个将会引发Import Error异常。
但正则表达式本身是不变的,所以本小节中的大多数基本概念仍然适用于旧版的regex和reg-sub模块。与旧模块形成鲜明对比的是,新的re模块支持功能更强大、更通用的Perl风格(具体说是Perl5的风格)的正则表达式,允许多线程共享同一经过编译的正则表达式对象,同时它还支持对正则表达式分组进行命名和按名字调用。另外,有一个名叫reconvert的转换模块是帮助开发者从regex/regsub模块迁移到re模块的。但请注意,正则表达式有不同的风格,我们主要研究当今Python语言中使用的正则表达式。
re引擎已在Python1.6版本中被重写,改进了它的性能并添加了对Unicode的支持。接口并没有改变,因此模块的名字也保持不变。新的re引擎,内部被叫做sre,替代了1.5版本中内部名为pcre的re引擎。
15.3.1 re模块:核心函数和方法
表15.2列出了re模块最常用的函数和方法。其中有很多函数也与已编译的正则表达式对象(regex objects)和正则“匹配对象”(match objects)的方法同名并且具有相同功能。
在本小节,我们来看两个主要的函数/方法match()和search(),以及compile()函数。在下一节我们还会再介绍更多,但如果想进一步了解我们涉及或没有涉及的更多相关信息,我们建议你参阅Python的文档。
核心笔记:RE编译(何时应该使用compile函数?)
在第14章,我们曾说过Python的代码最终会被编译为字节码,然后才被解释器执行。我们特别提到用调用eval()或exec()调用一个代码对象而不是一个字符串,在性能上会有明显的提升,这是因为对前者来说,编译过程不必执行。换句话说,使用预编译代码对象要比使用字符串快,因为解释器在执行字符串形式的代码前必须先把它编译成代码对象。
这个概念也适用于正则表达式,在模式匹配之前,正则表达式模式必须先被编译成regex对象。由于正则表达式在执行过程中被多次用于比较,我们强烈建议先对它做预编译,而且,既然正则表达式的编译是必须的,那使用么预先编译来提升执行性能无疑是明智之举。re.compile()就是用来提供此功能的。
其实模块函数会对已编译对象进行缓存,所以不是所有使用相同正则表达式模式的 search()和match()都需要编译。即使这样,你仍然节省了查询缓存,和用相同的字符串反复调用函数的性能开销。在Python1.5.2版本里,缓存区可以容纳20个已编译的正则表达式对象,而在1.6版本里,由于另外添加了对Unicode的支持,编译引擎的速度变慢了一些,所以缓存区被扩展到可以容纳100个已编译的regex对象。
15.3.2 使用compile()编译正则表达式
我们稍后要讲到的大多数re模块函数都可以作为regex对象的方法。注意,尽管我们建议预编译,但它并不是必需的。如果你需要编译,就用方法,如果不需要,可以使用函数。幸运的是无论你用哪种方式-函数还是方法,名字都是相同的。(也许你曾对此好奇,这正是模块函数和方法完全一样的原因,例如search()、match()等)在后面的例子里,我们将用字符串,这样可以省去一个小步骤。我们仍会用到几个预编译代码对象,这样你可以知道它的过程是怎么回事。
编译rex对象时给出一些可选标识符,可以得到特殊的编译对象。这些对象将允许不区别大小写的匹配,或使用系统的本地设置定义的字母表进行匹配等。详情请参阅有关文档。这些标识符也可以作为参数传给模块(改字)版本的match()和search()进行特定模式的匹配,其中一些标识符已在前面做过简短介绍(例如,DOTALL,LOCALE)-这些标识符多数用于编译,也正因如此它们可以被传给模块版本的match()和search(),而match()和search()肯定要对正则表达式模式编译一次。如果你想在regex对象的方法中使用这些标识符,则必须在编译对象时传递这些参数。
除下面的方法外,regex对象还有一些数据属性,其中两个是创建时给定的编译标识符和正则表达式模式。
15.3.3 匹配对象和group()、groups()方法
在处理正则表达式时,除regex对象外,还有另一种对象类型-匹配对象。这些对象是在match()或 search()被成功调用之后所返回的结果。匹配对象有两个主要方法:group()和groups()。
group()方法或者返回所有匹配对象或是根据要求返回某个特定子组。groups()则很简单,它返回一个包含唯一或所有子组的元组。如果正则表达式中没有子组的话,groups()将返回一个空元组,而group()仍会返回全部匹配对象。
Python语言中的正则表达式支持对匹配对象进行命名的功能,这部分内容超出了本介绍性小节对正则表达式的讨论范围。我们建议你阅读re模块的文档,里面有我们省略掉的关于这些高级主题的详细内容。
15.3.4 用match()匹配字符串
我们先来研究re模块的函数、正则表达式对象(regex object)的方法:match()。match()函数尝试从字符串的开头开始对模式进行匹配。如果匹配成功,就返回一个匹配对象,而如果匹配失败了,就返回None。匹配对象的group()方法可以用来显示那个成功的匹配。下面是如何运用match()(group())的一个例子:
模式“foo”完全匹配字符串“foo”。在交互解析器中,我们能确定m就是一个匹配对象的实例。
这是当匹配失败时的例子,它返回None:
上面的匹配失败,所以m被赋值为None,因为我们写的if语句中没有什么行动,所以也没有什么指令动作被执行。在以后的例子中,为了简洁,在可能的情况下,我们会省去if检查语句,但在实际编程中,最好写上它,以防止出现AttributeError异常(失败后返回None,此时它是没有group()属性(方法)的)。
即使字符串比模式要长,匹配也可能成功;只要模式是从字符串的开始进行匹配的。例如,模式“foo”在字符串“food on the table”中找到一个匹配,因为它是从该字符串开头进行匹配的:
如你看到的,尽管字符串比模式要长,但从字符串开头有一个成功的匹配。子串“foo”是从那个较长的字符串中抽取出来的匹配部分。
我们甚至可以充分利用Python语言面向对象的特性,间接省略中间结果,将最终结果保存到一起:
注意,上面的例子中,如果匹配失败,会引发一个AttributeError异常。
15.3.5 search()在一个字符串中查找一个模式(搜索与匹配的比较)
其实,你要搜索的模式出现在一个字符串中间的机率要比出现在字符串开头的机率更大一些。这正是search()派上用场的时候。search和match的工作方式一样,不同之处在于search会检查参数字符串任意位置的地方给定正则表达式模式的匹配情况。如果搜索到成功的匹配,会返回一个匹配对象,否则返回None。
现在我们来举例说明match()和search()之间的区别。让我们举一个对长字符串进行匹配的例子。这次,我们用字符串“foo”去匹配“seafood”:
如你所见,这里没有匹配成功。match()尝试从字符串起始处进行匹配模式,即,模式中的“f”试匹配到字符串中首字母“s”上,这样匹配肯定是失败的。但字符串“foo”确实出现在“seafood”中,那我们如何才能让Python得出肯定的结果呢?答案是用search()函数。search()搜索字符串中模式首次出现的位置,而不是尝试(在起始处)匹配。严格地说,search()是从左到右进行搜索。
在本小节以后的内容里,将通过大量的例子展示如何在Python语言中运用正则表达式,我们会用到regex对象的方法match()和search(),匹配对象的方法group()、groups()和正则表达式语法中的绝大多数特殊字符和符号。
15.3.6 匹配多个字符串(|)
在15.2小节里,我们在正则表达式“bat|bet|bit”中使用了管道符号。下面,我们把这个正则表达式用到Python的代码里:
15.3.7 匹配任意单个字符(.)
以下的例子中,我们将说明句点是不能匹配换行符或非字符(即空字符串)的:
下面的例子是来搜索一个真正句点(小数点)的正则表达式,在正则表达式中,用反斜线对它进行转义,使句点失去它的特殊意义:
15.3.8 创建字符集合([])
前面我们曾讨论过“[cr][23][dp][o2]”和“r2d2|c3po”是不同的。从下面的例子中,可以看出“r2d2|c3po”与“[cr][23][dp][o2]”相比有更加严格的限制:
15.3.9 重复、特殊字符和子组
正则表达式中最常见的情况包括特殊字符的使用,正则表达式模式的重复出现,以及使用圆括号对匹配模式的各部分进行分组和提取操作。我们曾看到过一个关于简单电子邮件地址的正则表达式 (“\w+@\w+.com”)或许我们想要匹配的邮件地址比这个正则表达式的允许的要多。比如,为了在域名前添加主机名称支持,即,支持“www.xxx.com”,而不只是允许“xxx.com”做整个域名,我们就必须修改现有的正则表达式。为了表示主机名是可选的,我们要写一个模式匹配主机名(后面跟一个句点),然后用问号“?”表示此模式可出现0次或1次,表示此部分是可选的,再把这个可选的正则表达式插入到我们前面的那个正则表达式中去:“\w+@(\w+.)?\w+.com”。从下面的例子中可以看出,这个表达式容许“.com”前面有一个或两个名字:
接下来,我们用以下模式进一步扩展我们的例子,允许任意数量的子域名存在。请特别注意细节的变化,将?改为:“\w+@(\w+.)\w+.com”:
但我们必须要说明的是仅用字母或数字组成的字符不能满足邮件地址中可能出现的各种字符。上述正则表达式不匹配如“xxx-yyy.com”这样的域名或其他带有非单词字符(如“\W”等)的域名。
前面,我们曾讨论过用括号匹配并保存子组做进一步处理的好处,这样做比在确定正则表达式匹配后,再单写一个子程序来解析一个字符串要好。我们还特别提到用来匹配以“-”分隔的字母或数字组成的字符串和数字串的正则表达式“\w+-\d+”,以及如何通过对此正则表达式划分子组以构建一个新的正则表达式,“(\w+)-(\d+)”来完成任务,下面是旧版正则表达式的执行情况:
上面的代码中,一个正则表达式被用来匹配由三个字母或数字组成的字符串,再接着三个数字的字符串。这个正则表达式匹配“abc-123”,但不匹配“abc-xyz”。我们现在来修改正则表达式,使它能分别提取包含字母或数字的部分和仅含数字的部分。请注意我们是如何用group()方法访问每个子组以及用groups()方法获取一个包含所有匹配子组的元组的:
如你所见,group()通常用来显示所有匹配部分,也可用来获取个别匹配的子组。我们可用groups()方法获得一个包含所有匹配子组的元组。
下面这个简单的例子通过子组的不同排列组合,帮助我们理解得更透彻:
15.3.10 从字符串的开头或结尾匹配及在单词边界上的匹配
下面的例子强调了锚点性正则表达式操作符。这些锚点性正则表达式操作符主要被用于搜索而不是匹配,因为match()总是从字符串的开头进行匹配的。
你可能在这里注意到了原始字符串(raw strings)的出现。在本章末尾的核心笔记中,有关于它的说明。通常,在正则表达式中使用原始字符串是个好主意。
你还应该了解另外四个re模块函数和regex对象方法:findall()、sub()、subn()和split()。
15.3.11 用findall()找到每个出现的匹配部分
findall()自Python 1.5.2版本被引入。它用于非重叠地搜索某字符串中一个正则表达式模式出现的情况。findall()和search()相似之处在于二者都执行字符串搜索,但findall()和match()与search()不同之处是, findall()总返回一个列表。如果findall()没有找到匹配的部分,会返回空列表;如果成功找到匹配部分,则返回所有匹配部分的列表(按从左到右出现的顺序排列)。
包含子组的搜索会返回更复杂的一个列表,这样做是有意义的,因为子组是允许你从单个正则表达式中抽取特定模式的一种机制,比如,匹配一个完整电话号码中的一部分(例如区号),或完整电子邮件地址的一部分(例如登录名)。
正则表达式仅有一个子组时,findall()返回子组匹配的字符串组成的列表;如果表达式有多个子组,返回的结果是一个元组的列表,元组中每个元素都是一个子组的匹配内容,像这样的元组(每一个成功的匹配对应一个元组)构成了返回列表中的元素。这些内容初次听到可能令人费解,但如果你看看各种例子,就会明白了。
15.3.12 用sub()(和subn())进行搜索和替换
有两种函数/方法用于完成搜索和代替的功能:sub()和subn()。二者几乎是一样的,都是将某字符串中所有匹配正则表达式模式的部分进行替换。用来替换的部分通常是一个字符串,但也可能是一个函数,该函数返回一个用来替换的字符串。subn()和sub()一样,但它还返回一个表示替换次数的数字,替换后的字符串和表示替换次数的数字作为一个元组的元素返回。
15.3.13 用split()分割(分隔模式)
re模块和正则表达式对象的方法split()与字符串的split()方法相似,前者是根据正则表达式模式分隔字符串,后者是根据固定的字符串分割,因此与后者相比,显著提升了字符分割的能力。如果你不想在每个模式匹配的地方都分割字符串,你可以通过设定一个值参数(非零)来指定分割的最大次数。
如果分隔符没有使用由特殊符号表示的正则表达式来匹配多个模式,那re.split()和string.split()的执行过程是一样的,见以下的例子(在每一个冒号处分隔):
但运用正则表达式后,我们会发现re.split()成了一个功能更强大的工具。比如,Unix系统下who命令输出所有已登录系统的用户的信息:
假如我们想要保存用户的登录信息,比如说,登录名,用户登录时的电传,他们的登录的时间以及登录地址。用上面的string.split()很难有效果,因为分隔这些数据的空白符号是毫无规律且不确定的。还有一个问题,就是在登录时间的数据中,月、日、时之间有一个空格。而我们一般想把这些有关时间的数据排在一起。
你需要用某种方式来描述这样一种模式:“在两个或更多个空格符处进行分隔”。正则表达式很容易做到这一点。我们能很快写出这个正则表达式模式:“\s\s+”,含义是至少2个空白字符。我们来写一个名为rewho.py的程序,它读入who命令的输出-假设已保存到名为whodata.txt的文件中。起初,我们写的rewho.py脚本看起来像这样:
我们现在执行who命令,将输出结果保存到文件whodata.txt,然后调用rewho.py来看看结果:
这是不错的尝试,但还不完全正确。首先,我们原先没有预料到输出中会包含一个TAB符号(ASCII\011)(它看上去像是至少两个空格,对吗?)。而且,我们可能对保存用来结束每行的换行符NEWLINE(ASCII\012)也没什么兴趣。我们现在就做些改动来修正这些问题,同时提升程序的整体质量。
首先,我们改从脚本里执行who命令,而不是从外部调用它后将命令的输出结果保存到文件whodata.txt——这样重复的步骤很快会令人厌烦的。要从我们写的脚本里调用另一个程序,可以用os.popen()命令,这个命令在14.5.2小节已介绍过。尽管os.popen()只能在Unix系统中使用,但本例子意在阐明re.split()的用法,它可是跨系统平台的。
我们去掉每行行尾的换行符(NEWLINE),并添加检查单个TAB符号的模式,把TAB作为re.split()的可选分隔符。例15.1是脚本rewho.py的最终版本。
例15.1 Unix下who命令输出结果进行分隔(rewho.py)
此脚本调用who命令,解析命令的输出结果,根据不同的空白符号分隔数据。
运行脚本,我们得到如下(正确)结果:
在DOS/Windows环境下,用dir命令代替who命令,也可完成此练习。
趁我们还熟悉ASCII字符,我们要提醒注意的是正则表达式的特殊字符和特殊ASCII字符是容易混淆的。我们可能用\n来表示一个ASCII换行字符,但也可以用\d表示匹配一个数字的正则表达式。如果同一个符号在ASCII和正则表达式中都可以用,就容易出问题了,所以在下页的核心笔记中,我们推荐使用Python语言中的“原始字符串”来避免混淆。还要注意:“\w”和“\W”这两个表示字母或数字的字符受L或LOCALE编译标识符的影响,在Python 1.6至Python 2.0以后的版本中受(U或UNICODE的)Unicode标识符号影响。
核心笔记:Python原始字符串(raw strings)的用法
你可能已经看到前面关于原始字符串用法的一些例子了。原始字符串的产生正是由于有正则表达式的存在。原因是ASCII字符和正则表达式特殊字符间所产生的冲突。比如,特殊符号“\b”在ASCII字符中代表退格键,但同时“\b”也是一个正则表达式的特殊符号,代表“匹配一个单词边界”。为了让RE编译器把两个字符“\b”当成你想要表达的字符串,而不是一个退格键,你需要用另一个反斜线对它进行转义,即可以这样写:“\b”。
但这样做会把问题复杂化,特别是当你的正则表达式字符串里有很多特殊字符时,就更容易令人困惑了。在第6章,我们曾介绍过原始字符串,它经常被用于简化正则表达式的复杂程度。事实上,很多Python程序员在定义正则表达式时都只使用原始字符串。下面的例子用来说明退格键“\b”和正则表达式“\b”(包含或不包含原始字符串)之间的区别:
你可能注意到我们在正则表达式里使用“\d”,没用原始字符串,也没出现什么问题。那是因为ASCII里没有对应的特殊字符,所以正则表达式编译器能够知道你指的是一个十进制数字。