关于从普通文本提取正则表达式的再思考
Mar 9th
rex按:写完上一篇文章之后,一直在考虑如何真正实现从普通文本中归纳正则表达式的实现。走了许多弯路,也学了不少知识。例如,perl黑豹书上复杂的数据结构、匿名散列和数组、refenrence;紫龙书上的状态机的构造,数据结构上图论的知识,都是很有用的。另外还新学了graphviz的用法。以前觉得很神秘,不过一用才发现很直观。本文的插图是使用online版本的graphviz画的。
除了本文的这种实现方法(基于图),我还使用另一种方式实现了,很简单:基于关键词。具体作法是,逐一读取每一行文本,使用\s+等将其split开,形成array;然后再对所有的array进行求交集的操作(使用hash),得到每一行都有的关键词;然后按从左到右的顺序建立这覆的正则式^(.*?)keyword1(.*?)keyword2….keywordN(.*?)$,再分别匹配每一行文本,得到hash的hash表,或者array的array,转置,并列输出,得到^(option1|option2…)keyword1(option..)…$这样的正则式。最后作为验证,再将所最终生成的正则与每一行匹配测试一下。
这样以词为单位做完之后,再逐个字母地分隔开来,递归地处理(option1|option2…)的部分。先是单词级,再是字母级,有利于先在最大程度上找出重复的内容;而且粗化和细化的处理过程,思路是一致的,粒度不同罢了。
新手请自重,高手请赐教,我的思路未必是正确或最优的。
问题
this is a red foxthis is a blue firefoxthis is a piga red fox
标准正则
有两种极端的解法是不可取的:
- ^.*$
- ^(this is a red fox|this is a blue firefox|this is a pig|a red fox)$
第一种失之于太宽泛,第二种失之于太狭隘。太宽泛则泥沙俱下,无论什么文本都能匹配;太狭隘则僵化死板,缺乏灵活性。好的正则表达式源于例文本(从例文本中提取规律),又高于例文本(能匹配同规律的其它文本)。匹配什么,排除什么,都有定则,所谓“君子有所为而有所不为”,指的就是这种情况(貌似跑题了:))。
思路
既然是一条正则匹配所有的文本,那么这条正则(记为$re)也应该匹配第一行文本。
第一行文本为this is a red fox。那么,从^this is a red fox$应该是$re的一个(真)子集。它的路径为:“^”->this->is->a->red->fox->”$”。全部节点之间,是串联关系,从左到右依次排列即可。
示意图如下(可以点击看全尺寸图,下同):
同理,第二行文本也应该是$re的子集。不过,由于已经存在了由^->this->is->a的路径,到a时出现支路,a->blue->firefox->$;
将此路径添加到示意图上,得到:
显而易见,这两条并列的支路,始于a,终于$,可以使用|来并列之。
好了,我们总结一下规律:
再往下,this is a pig,同理,只需要在原图基础上添加a->pig->$的支路即可。此时图示如下:
此时,观察可知,一种新的情况出现了。同时存在^->a,和“^”->this->is->a两条路径。想一下初中物理电路图,我们可以将这种情况称为“短路”,即,“^”->this->is->a这个线路的^、a两个节点之间,添加了一条无障碍通道,它能无视this、is的存在,因此,让this->is这条路径成为可选项。再总结一下规律:
如果有A->B->…C->D的路径,且有A->D的路径,则称A->D之间存在短路,此时,B->…->C可以用(B->…->C)?来表示(就是用括号来表示被短路的部分,问号表示短路之)。
这也就是为什么上文要强调是一个节点的缘故。
实现
…
上图其实是一个有向图,只需记录所有的顶点集合,路径集合,再来求各路径之间的关系;最后打印输出,即是所求。
这两个集合在读取文本文件行的时候可以一次性建立。不复杂。关键是关系的确立。
- 从一个顶点A出发的N条支路必定汇合(只是有时是同一个点,有时不在同一点而已。本文给出的例子是最简单的情况,这里可以假设为汇合到同一点)于M点。
- 这N条路中,每一条路径的长短以经过的节点个数来计算。例如上图中,^到a有一条路,上面的路径为2,下面的路径为0。
- 短的支路决定了这N条支路的关系。
- 长度为任意两点之间,最多只可能有一条长度为0的边。
- 如果存在长度为0的边,则其余的同级的支路被短路。
- 长度不为0的N-1条支路之间是并列关系。
- 整个图始于^,终于$。
[译]递归正则表达式
Dec 16th
原文在此。rex译于2009年12月15~17日,翻译过程中使用的是google docs@prism@firefox@ubuntu 9.10,很爽的体验。感谢余晟老师在正则和翻译方面的悉心指导。
平时我们用到的正则表达式,其实没那么“规则”。多数编程语言所支持的扩展的正则表达式,其运算能力比起形式语言理论所定义的“规则”正则表达式要强得多。
rex注:在这里其实可以看到,如果将正则表达式译为正规表达式,一切就都通顺了:
平时我们用到的正规表达式,其实没那么“正规”。多数编程语言所支持的扩展的正规表达式,其运算能力比起正式语言理论所定义的“正规”正规表达式要强得多。regular expression的日文是“正规表现”,在鸟哥书中,好像也将其称为正规表达式。via
例如,经常用到的捕获缓存,就是用来帮助我们临时存储任意正则表达式模式,以便重复使用。 又如,“环视断言”能让正则表达式引擎在做决定之前先偷偷看看环视一下。这些扩展让正则表达式非常强大,足以描述一些“上下文无关语法”。
Perl语言的正则表达式引擎特性异常丰富,其特征之一是懒惰正则子表达式(Lazy regular subexpressions),格式为(??{code}),其中的“code”可以是任意一段perl程序,该子表达式可能匹配时,这段程序就会执行。
我们可以利用这一特征来编写出非常有趣的东西,即将正则表达式自身嵌在它的“code”部分,由此生成递归的正则表达式(a recursive regular expression)!
一直以来,正则表达式无法匹配0n1n这种表达式,也就是由若干个0以及同等数量的1所组成的字符串。如果使用懒惰正则子表达式,这一经典问题就迎刃而解。
下面是匹配0n1n字串的perl正则表达式代码。
$regex = qr/0(??{$regex})?1/;
rex注:紫龙书第四章有云:“文法是比正则表达式表达能力更强的表示方法。每个可以使用正则表达式描述的构造,都可以使用文法来描述,但是反之不成立。换句话说,每个正则语言都是一个上下文无关语言,但是反之不成立。”书中交待的正则,也应该是指的常规正则表达式,而非现代语言中的扩展的正则表达式。一般使用正则表达式来构造小部件,而使用文法来组建语言框架。
此正则表达式匹配一个字符0,之后是正则表达式自身0或1次,之后是字符1。如果正则表达式自身部分不能匹配,那么它只能匹配01;如果自身部分可以匹配,则正则表达式匹配的是00($regex)?11,此时若不能匹配自身则结果是0011,若可以匹配就是000($regex)?111,……依次顺延。
下面是匹配050000150000的Perl程序:
1 2 3 4 5 6 7 8 9 10 11 | #!/usr/bin/perl $str = "0"x50000 . "1"x50000; $regex = qr/0(??{$regex})*1/; if ($str =~ /^$regex$/) { print "yes, it matches"; } else { print "no, it doesn't match"; } |
现在来看题图所示的Yo Dawg正则表达式。你先猜猜它的作用?正确答案是,它匹配(foo(bar())baz)这样完全嵌套的括号表达式(fully parenthesized expression)或((()()())())这样的平衡括号表达式(balanced parentheses)。
1 2 3 4 5 6 7 8 9 | $regex = qr/ \( # (1) match an open paren ( #(1),此处匹配开括号 ( # followed by #(2),紧接着是 [^()]+ # (3) one or more non-paren character #(3),1个或多个非括号字符 | # OR #(4),或 (??{$regex}) # (5) the regex itself #(5),正则式自身 )* # (6) repeated zero or more times #(6),重复0或多次 \) # (7) followed by a close paren ) #(7),紧接着是闭括号 /x; |
rex注:关于Yo Dawg图片的含义,可以参考这里。基本上是全是“Yo dawg, I herd you like X, so we put a Y in your Y so you can Z while you Z”的结构的配图文字。
构造此正则表达式的思路是这样的。对于完全嵌套的括号表达式,它的开始字符是一个开括号。这是最简单的一步,我们直接写出(上面程序中的(1))。同理,它的结束字符是闭括号,于是得到(7)。现在该动脑筋了,括号中间是什么呢?对,它可以是既不是开括号又不是闭括号的任意字符(第(3)点),也可以是另一个完全嵌套的表达式(即第(5)点)。所有的这些,既可以只匹配0次(第(3)点),以便构造最小的完全嵌套括号表达式(),也可以匹配多次来匹配较复杂的表达式。
去掉 /x 选项(即,不再使用多行风格的注释模式),可以简记为:
$regex = qr/\(([^()]+|(??{$regex}))*\)/;
但是切勿在正式产品中使用这一特性,它太诡异,不好把握。建议使用较稳定成熟的Text::Balanced 或 Regexp::Common 模块。
rex注:对于(??{code}),perl官方的提示是:此正则表达式仅作测试使用,可能有更新而不作提示。代码执行时产生的副作用,因版本而异,运行结果或有不同,取决于正则引擎的后期优化。
最后提醒大家,在Perl 5.10中已经可以使用递归捕获缓存来替代懒惰代码子表达式了,运行结果相同。
下面是匹配0n1n的递归捕获缓存语法(?N)的实现:
my $rx = qr/(0(?1)*1)/;
(?1)*的含义是“匹配第一组0或多次”,这里的第一组是指整个正则表达式。
请自行动手,重写平衡括号的正则表达式,当作练习。
祝玩得开心!




Comments