Python 正则表达式(更长的正则表达式示例)
更长的正则表达式示例
我们现在将浏览一个深入的示列,它以不同的方式使用正则表达式来操作字符串。首先
是一些实际上生成用于操作随机数(但不是太随机)的代码。示例1-5 展示了gendata.py,这
是一个生成数据集的脚本。尽管该程序只是将简单地将生成的字符串集显示到标准输出,但
是该输出可以很容易重定向到测试文件。
示例1-5 用于正则表达式练习的数据生成器(gendata.py)
该脚本为正则表达式练习创建随机数据,然后将生成的数据输出到屏幕。要将该程序移植到Python 3,仅需要将
print 语句修改为函数,将xrange()函数修改为range(),以及将sys.maxint 修改为sys.maxsize。
from random import randrange,choice
from string import ascii_lowercase as lc
from sys import maxint
from time import ctimetlds=('com','edu','net','org','gov')for i in xrange(randrange(5,11)):dtint=randrange(maxint) #pick datedtstr=ctime(dtint) #date stringllen=randrange(4,8) #login is shorterlogin=''.join(choice(lc) for j in range(llen))dlen=randrange(llen,13) #domain is longerdom=''.join(choice(lc) for j in xrange(dlen))print('%s::%s@%s.%s::%d-%d-%d' % (dtstr,login,dom,choice(tlds),dtint,llen,dlen))
该脚本生成拥有三个字段的字符串,由一对冒号或者一对双冒号分隔。第一个字段是随
机(32 位)整数,该整数将被转换为一个日期。下一个字段是一个随机生成的电子邮件地址。
最后一个字段是一个由单横线(-)分隔的整数集。
运行这段代码,我们将获得以下输出(读者将会从此获益颇多),并将该输出在本地另存
为redata.txt 文件。
Thu Jul 22 19:21:19 2004::izsp@dicqdhytvhv.edu::1090549279-4-11
Sun Jul 13 22:42:11 2008::zqeu@dxaibjgkniy.com::1216014131-4-11
Sat May 5 16:36:23 1990::fclihw@alwdbzpsdg.edu::641950583-6-10
Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::1171590364-6-8
Thu Jun 26 19:08:59 2036::ugxfugt@jkhuqhs.net::2098145339-7-7
Tu e Apr 10 01:04:45 2012::zkwaq@rpxwmtikse.com::1334045085-5-10
读者或者可能会辨别出来,但是来自该程序的输出是为正则表达式处理做准备的。后续将逐
行解释,我们将实现一些正则表达式来操作这些数据,以及为本章末尾的练习留下很多内容。
匹配字符串
对于后续的练习,为正则表达式创建宽松和约束性的版本。建议读者在一个简短的应用
中测试这些正则表达式,该应用利用之前所展示的示例文件redata.txt(或者使用通过运行
gendata.py 生成的数据)。当做练习时,读者将需要再次使用该数据。
在将正则表达式放入应用中之前,为了测试正则表达式,我们将导入re 模块,然后将
redata.txt 中的一个示例行赋给字符串变量data。如下所示,这些语句在所有展示的示例中都
是常量。
>>> import re
>> > data = 'Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::1171590364-6-8'
在第一个示例中,我们将创建一个正则表达式来提取(仅仅)数据文件redata.txt 中每一
行时间戳中一周的几天。我们将使用下面的正则表达式。
"^ Mon|^Tue|^Wed|^Thu|^Fri|^Sat|^Sun"
该示例需要字符串以列出的7 个字符串中的任意一个开头(“^”正则表达式中的脱字符)。
如果我们将该正则表达式“翻译”成自然语言,读起来就会像这样:“字符串应当以“Mon”,
“Tue”,. . . ,“Sat”或者“Sun”开头。
换句话说,如果按照如下所示的方式对日期字符串分组,我们就可以使用一个脱字符来
替换所有脱字符。
"^ (Mon|Tue|Wed|Thu|Fri|Sat|Sun)"
括住字符串集的圆括号意思是:这些字符串中的一个将会有一次成功匹配。这是我们一
开始就使用的“友好的”正则表达式版本,该版本并没有使用圆括号。如下所示,在这个修
改过的正则表达式版本中,可以以子组的方式来访问匹配字符串。
>>> patt = '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)'
>>> m = re.match(patt, data)
>>> m.group() # entire match
'Thu'
>>> m.group(1) # subgroup 1
'Thu'
>>> m.groups() # all subgroups
(' Thu',)
我们在该示例所实现的这个特性可能看起来并不是革命性的,但是在下一个示例或者作
为正则表达式的一部分提供额外数据来实现字符串匹配操作的任何地方,它确定有它的独到
之处,即使这些字符并不是你所感兴趣字符的一部分。
以上两个正则表达式都是非常严格的,尤其是要求一个字符串集。这可能在一个国际化
的环境中并不能良好地工作,因为所在的环境中会使用当地的日期和缩写。一个宽松的正则
表达式将为:^\w{3}。该正则表达式仅仅需要一个以三个连续字母数字字符开头的字符串。
再一次,将正则表达式转换为正常的自然语言:脱字符^表示“作为起始”,\w 表示任意单个
字母数字字符,{3}表示将会有3 个连续的正则表达式副本,这里使用{3}来修饰正则表达式。
再一次,如果想要分组,就必须使用圆括号,例如^(\w{3})。
>>> patt = '^(\w{3})'
>>> m = re.match(patt, data)
>>> if m is not None: m.group()
...
'Thu'
>>> m.group(1)
'T hu'
注意,正则表达式^(\w){3}是错误的。当{3}在圆括号中时,先匹配三个连续的字母数字字符,
然后表示为一个分组。但是如果将{3}移到外部,它就等效于三个连续的单个字母数字字符。
>>> patt = '^(\w){3}'
>>> m = re.match(patt, data)
>>> if m is not None: m.group()
...
'Thu'
>>> m.group(1)
'u '
当我们访问子组1 时,出现字母“u”的原因是子组1 持续被下一个字符替换。换句话说,
m.group(1)以字母“T”作为开始,然后变为“h”,最终被替换为“u”。这些是单个字母数字
字符的三个独立(并且重叠)分组,与一个包含三个连续字母数字字符的单独分组相反。
在下一个(而且是最后)的示例中,我们将创建一个正则表达式来提取redata.txt 每一行
的末尾所发现的数字字段。
搜索与匹配……还有贪婪
然而,在创建任何正则表达式之前,我们就意识到这些整数数据项位于数据字符串的末
尾。这就意味着我们需要选择使用搜索还是匹配。发起一个搜索将更易于理解,因为我们确
切知道想要查找的内容(包含三个整数的数据集),所要查找的内容不是在字符串的起始部分,
也不是整个字符串。如果我们想要实现匹配,就必须创建一个正则表达式来匹配整个行,然
后使用子组来保存想要的数据。要展示它们之间的差别,就需要先执行搜索,然后实现匹配,
以展示使用搜索更适合当前的需要。
因为我们想要寻找三个由连字符分隔的整数,所以可以创建自己的正则表达式来说明这
一需求:\d±\d±\d+。该正则表达式的含义是,“任何数值的数字(至少一个)后面跟着一个
连字符,然后是多个数字、另一个连字符,最后是一个数字集。”我们现在将使用search()来
测试该正则表达式:
>>> patt = '\d+-\d+-\d+'
>>> re.search(patt, data).group() # entire match
'1 171590364-6-8'
一个匹配尝试失败了,为什么呢?因为匹配从字符串的起始部分开始,需要被匹配的数
值位于字符串的末尾。我们将不得不创建另一个正则表达式来匹配整个字符串。但是可以使
用惰性匹配,即使用“.+”来表明一个任意字符集跟在我们真正感兴趣的部分之后。
patt = '.+\d+-\d+-\d+'
>>> re.match(patt, data).group() # entire match
'T hu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::1171590364-6-8'
该正则表达式效果非常好,但是我们只想要末尾的数字字段,而并不是整个字符串,因
此不得不使用圆括号对想要的内容进行分组。
>>> patt = '.+(\d+-\d+-\d+)'
>>> re.match(patt, data).group(1) # subgroup 1
'4 -6-8'
发生了什么?我们将提取1171590364-6-8,而不仅仅是4-6-8。第一个整数的其余部分在哪
儿?问题在于正则表达式本质上实现贪婪匹配。这就意味着对于该通配符模式,将对正则表达
式从左至右按顺序求值,而且试图获取匹配该模式的尽可能多的字符。在之前的示例中,使用
“.+”获取从字符串起始位置开始的全部单个字符,包括所期望的第一个整数字段。\d+仅仅需
要一个数字,因此将得到“4”,其中.+匹配了从字符串起始部分到所期望的第一个数字的全部
内容:“Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::117159036”,如图1-2 所示。
其中的一个方案是使用“非贪婪”操作符“?”。读者可以在“*”、“+”或者“?”之后使
用该操作符。该操作符将要求正则表达式引擎匹配尽可能少的字符。因此,如果在“.+”之
后放置一个“?”,我们将获得所期望的结果,如图1-3 所示。
>>> patt = '.+?(\d+-\d+-\d+)'
>>> re.match(patt, data).group(1) # subgroup 1
'1171590364-6-8'
另一个实际情况下更简单的方案,就是把“::”作为字段分隔符。读者可以仅仅使用正
则字符串strip(‘:: ‘)方法获取所有的部分,然后使用strip(’-’)作为另一个横线分隔符,就能够获
取最初想要查询的三个整数。现在,我们不想先选择该方案,因为这就是我们如何将字符串放在一起,以使用gendata.py 作为开始!
最后一个示例:假定我们仅想取出三个整数字段中间的那个整数。如下所示,这就是实现的
方法(使用一个搜索,这样就不必匹配整个字符串):-(\d+)-。尝试该模式,将得到以下内容。
>>> patt = '-(\d+)-'
>>> m = re.search(patt, data)
>>> m.group() # entire match
'-6-'
>>> m.group(1) # subgroup 1
'6 '
本章几乎没有涉及正则表达式的强大功能,在有限的篇幅里面我们不可能做到。然而,
我们希望已经向读者提供了足够有用的介绍性信息,使读者能够掌握这个强有力的工具,并
融入到自己的编程技巧里面。建议读者阅读参考文档以获取在Python 中如何使用正则表达式
的更多细节。对于想要更深入研究正则表达式的读者,建议阅读由 Jeffrey E. F. Friedl.编写的
Mastering Regular Expressions。