本节书摘来自异步社区《正则表达式经典实例(第2版)》一书中的第2章,第2.10节,作者: 【美】Jan Goyvaerts , Steven Levithan著,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.10 再次匹配先前匹配的文本
问题描述
创建一个正则表达式来匹配按照yyyy-mm-dd格式的“神奇”日期。神奇日期指的是年份后2位与月份和该月的日期都是相同的数字。例如,2008-08-08就是一个神奇日期。你可以假设目标文本中的所有日期都是有效的。这个正则表达式并不需要考虑去掉像9999-99-99这样的日期,因为它们不会出现在目标文本中。你只需要找到神奇的日期即可。
解决方案
\b\d\d(\d\d)-\1-\1\b
正则选项:无
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
讨论
为了在正则表达式中匹配先前匹配到的文本,我们首先必须记录上次匹配的文本。这可以使用捕获分组来实现,实例2.9中已经讲解过。在此之后,我们可以使用反向引用(backreference)来在该正则表达式中的任何地方匹配相同的文本。你可以使用一个反斜杠之后跟一个单个数字(1~9)来引用前9个捕获分组。而第10~99组,则要使用‹10›~‹99›。
不要使用01。它或者是一个八进制的转义,或者会产生一个错误。在本书中我们不会用到八进制转义,因为xFF这样的十六进制转义更加容易理解。
当正则表达式‹bdd(dd)-1-1b›遇到2008-08-08的时候,开始的‹dd›会匹配20。接着正则引擎会进入捕获分组,并记录目标文本中所在的位置。
在捕获分组中的‹dd›会匹配08,然后引擎会到达分组的右括号。在这个点上,匹配的部分08会被保存到1号捕获分组中。
下一个记号是连字符,它会按照字面进行匹配。接着就遇到了反向引用。正则引擎会检查第一个捕获分组的内容:08。然后引擎会试图按照字面来匹配这个文本。如果该正则表达式是不区分大小写的,那么捕获分组也会按照这种方式进行匹配。在这里,反向引用匹配成功。下一个连字符和反向引用也会匹配成功。最终,单词分界符会匹配目标文本的结尾,这样就找到了一个完整匹配:2008-08-08。现在捕获分组中依然保存的是08。
如果一个捕获分组被重复,这可以通过量词(实例2.12)或者是回溯(实例2.13)来实现,每次捕获分组匹配成功,都会覆盖之前保存的捕获分组匹配的内容。对该分组的反向引用只会匹配该分组最后一次捕获到的文本。
如果同一个正则表达式遇到2008-05-24 2007-07-07的时候,当‹bdd(dd)›匹配到2008的时候,该分组第一次捕获到的内容08,会被保存到第一个(也是唯一一个)捕获分组中。接下来,连字符会匹配它自身。反向引用在试图用05来匹配‹08›的时候,匹配失败。
由于在该正则表达式中不存在其他的选择分支,引擎会放弃匹配尝试。这包括清除所有的捕获分组。当引擎再次尝试的时候,从目标文本中的第一个0开始,‹1›不再存有任何文本内容。
接下来继续处理2008-05-24 2007-07-07,该分组下一次会捕获到内容是当‹bdd(dd)›匹配到2007的时候,它会把07保存起来。接下来,连字符匹配自身。现在反向引用会试图匹配‹07›。这次匹配是成功的,接着下一个连字符、反向引用以及单词边界都匹配成功。结果是找到了2007-07-07。
因为正则引擎是从前向后处理的,因此应当把捕获括号放到反向引用的前面。正则表达式‹bdd1-(dd)-1›和‹bdd1-1-(dd)b›永远不可能匹配到任何东西。因为这里的反向引用是在捕获分组之前出现的,而它还没有捕获到任何东西。除非你使用的是JavaScript,否则反向引用指向还没有进行匹配尝试的分组时,它总是会匹配失败。
还没有参与匹配的分组,并不等同于捕获到长度为0的分组。对一个长度为0的捕获分组的反向引用总是会匹配成功。当‹(^)1›匹配字符串的开始的时候,第一个捕获分组会捕获到^号的长度为0的匹配,从而‹1›会匹配成功。在实践中,这会发生在当所有捕获分组的内容都是可选的情况下。
JavaScript是我们所知道的唯一与正则表达式中几十年反向引用的传统相违背的流派。在JavaScript中,或者至少在遵循JavaScript标准的实现中,对一个还没有参与匹配的分组的反向引用总是会匹配成功,这同捕获了长度为0的匹配的分组的反向引用是一样的。因此,在JavaScript中,‹bdd1-1-(dd)b›会成功匹配12--34。