15.4 正则表达式示例
现在我们来通读一个详细完整的例子,它展示了用正则表达式处理字符串的不同办法。第一步:拿出一段代码用来生成随机数据,生成的数据用于以后操作。例15.2中,脚本gendata.py生成一个数据集。虽然程序只是将生成的字符串显示到标准输出,但此输出结果也可以重定向到一个测试文件中。
例15.2 正则表达式练习的数据生成代码(gendata.py)
为练习使用正则表达式生成随机数据,并将产生的数据输出到屏幕。
这个脚本生成3个字段,字段由一对冒号,或双冒号分隔。第一个字段是一个随机(32位)整型,被转换为一个日期(见“核心笔记”)。第二个字段是一个随机产生的电子邮件(E-mail)地址,最后一个字段是由单个横线(-)分隔的一个整型集合。
执行这段代码,我们得到以下输出(你得到的输出肯定和本书中的不同),并把数据保存到本地文件redata.txt中:
你或许能看出来,这个程序的输出数据适合用正则表达式来处理。在我们逐行解释后,会用几个正则表达式对这些数据的进行操作,也为本章后面的练习做好准备。
逐行解释
1 ~ 6行
在这个示例脚本里,我们要使用多个模块。但因为我们只需要用到这些模块中的一两个函数,所以不必引入整个模块,只须引入模块中某些属性即可。我们用from-import而不是import正是基于这个原因。代码第一行是Unix起始提示符,后面是from-import这几行。
8行
domes是一组简单的包含顶级域名的集合,我们将从中随机挑选一个来随机生成电子邮件地址。
10 ~ 12行
每次gendata.py执行都会产生5~10行的输出。(这个脚本用函数random.randint()生成我们需要的所有随机整型。)在每个输出行中,我们从整个可能的范围(0〜231-1即[sys.maxint])里,随机选一个整型,然后把这个整型用time.ctime()转换成一个日期。大多数安装Python的基于Unix系统的计算机上,系统时间是根据1970年1月1日零点——即纪元(epoch)至今的秒数来计算的。如果我们选择32位整型,那系统日期就代表从纪元(epoch)到纪元后232秒之间的某个时刻。
14 ~ 22行
我们规定随机生成的邮箱地址中登录名的长度必须在4~7个字符。我们随机选择4~7个小写字母,依次将它们连结到一个字符串中。函数random.choice()的用处就是根据指定序列,随机返回该序列中的一个元素。在这里我们指定序列是26个小写字母,string.lowercase。我们规定虚拟邮箱地址的域名长度在4〜12个字符,但不能短于登录名的长度。最后,我们随机选择一些小写字母,依次将它们连接起来组成域名。
24 ~ 25行
这是本脚本的关键步骤:把随机数据组合到一起显示到输出行。以日期字符串开头,后面是分隔符,然后是随机生成的电子邮件地址。这个任意的电子邮件地址是我们把登录名,“@”符号,域名和一个随机选择的顶级域名连接到一起组成的。在最后一个双冒号后面,我们还加了一个随机整型字符串,它的前部分是与所选随机日期对应是整型,后面的部分分别是登录名和域名的长度,这几个整型之间由连字符分隔。
15.4.1 匹配一个字符串
在下面的练习里,写出你的正则表达式,包括宽松和限制性强的两个版本。我们建议你用前面的例子redata.txt(或你自己运行gendata.py生成的随机数据)来测试小程序里的这些正则表达式。在做练习的时候,你还会再次用到这些数据。
在把正则表达式写入到我们的小程序之前,我们先要对它进行测试。我们先引入re模块,将redata.txt中的一行数据赋值到一个字符串变量中。下面的语句在以下的两个示例中都是这样,没有变化。
在第一个例子中,我们将写一个正则表达式,用它从文件redata.txt的每一行中(仅)提取时间戳中的有关星期的数据字段。我们将用到以下这个正则表达式:
上例要求字符串是以所列出的7个字符串之一作为开头(“^”正则表达式操作符)。如果我们想把上面的正则表达式“翻译”过来,它的意思大概是:“字符串必须以“Mon,” “Tue, ”…, “Sat, ”或“Sun”之一打头”。
或者,我们可以只用一个“^”符号,将日期字符串归为一组:
在这组字符串集合两边的圆括号表示是只有满足这些字符串之一匹配才能成功。这是比我们前面看到的那个没有圆括号的正则表达式“更友好”。而且,使用这个修改后的正则表达式还有一个好处,能使我们方便地访问被匹配字符串的那个子组:
我们在这个例子里所看到的功能似乎没有那么新鲜或与众不同,但它对于下面的例子或是通过在正则表达式中添加额外数据来处理字符串匹配时就很有帮助了,即使这些字符并不是你感兴趣的字符串中的某部分。上面的两个正则表达式都是限制性很强的,特别要求只含有某些字符串。但在国际语言的系统环境中,使用各地区本地化时间和缩写的情况下,可能就行不通了。限制性更宽松的正则表达式是:“^\w{3}”。
这个正则表达式只要求字符串以三个由字符或数字组成的字符作开头。要是把它翻译成白话,就是,上箭头(^carat)表示以…开始,“\w”指任意一个由字符或数字组成的字符,“{3}”表示它左边描述的正则表达式模式必须连续出现三次。注意,如果你要对这个正则表达式分组,请用圆括号(),即,“^(\w{3})”:
注意,要是把正则表达式写成“^(\w){3}”是不正确的。如果把“{3}”写在圆括号里((\w{3})),表示匹配三个连续的由字符或数字组成的字符,再把这三个字符视为一个组。但如果把“{3}”挪到圆括号的外边((\w){3}),那现在它的含义就变成三个连续的单个由字符或数字组成的字符:
访问子组1的数据时,只看到“u”是因为子组1中的数据被不断地替换成下一个字符。也就是说,m.group(1)开始的结果是“T”,然后是“h”,最后又被替换成“u”。它们是三个独立(而且重复)的组,每个组是由字符或数字所组成的字符,而不是由连续的三个字符或数字组成的字符所形成的单个组。
在下一个(也是最后的)例子中,我们将写一个正则表达式来提取文件redata.txt中每行末尾的数值字段。
15.4.2 搜索与匹配的比较,“贪婪”匹配
在我们写正则表达式前,先明确这些整型数据项是在字符串数据的末尾。这意味着我们有两种选择:搜索(search)或匹配(match)。使用搜索更合适,因为我们确切地知道要搜索的数据是什么(三个整型的集合),它不在字符串的开头,也不是字符串的全部内容。如果我们用匹配(match)的方法,就不得不写一个正则表达式来匹配整行内容,并用子组保存我们感兴趣的那部分数据。为说明它们之间的区别,我们先用搜索搜索,再尝试用匹配来做,向你证明搜索搜索更适合。
因为我们要搜索的是三个由连字符号(-)分隔的整型集,所以我们写出如下正则表达式:“\d+-\d+-\d+”。这个正则表达式描述的是,“任意数字(至少有一个),后面有连字符号(-),然后是任意个数的数字(至少有一个),接着是另一个连字符号(-),最后还是任意数字(至少有一个)的集合。”,我们用search()来测试这个正则表达式:
尝试用这个正则表达式来匹配数据会失败,这是为什么呢?因为匹配从字符串的起始位置开始进行的,而我们要找的数值字符串在末尾。我们只能再写一个匹配全部字符串的正则表达式。还有一个偷懒的办法,就是用“.+”来表示任意个字符集,后面再接上我们真正感兴趣的数据:
这个方法不错,可是我们只想获得每行末尾数字的字段,而不是整个字符串,所以需要用圆括号将我们感兴趣的那部分数据分成一组:
到底怎么回事呢?我们本应该得到数据“1171590364-6-8”,而不应该是“4-6-8”啊。第一个整型字段的前半部分到哪里去了呢?原因是:正则表达式本身默认是贪心匹配的。也就是说,如果正则表达式模式中使用到通配字,那它在按照从左到右的顺序求值时,会尽量“抓取”满足匹配的最长字符串。在我们上面的例子里,“.+”会从字符串的起始处抓取满足模式的最长字符,其中包括我们想得到的第一个整型字段的中的大部分。“\d+”只需一位数字就可以匹配,所以它匹配了数字“4”,而“.+”则匹配了从字符串起始到这个第一位数字“4”之间的所有字符:“Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::117159036”,如图15-2所示。
图 15-2 为什么匹配错了:“+”是贪心的量词(操作符)
一个解决办法是用“非贪婪”操作符“?”。这个操作符可以用在“*”、“+”或“?”的后面。它的作用是要求正则表达式引擎匹配的字符越少越好。因此,如果我们把“?”放在“.+”的后面,我们就得到了想要的结果,见图15-3。
图 15-3 解决“贪婪”匹配问题:“?”要求非“贪婪”匹配
另一种办法,更简单,注意运用“::”做字段分隔符号。你可以用一般字符串的strip(‘::’)方法,得到全部字符,然后用strip(‘-’)得到你要找的三个整型字段。我们现在不采用这种方法,因为我们的脚本gendata.py正是通过这种方法把字符组合到一起的。
最后一个例子:假设我们只想抽取三个整型字段里中间的那个整型部分。我们是这么做的(用搜索,这样就不必匹配整个字符了): “-(\d+)-”。用这个模式“-(\d+)-”,我们得到:
在本章中,有很多正则表达式的强大功能我们未能涉及,由于篇幅所限,我们无法详细介绍它们。但我们希望所提供的信息和技巧对你的编程实践有所帮助。我们建议你参阅有关文档以获得更多在Python语言中使用正则表达式的知识。要精通正则表达式,我们建议你阅读Jeffrey E.F.Friedl所编写的《精通正则表达式》(Mastering Regular Expressions)一书。