作者: 开心石头
在上一篇文章里,我们介绍了正则表达式中断言相关的一些概念,在本文里,我们会介绍正则表达式中递归的运用与利用正则表达式修改目标字符串。
正则表达式中的递归
接触过程序的朋友可能都遇到过成对的各种括号吧,这些括号常常相互嵌套,而且嵌套的层次数目无法确定。试想一下如果想提取一段程序里用括号括起的一段代码,这里面很可能包含了层次数目不定的其它括号对,用正则表达式该如何完成?
在Perl 5.6之前这的确有点困难,不过从Perl 5.6之后,引入了递归正则表达式,这个问题得到了解决。通常在正则表达式里用“(?R)”表示一个对自己的引用,下面让我们看看用什么正则表达式来解决刚才提出的问题。
/\( ( (?>[^()]+) | (?R) )* \)/x |
现在让我们来分析这个模式的含义,这里使用了“x”模式修正符,以便可以在模式中加入空格以方便阅读。
模式的开头是匹配第一个左圆括号,然后我们需要捕获的子模式,注意,字模式后面跟了量词“*”,表示此模式可以重复0到多次。最后是一个结束圆括号。现在我们分析子模式( (?>[^()]+) | (?R) )的内容。这是一个分支子模式,表示模式可以有两种情况,第一种是(?>[^()]+),这是一个一次性子模式,代表一个以上的非括号字符,另一种情况是| (?R),也即对正则表达式自己的递归调用——\( ( (?>[^()]+) | (?R) )* \),又寻找一个左圆括号,开始查找一对嵌套的圆括号包含的内容。
分析到这里,这个正则表达式的含义已经基本清楚,但你注意到没有,这里为什么要使用一次性子模式(?>[^()]+)来查找非括号字符串?
事实上,由于递归的层次是无限的,这种处理非常必要,特别是遇到不匹配的字符串时,它不会让你陷入长时间的等待。考虑一下下面这个目标字符串,
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa()
在得出不匹配的最终结果前,如果不使用一次性子模式,解析器将尝试每一种可能的方法来分割目标字符串,这将浪费大量的时间。
用正则表达式修改目标
并非所有的正则表达式工具都允许你修改目标字符串,它们中的一些仅仅使用正则表达式来查找匹配指定模式的字符串,在Linux中,最为广泛使用的支持正则表达式的工具就是grep命令,这是一个专门用来查找的工具,再就是一些文本编辑器工具,它们有的允许使用正则表达式替换,有的则不允许,这需要查看你使用的工具的在线手册。
对于那些允许你使用正则表达式来修改目标字符串的工具中,它们之间的一些不同你必然放在心上:
这些不同首先表现在替换的具体形式上,有的是以对话框的形式分别让你输入需要查找的模式和被替换的内容,有些则使用命令使界面通过定界符来分隔匹配的模式与需要替换的内容,对于一些编程语言工具,它们通常通过函数的不同参数来分别定义需要匹配的模式与替换的内容。
另一个需要注意的不同是这些工具具体修改的对象。大多数基于Linux的命令行工具一般是通过标准输出或者管道来修改缓存的内容而非直接修改磁盘上存储的文件,而文本编辑器工具或编程语言通常会直接修改目标文件。
我们下面用Linux下sed命令的格式来举几个正则表达式的例子:
模式:s/cat/dog/g
输入:wild dogs, bobcats, lions, and other wild cats
输出:wild dogs, bobdogs, lions, and other wild dogs
模式:s/[a-z]+i[a-z]*/nice/g
输入:wild dogs, bobcats, lions, and other wild cats
输出: nice dogs, bobcats, nice, and other nice cats
当我们使用模式进行替换操作时,目标字符串中所有匹配模式的字符串都将被替换。
下面再举一个使用逆向引用进行替换的例子:
模式:s/([A-Z])([0-9]{2,4}) /\2:\1 /g
输入: A37 B4 C107 D54112 E1103 XXX
输出: 37:A B4 107:C D54112 1103:E XXX
前面已经介绍过默认情况下的匹配一般是greedy的,这常会使实际匹配的部分大于你希望匹配的内容,特别是在进行替换操作时这将更加危险,因为如果你在错误匹配的情况下执行了一次替换操作,实际上你是删除了目标中的有效内容,特别是当这种操作面向文件时造成的危害就更大了。因此,牢记一个不严格的字符类加上一个不严格的量词足以造成不可挽回的后果,执行类似操作前一定要多测试一下不同的目标字符串,尽可能避免这种情况的发生。
在本教程的下一篇文章里,我们会介绍一款可以方便进行正则表达式学习的工具和一些正则表达式编写的思路。