Archive for October, 2009
环视(一)
Regex Guru 天牛神作,尝试翻成中文,不过总觉得翻成中文后变得很水。
原文地址: http://www.regular-expressions.info/lookaround.html
顺序环视和逆序环视
Perl 5 引入了两个强大的正则结构:顺序环视和逆序环视,二者合称为环视,也叫做零长度断言。环视和行起始(结束)锚、词起始(结束)锚一样是零长度匹配 (zero-width matches)的。但与锚匹配位置不同,环视匹配的是字符,并且在匹配完成之后,匹配到的字符就被丢弃,而只返回匹配是否成功,这就是为何被称之为断言。因为匹配的字符会被丢弃,因此环视不会消费(consume)字符,而仅仅是判断是否发生了一次成功匹配。借助环视可以创建几乎不可能的、简洁的正则表达式。
肯定顺序环视和否定顺序环视
当需要匹配某字符接下来不是某字符的情况时就需要用到否定顺序环视。像“字符 q 后面跟一个除 u 以外的字符”这样的需求是办法用否定字符组(Negated Character Classes)来实现的,但否定顺序环视 q(?!u) 则可以实现这个需求。否定顺序环视结构由一对圆括号标识,开括号后面跟一个问号和感叹号,再后面就是表达式字符 u。
肯定顺序环视与否定顺序环视原理相同,q(?=u) 匹配字符 q 后面紧跟字符 u 的情况,不过 u 并不作为匹配结果的一部分。肯定顺序环视结果同样由一对圆括号标识,开括号后面跟一个问号,等于号则代替感叹号的位置,表明该顺序环视是“肯定”的。
在顺序环视中可以包括的任意正则表达式(逆序环视则并非如此),如果其中包括分组捕获,那么将会产生与分组捕获对应的反向引用,而环视本身并不会创建任何的反向引用,因而也不会导致反向引用计数地增加。在环视中创建分组捕获也是用圆括号把表达式括起来,像这样:(?=u(regex)),如果错误地将嵌套关系书写为 ((?=regex)) 以期望捕获整个环视匹配的结果是行不通的,因为当引擎试图存储匹配结果到反向引用时,内层的环视已经将匹配结果丢弃,因为反向引用到的值始终都只是空字符。
引擎细节
首先看引擎是如何将表达式 q(?!u) 应用到目标文本 Iraq 的。第 1 个表达式字符是字面量 q,如你所知,这将使引擎发现目标文本中最靠左的 q 字符,q 是目标文本 Iraq 中最后一个字符,当前表达式字符在前推以后就变成空字符。接下来引擎注意到表达式中的顺序环视结构,就直接让环视中的 u 参与匹配,u 与空字符匹配失败,引擎记录下环视中的表达式匹配失败后发现这是个否定顺序环视,因而意味着环视中的表达式匹配失败,环视本身就匹配成功。表达式 q 和顺序环视均匹配成功,此时整个表达式也就匹配成功并返回匹配到的目标字符 q。
接着看看同样的表达式应用到目标文本 quit 的匹配过程。q 匹配 q,然后是环视中表达式 u 和目标文本中的 u 进行比较,匹配成功,环视中的表达式结束,接着引擎前进到目标文本中的字符 i,并记下环视中的表达式匹配成功然后丢弃环匹配结果,于是引擎又从目标文本中的 i 回溯到 u。
因为这是个否定顺序环视,环视中的表达式匹配成功结果导致环视本身匹配失败,而整个表达式的结束导致引擎不得不重新开始新一轮匹配,q 和 u 比较,匹配失败,q 和 i 比较,匹配失败,q 和 t 比较,还是匹配失败,目标文本结束。最终引擎报告整个表达式匹配失败。
再来一个稍复杂的例子,以确保你真的理解了顺序环视的含义。将 q(?=u)i 应用到 quit 的过程中,q 匹配 q,u 匹配 u,之后环视匹配成功,其中匹配到的 u 被再次丢弃,而引擎则从目标文本中的 i 回溯到 u,匹配继续,表达式中的 i 和目标文本中的 u 匹配失败,目标文本结束,于是引擎又从目标文本中的 u 开始新一轮匹配,最后整个表达式还是匹配失败,因为目标文本中已经没有字符 q 能和表达式中的 q 成功匹配了。
PCRE/Python 下的 re 细节(4) — 条件分支(conditional)
所谓条件分支,就是“当 x 成功捕获的话,则执行多选分支 y,否则执行多选分支 z”,也就是说这个东西在表达式中实现了 if/then/else 的功能。条件分支的语法比较类似于多选分支,只是更复杂,像这样:…(?(1)then|else)…
(?(1)… 就表示开始一个条件分支的匹配,数字 1 的意思是测试整个表达式中第 1 个反向引用是否有参与匹配,如果有则匹配表达式 then,否则匹配表达式 else。看 recipe 2.17 中表达式 (a)?b(?(1)c|d) 的匹配过程。(a)?b(?(1)c|d) 如期望的那样可以匹配目标文本 abc、bd、abd,但匹配不到 bc。
匹配目标文本 abc 时,(a)? 匹配成功,\1 捕获到字符 a,然后前推当前表达式字符到 b,再次匹配成功,继续前推当前表达式字符。这时引擎看到 (?(1) 就识别出这是一个条件分支匹配,于是测试反向引用 1 有没有参与匹配,发现有,于是执行 | 前的部分,也就是匹配字符 c,依然匹配成功,于是整个表达式宣告匹配成功。
匹配目标文本 bd 的过程:(a)? 匹配失败,\1 没有捕获到任何内容,然后匹配过程与匹配 abc 时类似,直到当引擎在条件分支中发现反向引用 1 没有参与匹配,会将 | 后的部分 d 拿来进行匹配,还是匹配成功,于是整个表达式匹配成功。匹配 bc 时则是当引擎发现反向引用 1 没有参与匹配时,就将 | 后的 d 拿来匹配时,结果匹配失败,于是整个表达式也相应匹配失败。
目标文本 abd 的匹配过程是比较有趣的,首先 (a)? 匹配成功,\1 捕获到字符 a,前推当前表达式和当前目标文本,b 和 b 匹配成功,继续前推表达式和目标文本,因为 \1 有内容,所以当前表达式字符为 c,c 和 d 匹配失败,但并不意味着整个表达式也匹配失败了,不要忘了 (a)? 中的 ? 是可以回溯的,所以当前表达式字符和当前目标文本字符均发生回溯,接下来的匹配过程就跟匹配 bd 的过程完全一致了,最终整个表达式还是匹配成功。而如果期望 abd 匹配失败的话,只要在 (a)? 前增加一个 ^ 锚就好了。
有个值得注意的地方是表达式 (a)?b(?(1)c|d) 中 (a)? 的 ? 是在括号外面的,如果写成 (a?)b(?(1)c|d) 的话,即使 (a?) 匹配失败,但 (?(1) 还是会返回 true,因为 (a?) 始终都被认为是有参与匹配的,所以 (?(1)c|d) 总是 | 前的 c 参与后续的匹配过程。