正则相关

来源:互联网 发布:淘宝专业代购 编辑:程序博客网 时间:2024/06/05 19:19

贪婪与非贪婪

1.概述

贪婪与非贪婪模式影响的是被量词修饰的子表达式的匹配行为,贪婪模式在整个表达式匹配成功的前提下,尽可能多的匹配,而非贪婪模式在整个表达式匹配成功的前提下,尽可能少的匹配。非贪婪模式只被部分NFA引擎所支持。

属于贪婪模式的量词,也叫做匹配优先量词,包括:

“{m,n}”、“{m,}”、“?”、“*”和“+”。

在一些使用NFA引擎的语言中,在匹配优先量词后加上“?”,即变成属于非贪婪模式的量词,也叫做忽略优先量词,包括:

“{m,n}?”、“{m,}?”、“??”、“*?”和“+?”。

从正则语法的角度来讲,被匹配优先量词修饰的子表达式使用的就是贪婪模式,如“(Expression)+”;被忽略优先量词修饰的子表达式使用的就是非贪婪模式,如“(Expression)+?”。

对于贪婪模式,各种文档的叫法基本一致,但是对于非贪婪模式,有的叫懒惰模式或惰性模式,有的叫勉强模式,其实叫什么无所谓,只要掌握原理和用法,能够运用自如也就是了。个人习惯使用贪婪与非贪婪的叫法,所以文中都会使用这种叫法进行介绍。

2.贪婪与非贪婪模式匹配原理

对于贪婪与非贪婪模式,可以从应用和原理两个角度进行理解,但如果想真正掌握,还是要从匹配原理来理解的。

先从应用的角度,回答一下“什么是贪婪与非贪婪模式?”

2.1     从应用角度分析贪婪与非贪婪模式

2.1.1  什么是贪婪与非贪婪模式

先看一个例子

举例:

源字符串:aa<div>test1</div>bb<div>test2</div>cc

正则表达式一:<div>.*</div>

匹配结果一:<div>test1</div>bb<div>test2</div>

正则表达式二:<div>.*?</div>

匹配结果二:<div>test1</div>(这里指的是一次匹配结果,所以没包括<div>test2</div>)

根据上面的例子,从匹配行为上分析一下,什是贪婪与非贪婪模式。

正则表达式一采用的是贪婪模式,在匹配到第一个“</div>”时已经可以使整个表达式匹配成功,但是由于采用的是贪婪模式,所以仍然要向右尝试匹配,查看是否还有更长的可以成功匹配的子串,匹配到第二个“</div>”后,向右再没有可以成功匹配的子串,匹配结束,匹配结果为“<div>test1</div>bb<div>test2</div>”。当然,实际的匹配过程并不是这样的,后面的匹配原理会详细介绍。

仅从应用角度分析,可以这样认为,贪婪模式,就是在整个表达式匹配成功的前提下,尽可能多的匹配,也就是所谓的“贪婪”,通俗点讲,就是看到想要的,有多少就捡多少,除非再也没有想要的了。

正则表达式二采用的是非贪婪模式,在匹配到第一个“</div>”时使整个表达式匹配成功,由于采用的是非贪婪模式,所以结束匹配,不再向右尝试,匹配结果为“<div>test1</div>”。

仅从应用角度分析,可以这样认为,非贪婪模式,就是在整个表达式匹配成功的前提下,尽可能少的匹配,也就是所谓的“非贪婪”,通俗点讲,就是找到一个想要的捡起来就行了,至于还有没有没捡的就不管了。

2.1.2  关于前提条件的说明

在上面从应用角度分析贪婪与非贪婪模式时,一直提到的一个前提条件就是“整个表达式匹配成功”,为什么要强调这个前提,我们看下下面的例子。

正则表达式三:<div>.*</div>bb

匹配结果三:<div>test1</div>bb

修饰“.”的仍然是匹配优先量词“*”,所以这里还是贪婪模式,前面的“<div>.*</div>”仍然可以匹配到“<div>test1</div>bb<div>test2</div>”,但是由于后面的“bb”无法匹配成功,这时“<div>.*</div>”必须让出已匹配的“bb<div>test2</div>”,以使整个表达式匹配成功。这时整个表达式匹配的结果为“<div>test1</div>bb”,“<div>.*</div>”匹配的内容为“<div>test1</div>”。可以看到,在“整个表达式匹配成功”的前提下,贪婪模式才真正的影响着子表达式的匹配行为,如果整个表达式匹配失败,贪婪模式只会影响匹配过程,对匹配结果的影响无从谈起。

非贪婪模式也存在同样的问题,来看下面的例子。

正则表达式四:<div>.*?</div>cc

匹配结果四:<div>test1</div>bb<div>test2</div>cc

这里采用的是非贪婪模式,前面的“<div>.*?</div>”仍然是匹配到“<div>test1</div>”为止,此时后面的“cc”无法匹配成功,要求“<div>.*?</div>”必须继续向右尝试匹配,直到匹配内容为“<div>test1</div>bb<div>test2</div>”时,后面的“cc”才能匹配成功,整个表达式匹配成功,匹配的内容为“<div>test1</div>bb<div>test2</div>cc”,其中“<div>.*?</div>”匹配的内容为“<div>test1</div>bb<div>test2</div>”。可以看到,在“整个表达式匹配成功”的前提下,非贪婪模式才真正的影响着子表达式的匹配行为,如果整个表达式匹配失败,非贪婪模式无法影响子表达式的匹配行为。

2.1.3  贪婪还是非贪婪——应用的抉择

通过应用角度的分析,已基本了解了贪婪与非贪婪模式的特性,那么在实际应用中,究竟是选择贪婪模式,还是非贪婪模式呢,这要根据需求来确定。

对于一些简单的需求,比如源字符为“aa<div>test1</div>bb”,那么取得div标签,使用贪婪与非贪婪模式都可以取得想要的结果,使用哪一种或许关系不大。

但是就2.1.1中的例子来说,实际应用中,一般一次只需要取得一个配对出现的div标签,也就是非贪婪模式匹配到的内容,贪婪模式所匹配到的内容通常并不是我们所需要的。

那为什么还要有贪婪模式的存在呢,从应用角度很难给出满意的解答了,这就需要从匹配原理的角度去分析贪婪与非贪婪模式。

2.2     从匹配原理角度分析贪婪与非贪婪模式

如果想真正了解什么是贪婪模式,什么是非贪婪模式,分别在什么情况下使用,各自的效率如何,那就不能仅仅从应用角度分析,而要充分了解贪婪与非贪婪模式的匹配原理。

2.2.1  从基本匹配原理谈起

NFA引擎基本匹配原理参考:正则基础之——NFA引擎匹配原理。

这里主要针对贪婪与非贪婪模式涉及到的匹配原理进行介绍。先看一下贪婪模式简单的匹配过程。

源字符串:"Regex"

正则表达式:".*"

 2-1

 

图2-1

注:为了能够看清晰匹配过程,上面的空隙留得较大,实际源字符串为“”Regex””,下同。

来看一下匹配过程。首先由第一个“"”取得控制权,匹配位置0位的“"”,匹配成功,控制权交给“.*”。

“.*”取得控制权后,由于“*”是匹配优先量词,在可匹配可不匹配的情况下,优先尝试匹配。从位置1处的“R”开始尝试匹配,匹配成功,继续向右匹配,匹配位置2处的“e”,匹配成功,继续向右匹配,直到匹配到结尾的“””,匹配成功,由于此时已匹配到字符串的结尾,所以“.*”结束匹配,将控制权交给正则表达式最后的“"”。

“"”取得控制权后,由于已经在字符串结束位置,匹配失败,向前查找可供回溯的状态,控制权交给“.*”,由“.*”让出一个字符,也就是字符串结尾处的“””,再把控制权交给正则表达式最后的“"”,由“"”匹配字符串结尾处的“"”,匹配成功。

此时整个正则表达式匹配成功,其中“.*”匹配的内容为“Regex”,匹配过程中进行了一次回溯。

接下来看一下非贪婪模式简单的匹配过程。

源字符串:"Regex"

正则表达式:".*?"

 

 

图2-2

看一下非贪婪模式的匹配过程。首先由第一个“"”取得控制权,匹配位置0位的“"”,匹配成功,控制权交给“.*?”。

“.*?”取得控制权后,由于“*?”是忽略优先量词,在可匹配可不匹配的情况下,优先尝试不匹配,由于“*”等价于“{0,}”,所以在忽略优先的情况下,可以不匹配任何内容。从位置1处尝试忽略匹配,也就是不匹配任何内容,将控制权交给正则表达式最后的“””。

“"”取得控制权后,从位置1处尝试匹配,由“"”匹配位置1处的“R”,匹配失败,向前查找可供回溯的状态,控制权交给“.*?”,由“.*?”吃进一个字符,匹配位置1处的“R”,再把控制权交给正则表达式最后的“"”。

“"”取得控制权后,从位置2处尝试匹配,由“"”匹配位置1处的“e”,匹配失败,向前查找可供回溯的状态,重复以上过程,直到由“.*?”匹配到“x”为止,再把控制权交给正则表达式最后的“"”。

“"”取得控制权后,从位置6处尝试匹配,由“"”匹配字符串最后的“"”,匹配成功。

此时整个正则表达式匹配成功,其中“.*?”匹配的内容为“Regex”,匹配过程中进行了五次回溯。

2.2.2  贪婪还是非贪婪——匹配效率的抉择

通过匹配原理的分析,可以看到,在匹配成功的情况下,贪婪模式进行了更少的回溯,而回溯的过程,需要进行控制权的交接,让出已匹配内容或匹配未匹配内容,并重新尝试匹配,在很大程度上降低匹配效率,所以贪婪模式与非贪婪模式相比,存在匹配效率上的优势。

但2.2.1中的例子,仅仅是一个简单的应用,读者看到这里时,是否会存在这样的疑问,贪婪模式就一定比非贪婪模式匹配效率高吗?答案是否定的。

举例:

需求:取得两个“"”中的子串,其中不能再包含“"”。

正则表达式一:".*"

正则表达式二:".*?"

情况一:当贪婪模式匹配到更多不需要的内容时,可能存在比非贪婪模式更多的回溯。比如源字符串为“The word"Regex" means regular expression.”。

情况二:贪婪模式无法满足需求。比如源字符串为“The phrase "regular expression" is called "Regex" for short.”。

对于情况一,正则表达式一采用的贪婪模式,“.*”会一直匹配到字符串结束位置,控制权交给最后的“””,匹配不成功后,再进行回溯,由于多匹配的内容“means regular expression.”远远超过需匹配内容本身,所以采用正则表达式一时,匹配效率会比使用正则表达式二的非贪婪模式低。

对于情况二,正则表达式一匹配到的是“"regular expression" is called "Regex"”,连需求都不满足,自然也谈不上什么匹配效率的高低了。

以上两种情况是普遍存在的,那么是不是为了满足需求,又兼顾效率,就只能使用非贪婪模式了呢?当然不是,根据实际情况,变更匹配优先量词修饰的子表达式,不但可以满足需求,还可以提高匹配效率。

源字符串:"Regex"

给出正则表达式三:"[^"]*"

看一下正则表达式三的匹配过程。

 2-3

 

图2-3

首先由第一个“"”取得控制权,匹配位置0位的“"”,匹配成功,控制权交给“[^"]*”。

“[^"]*”取得控制权后,由于“*”是匹配优先量词,在可匹配可不匹配的情况下,优先尝试匹配。从位置1处的“R”开始尝试匹配,匹配成功,继续向右匹配,匹配位置2处的“e”,匹配成功,继续向右匹配,直到匹配到“x”,匹配成功,再匹配结尾的“””时,匹配失败,将控制权交给正则表达式最后的“"”。

“””取得控制权后,匹配字符串结尾处的“””,匹配成功。

此时整个正则表达式匹配成功,其中“[^"]*”匹配的内容为“Regex”,匹配过程中没有进行回溯。

将量词修饰的子表达式由范围较大的“.”,换成了排除型字符组“[^"]”,使用的仍是贪婪模式,很完美的解决了需求和效率问题。当然,由于这一匹配过程没有进行回溯,所以也不需要记录回溯状态,这样就可以使用固化分组,对正则做进一步的优化。

给出正则表达式四:"(?>[^"]*)"

固化分组并不是所有语言都支持的,如.NET支持,而Java就不支持,但是在Java中却可以使用更简单的占有优先量词来代替:"[^"]*+"。

3.贪婪还是非贪婪模式——再谈匹配效率

一般来说,贪婪与非贪婪模式,如果量词修饰的子表达式相同,比如“.*”和“.*?”,它们的应用场景通常是不同的,所以效率上一般不具有可比性。

而对于改变量词修饰的子表达式,以满足需求时,比如把“.*”改为“[^"]*”,由于修饰的子表达式已不同,也不具有直接的可对比性。但是在相同的子表达式,又都可以满足需求的情况下,比如“[^"]*”和“[^"]*?”,贪婪模式的匹配效率通常要高些。

同时还有一个事实就是,非贪婪模式可以实现的,通过优化量词修饰的子表达式的贪婪模式都可以实现,而贪婪模式可以实现的一些优化效果,却未必是非贪婪模式可以实现的。

贪婪模式还有一点优势,就是在匹配失败时,贪婪模式可以更快速的报告失败,从而提升匹配效率。下面将全面考察贪婪与非贪婪模式的匹配效率。

3.1     效率提升——演进过程

在了解了贪婪与非贪婪模式的匹配基本原理之后,我们再来重新看一下正则效率提升的演进过程。

需求:取得两个“"”中的子串,其中不能再包含“"”。

源字符串:The phrase "regular expression" is called "Regex" for short.

正则表达式一:".*"

正则表达式一匹配的内容为“"regular expression" is called "Regex"”,不符合要求。

提出正则表达式二:".*?"

首先“"”取得控制权,由位置0位开始尝试匹配,直到位置11处匹配成功,控制权交给“.*?”,匹配过程同2.2.1中非贪婪模式的匹配过程。“.*?”匹配的内容为“Regex”,匹配过程中进行了四次回溯。

如何消除回溯带来的匹配效率的损失,就是使用更小范围的子表达式,采用贪婪模式,提出正则表达式三:"[^"]*"

首先“"”取得控制权,由位置0位开始尝试匹配,直到位置11处匹配成功,控制权交给“[^"]*”,匹配过程同2.2.2节中非贪婪模式的匹配过程。“[^"]*”匹配的内容为“Regex”,匹配过程中没有进行回溯。

3.2     效率提升——更快的报告失败

以上讨论的是匹配成功的演进过程,而对于一个正则表达式,在匹配失败的情况下,如果能够以最快的速度报告匹配失败,也会提升匹配效率,这或许是我们设计正则过程中最容易忽略的。而在源字符串数据量非常大,或正则表达式比较复杂的情况下,是否能够快速报告匹配失败,将对匹配效率产生直接的影响。

下面将构建匹配失败的正则表达式,对匹配过程进行分析。

以下匹配过程分析中,源字符串统一为:The phrase "regular expression" is called "Regex" for short.

3.2.1  非贪婪模式匹配失败过程分析

 

 3-1

图3-1

构建匹配失败的非贪婪模式的正则表达式:".*?"@

由于最后的“@”的存在,这个正则表达式最后一定是匹配失败的,那么看一下匹配过程。

首先由“"”取得控制权,由位置0处开始尝试匹配,匹配失败,直到图中标示的A处匹配成功,控制权交给“.*?”。

“.*?”取得控制权后,由A后面的位置开始尝试匹配,由于是非贪婪模式,首先忽略匹配,将控制权交给“"”,同时记录一下回溯状态。“"”取得控制权后,由A后面的位置开始尝试匹配,匹配字符“r”失败,查找可供回溯的状态,将控制权交给“.*?”,由“.*?”匹配字符“r”。重复以上过程,直到“.*?”匹配了B处前面的字符“n”,“"”匹配了B处的字符“””,将控制权交给“@”。由“@”匹配接下来的空格“ ”,匹配失败,查找可供回溯的状态,控制权交给“.*?”,由“.*?”匹配空格。继续重复以上匹配过程,直到由“.*?”匹配到字符串结束位置,将控制权交给“"”。由于已经是字符串结束位置,匹配失败,报告整个表达式在位置11处匹配失败,一轮匹配尝试结束。

正则引擎传动装置使正则向前传动,进入下一轮尝试。后续匹配过程与第一轮尝试匹配过程基本类似,可以参考图3-1。

从匹配过程中可以看到,非贪婪模式的匹配失败过程,几乎每一步都伴随着回溯过程,对匹配效率的影响是很大的。

3.2.2  贪婪模式匹配失败过程分析——大范围子表达式

 3-2

 

图3-2

PS:以上分析过程图示参考了《精通正则表达式》一书相关章节图示。

构建匹配失败的贪婪模式的正则表达式:".*"@

其中量词修饰的子表达式为匹配范围较大的“.”,由于最后的“@”的存在,这个正则表达式最后也是一定匹配失败的,看一下匹配过程。

首先由“"”取得控制权,由位置0处开始尝试匹配,匹配失败,直到图中标示的A处匹配成功,控制权交给“.*”。

“.*”取得控制权后,由A后面的位置开始尝试匹配,由于是贪婪模式,优化尝试匹配,一直匹配到字符串的结束位置,将控制权交给“"”。“"”取得控制权后,由于已经是字符串的结束位置,匹配失败,查找可供回溯的状态,将控制权交给“.*”,由“.*”让出已匹配字符“.”。重复以上过程,直到后面“"”匹配了C处后面的字符“””,将控制权交给“@”。由“@”匹配接下来D处的空格“ ”,匹配失败,查找可供回溯的状态,控制权交给“.*”,由“.*”让出已匹配文本。继续重复以上匹配过程,直到由“.*”让出所有已匹配的文本到I处,将控制权交给“"”。“"”匹配失败,由于已经没有可供回溯的状态,报告整个表达式在位置11处匹配失败,一轮匹配尝试结束。

正则引擎传动装置使正则向前传动,进入下一轮尝试。后续匹配过程与第一轮尝试匹配过程基本类似,可以参考图3-2。

从匹配过程中可以看到,大范围子表达式贪婪模式的匹配失败过程,从总体上看,与非贪婪模式没有什么区别,最终进行的回溯次数与非贪婪模式基本一致,对匹配效率的影响仍然很大。

3.2.3  贪婪模式匹配失败过程分析——改进的子表达式

 3-3

 

图3-3

构建匹配失败的贪婪模式的正则表达式:"[^"]*"@

其中量词修饰的子表达式,改为匹配范围较小的排除型字符组“[^"]”,由于最后的“@”的存在,这个正则表达式最后也是一定匹配失败的,看一下匹配过程。

首先由“"”取得控制权,由位置0处开始尝试匹配,匹配失败,直到图中标示的A处匹配成功,控制权交给“[^"]*”。

“[^"]*”取得控制权后,由A后面的位置开始尝试匹配,由于是贪婪模式,优先尝试匹配,一直匹配到B处,将控制权交给“"”。“"”匹配接下来的的字符“"”,匹配成功,将控制权交给“@”。由“@”匹配接下来的空格“ ”,匹配失败,查找可供回溯的状态,控制权交给“[^"]*”,由“[^"]*”让出已匹配文本。继续重复以上匹配过程,直到由“[^"]*”让出所有已匹配的文本到C处,将控制权交给“"”。“"”匹配失败,由于已经没有可供回溯的状态,报告整个表达式在位置11处匹配失败,一轮匹配尝试结束。

正则引擎传动装置使正则向前传动,进入下一轮尝试。后续匹配过程与第一轮尝试匹配过程基本类似,可以参考图3-3。

从匹配过程中可以看到,使用了排除型字符组的贪婪模式的匹配失败过程,从总体上看,大量减少了每轮回溯的次数,可以有效的提升匹配效率。

3.2.4  贪婪模式匹配失败过程分析——固化分组

通过3.2.3节的分析可以知道,由于“[^"]*”使用了排除型字符组,那么图3-3中,在A和B之间被匹配到的字符,就一定不会是字符“"”,所以B到C之间回溯过程就是多余的,也就是说在这之间的可供回溯的状态完全可以不记录。.NET中可以使用固化分组,Java中可以使用占有优先量词来实现这一效果。

 3-4

 

图3-4

首先由“"”取得控制权,由位置0处开始尝试匹配,匹配失败,直到图中标示的A处匹配成功,控制权交给“(?>[^"]*)”。

“(?>[^"]*)”取得控制权后,由A后面的位置开始尝试匹配,由于是贪婪模式,优先尝试匹配,一直匹配到B处,将控制权交给“"”,在这一匹配过程中,不记录任何可供回溯的状态。“"”匹配接下来的字符“””,匹配成功,将控制权交给“@”。由“@”匹配接下来的空格“ ”,匹配失败,查找可供回溯的状态,由于已经没有可供回溯的状态,报告整个表达式在位置11处匹配失败,一轮匹配尝试结束。

正则引擎传动装置使正则向前传动,进入下一轮尝试。后续匹配过程与第一轮尝试匹配过程基本类似,可以参考图3-4。

从匹配过程中可以看到,使用了固化分组的贪婪模式的匹配失败过程,没有涉及到回溯,可以最大限度的提升匹配效率。

3.3     非贪婪模式向贪婪模式的转换

使用匹配范围较大的子表达式时,贪婪模式与非贪婪模式匹配到的内容会有所不同,但是通过优化子表达式,非贪婪模式可以实现的匹配,贪婪模式都可以实现。

比如在实际应用中,匹配img标签的内容。

举例:

需求:取得img标签中的图片地址,src=后固定为“””

源字符串:<img class="test" src="/img/logo.gif" title="测试" />

正则表达式一:<img\b.*?src="(.*?)".*?>

匹配结果中,捕获组1的内容即为图片地址。可以看到,这个例子中使用的都是非贪婪模式,而根据上面章节的分析,后面两个非贪婪模式都可以使用排除型字符组,将非贪婪模式转换为贪婪模式。

正则表达式二:<img\b.*?src="([^"]*)"[^>]*>

注:“src="…"”和标签结束标记符“>”之间的属性中,也可能出现字符“>”,但那是极端情况,这里不予讨论。

后两处非贪婪模式,可以通过排除型字符组转换为贪婪模式,提高匹配效率,而“src=”前的非贪婪模式,由于要排除的是一个字符序列“src=”,而不是单独的某一个或几个字符,所以不能使用排除型字符组。当然也不是没有办法,可以使用顺序环视来达到这一效果。

正则表达式三:<img\b(?:(?!src=).)*src="([^"]*)"[^>]*>

“(?!src=).”表示这样一个字符,从它开始,右侧不能是字符序列“src=”,而“(?:(?!src=).)*”就表示符合上面规则的字符,有0个或无限多个。这样就达到排除字符序列的目的,实现的效果同排除型字符组一样,只不过排除型字符组排除的是一个或多个字符,而这种环视结构排除的是一个或多个有序的字符序列。

但是以顺序环视的方式排除字符序列,由于在匹配每一个字符时,都要进行较多的判断,所以相对于非贪婪模式,是提升效率还是降低效率,要根据实际情况进行分析。对于简单的正则表达式,或是简单的源字符串,一般来说是非贪婪模式效率高些,而对于数量较大源字符串,或是复杂的正则表达式,一般来说是贪婪模式效率高些。

比如上面取得img标签中的图片地址需求,基本上用正则表达二就可以了;对于复杂的应用,如平衡组中,就需要使用结合环视的贪婪模式了。

以匹配嵌套div标签的平衡组为例:

Regex reg = new Regex(@"(?isx)                      #匹配模式,忽略大小写,“.”匹配任意字符

                      <div[^>]*>                      #开始标记“<div...>”

                          (?>                         #分组构造,用来限定量词“*”修饰范围

                              <div[^>]*>  (?<Open>)   #命名捕获组,遇到开始标记,入栈,Open计数加1

                          |                           #分支结构

                              </div>  (?<-Open>)      #狭义平衡组,遇到结束标记,出栈,Open计数减1

                          |                           #分支结构

                              (?:(?!</?div\b).)*      #右侧不为开始或结束标记的任意字符

                          )*                          #以上子串出现0次或任意多次

                          (?(Open)(?!))               #判断是否还有'OPEN',有则说明不配对,什么都不匹配

                      </div>                          #结束标记“</div>”

                      ");

“(?:(?!</?div\b).)*”这里使用的就是结合环视的贪婪模式,虽然每匹一个字符都要做很多判断,但这种判断是基于字符的,速度很快,而如果这里使用非贪婪模式,那么每次要做的就是分支结构“|”的判断了,而分支结构是非常影响匹配效率的,其代价远远高于对确定字符的判断。而另外一个原因,就是贪婪模式可以结合固化分组来提升效率,而对非贪婪模式使用固化分组却是没有意义的。

4.贪婪与非贪婪——最后的回顾

4.1     一个例子的匹配原理回顾

再回过头来看一下2.1.1节例子中正则,前面从应用角度进行了分析,但讨论过匹配原理后会发现,匹配过程并不是那么简单的,下面从匹配原理角度分析的匹配过程。

 4-1

 

图4-1

首先由“<”取得控制权,由位置0位开始尝试匹配,匹配字符“a”,匹配失败,第一轮匹配结束。第二轮匹配从位置1开始尝试匹配,同样匹配失败。第三轮从位置3开始尝试匹配,匹配字符“<”,匹配成功,控制权交给“d”。

“d”尝试匹配字符“d”,匹配成功,控制权交给“i”。重复以上过程,直到由“>”匹配到字符“>”,控制权交给“.*”。

“.*”属于贪婪模式,将从B处后的字符“t”开始,一直匹配到E处,也就是字符串结束位置,将控制权交给“<”。

“<”从字符串结束位置尝试匹配,匹配失败,向前查找可供回溯的状态,把控制权交给“.*”,由“.*”让出一个字符“c”,把控制权再交给“<”,尝试匹配,匹配失败,向前查找可供回溯的状态。一直重复以上过程,直到“.*”让出已匹配的字符“<”,实际上也就是让出了已匹配的子串“</div>cc”为止,“<”才匹配字符“<”成功,控制权交给“/”。

接下来由“/”、“d”、“i”、“v”分别匹配对应的字符成功,此时整个正则表达式匹配完毕。

4.2     贪婪与非贪婪——量词的细节

4.2.1  区间量词的非贪婪模式

前面提到的非贪婪模式,一直都是使用的“*?”,而没有涉及到其它的区间量词,对于“*?”和“+?”这样的非贪婪模式,大多数接触过正则表达式的人都可以理解,但是对于区间量词的非贪婪模式,比如“{m,n}?”,要么是没见过,要么是不理解,主要是这种应用场景非常少,所以被忽略了。

首先需要明确的一点,就是量词“{m,n}”是匹配优先量词,虽然它有了上限,但是在达到上限之前,能够匹配,还是要尽可能多的匹配的。而“{m,n}?”就是对应的忽略优先量词了,在可匹配可不匹配的情况下,尽可能少的匹配。

接下来举一个例子说明这种非贪婪模式的应用。

举例(参考 限制字符长度与最小匹配):

需求:如何限制在长度为100的字符串中,从头匹配到最先出现的abc

csdn.{1,100}abc 这样写是最大匹配(1-100个字符串中,我需要最小的)

比如csdnfddabckjdsfjabc,匹配结果应为:csdnfddabc

正则表达式:csdn.{1,100}?abc

或许对这个例子还有人不是很理解,但是想想,其实“*”就等价于“{0,}”,“+”就等价于“{1,}”,“*?”也就是“{0,}?”,抽象出来也就是“{m,}?”,即上限为无穷大。如果上限为一个固定值,那就是“{m,n}?”,这样应该也就可以理解了。

“{m}”没有放在匹配优先量词中,同样的,“{m}?”虽然被部分语言所支持,但是也没有放在忽略优先量词中,主要是因为这两种量词,实现的效果是一样的,只有被修饰的子表达式匹配m次才能匹配成功,且没有可供回溯的状态,所以也不存在是匹配优先还是忽略优先的问题,也就不在本文的讨论范围内。事实上即使讨论也没有意义的,只要知道它们的匹配行为也就是了。

4.2.2  忽略优先量词的匹配下限

对于匹配优先量词的匹配下限很好理解,“?”等价于“{0,1}”,它修饰的子表达式,最少匹配0次,最多匹配1次;“*”等价于“{0,}”,它修饰的子表达式,最少匹配0次,最多匹配无穷多次;“+”等价于“{1,}”,它修饰的子表达式,最少匹配1次,最多匹配无穷多次。

对于忽略优先量词的下限,也是容易忽略的。

“??”也是忽略优先量词,被修饰的子表达式使用的也是非贪婪模式,“??”修饰的子表达式,最少匹配0次,最多匹配1次。在匹配过程中,遵循非贪婪模式匹配原则,先不匹配,即匹配0次,记录回溯状态,只有不得不匹配时,才去尝试匹配。

“*?”修饰的子表达式,最少匹配0次,最多匹配无穷多次;“+?”修饰的子表达式,最少匹配1次,最多匹配无穷多次,“+?”虽然使用的是非贪婪模式,在匹配过程中,首先要匹配一个字符,之后才是忽略匹配的,这一点也需要注意。

4.3     贪婪与非贪婪模式小结

Ø  从语法角度看贪婪与非贪婪

被匹配优先量词修饰的子表达式,使用的是贪婪模式;被忽略优先量词修饰的子表达式,使用的是非贪婪模式。

匹配优先量词包括:“{m,n}”、“{m,}”、“?”、“*”和“+”。

忽略优先量词包括:“{m,n}?”、“{m,}?”、“??”、“*?”和“+?”。

Ø  从应用角度看贪婪与非贪婪

贪婪与非贪婪模式影响的是被量词修饰的子表达式的匹配行为,贪婪模式在整个表达式匹配成功的前提下,尽可能多的匹配;而非贪婪模式在整个表达式匹配成功的前提下,尽可能少的匹配。非贪婪模式只被部分NFA引擎所支持。

Ø  从匹配原理角度看贪婪与非贪婪

能达到同样匹配结果的贪婪与非贪婪模式,通常是贪婪模式的匹配效率较高。

所有的非贪婪模式,都可以通过修改量词修饰的子表达式,转换为贪婪模式。

贪婪模式可以与固化分组结合,提升匹配效率,而非贪婪模式却不可以。


反向引用

1.概述

捕获组捕获到的内容,不仅可以在正则表达式外部通过程序进行引用,也可以在正则表达式内部进行引用,这种引用方式就是反向引用。要了解反向引用,首先要了解捕获组,关于捕获组,参考 正则基础之——捕获组(capture group)。

反向引用的作用通常是用来查找或限定重复、查找或限定指定标识配对出现等等。

对于普通捕获组和命名捕获组的引用,语法如下:

普通捕获组反向引用:\k<number>,通常简写为\number

命名捕获组反向引用:\k<name>或者\k'name'

普通捕获组反向引用中number是十进制的数字,即捕获组的编号;命名捕获组反向引用中的name为命名捕获组的组名。

2.反向引用匹配原理

捕获组(Expression)在匹配成功时,会将子表达式匹配到的内容,保存到内存中一个以数字编号的组里,可以简单的认为是对一个局部变量进行了赋值,这时就可以通过反向引用方式,引用这个局部变量的值。一个捕获组(Expression)在匹配成功之前,它的内容可以是不确定的,一旦匹配成功,它的内容就确定了,反向引用的内容也就是确定的了。

反向引用必然要与捕获组一同使用的,如果没有捕获组,而使用了反向引用的语法,不同语言的处理方式不一致,有的语言会抛异常,有的语言会当作普通的转义处理。

2.1     从一个简单例子说起

源字符串:abcdebbcde

正则表达式:([ab])\1

对于正则表达式“([ab])\1”,捕获组中的子表达式“[ab]”虽然可以匹配“a”或者“b”,但是捕获组一旦匹配成功,反向引用的内容也就确定了。如果捕获组匹配到“a”,那么反向引用也就只能匹配“a”,同理,如果捕获组匹配到的是“b”,那么反向引用也就只能匹配“b”。由于后面反向引用“\1”的限制,要求必须是两个相同的字符,在这里也就是“aa”或者“bb”才能匹配成功。

考察一下这个正则表达式的匹配过程,在位置0处,由“([ab])”匹配“a”成功,将捕获的内容保存在编号为1的组中,然后把控制权交给“\1”,由于此时捕获组已记录了捕获内容为“a”,“\1”也就确定只有匹配到“a”才能匹配成功,这里显然不满足,“\1”匹配失败,由于没有可供回溯的状态,整个表达式在位置0处匹配失败。

正则引擎向前传动,在位置5之前,“([ab])”一直匹配失败。传动到位置5处时,,“([ab])”匹配到“b”,匹配成功,将捕获的内容保存在编号为1的组中,然后把控制权交给“\1”,由于此时捕获组已记录了捕获内容为“b”,“\1”也就确定只有匹配到“b”才能匹配成功,满足条件,“\1”匹配成功,整个表达式匹配成功,匹配结果为“bb”,匹配开始位置为5,结束位置为7。

扩展一下,正则表达式“([a-z])\1{2}”也就表达连续三个相同的小写字母。

2.2     一个复杂例子的分析

详细的分析讨论参考:正则表达式正向预搜索的问题。

源字符串:aaa bbbb ffffff 999999999

正则表达式:(\w)((?=\1\1\1)(\1))+

测试代码:

string test = "aaa bbbb ffffff 999999999";

Regex reg = new Regex(@"(\w)((?=\1\1\1)(\1))+");

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

      richTextBox2.Text += "匹配结果:" + m.Value.PadRight(12, ' ') + "匹配开始位置:" + m.Index + "\n";

}

//输出

匹配结果:bb          匹配开始位置:4

匹配结果:ffff        匹配开始位置:9

匹配结果:9999999     匹配开始位置:16

匹配结果分析:

正则表达式(\w)((?=\1\1\1)(\1))+从匹配结果上分析,其实就等价于 (\w)(\1)*(?=\1\1\1)(\1) ,这个会相对好理解一些,下面讨论下分析过程。

因为“+”等价于“{1,}”,表示至少匹配1次,下面把子表达式“((?=\1\1\1)(\1))+”展开来看下规律,下表中的“次数”表示子表达式“((?=\1\1\1)(\1))+”匹配成功的次数 。

次数

等价表达式

1

(\w)((?=\1\1\1)(\1))

2

(\w)((?=\1\1\1)(\1))((?=\1\1\1)(\1))

3

(\w)((?=\1\1\1)(\1))((?=\1\1\1)(\1))((?=\1\1\1)(\1))

如果最后一个“((?=\1\1\1)(\1))”匹配成功,那么中间的“((?=\1\1\1)(\1))”一定可以匹配成功,所以中间的限制条件(?=\1\1\1)就没有意义了,这时就可以简写为“(\1)”,也就是

次数

等价表达式

1

(\w)((?=\1\1\1)(\1))

2

(\w)(\1)((?=\1\1\1)(\1))

3

(\w)(\1)(\1)((?=\1\1\1)(\1))

可以归纳为等价于

(\w)(\1)*((?=\1\1\1)(\1))

因为“((?=\1\1\1)(\1))”开始和结尾的()原来是用作量词+修饰范围的,这里已经没有什么意义了,所以表达式最后可以归纳为等价于

(\w)(\1)*(?=\1\1\1)(\1)

分析这个表达式就容易多了。“(\w)”匹配一个字符,占一位,“\1”是对“\w”匹配内容的引用,“(\1)*”可以匹配0到无穷多个“(\w)”匹配到的字符,“(?=\1\1\1)(\1)”只占一位,但是“(?=\1\1\1)”要求所在位置右侧有三个连续相同的“(\w)”匹配到的字符,所以在“(?=\1\1\1)”这个位置右侧应该有三个字符,不过只有这个位置右侧的一个字符计入最后的匹配结果,最后两个只作为限制条件,不计入最后的匹配结果 。

以“999999999”为例,第一个“9”由“(\w)”匹配,第二到第六个“9”由“(\1)*”来匹配,第七个“9”由“(?=\1\1\1)(\1)”中最后的“(\1)”来匹配,而第七、八、九这三个“9”是用来保证满足“(?=\1\1\1)”这个条件的。

2.3     反向引用的编号

对于普通捕获组的反向引用,是通过捕获组的编号来实现的,那么对于一些可能存在歧义的语法又是如何解析的呢?对于正则表达式

([ab])\10

这里的“\10”会被解析成第10个捕获组的反向引用,还是第1个捕获组的反向引用加一个普通字符“0”呢?不同语言的处理方式是不一样的。

string test = "ab0cdebb0cde";

richTextBox2.Text = Regex.Match(test, @"([ab])\10").Value;

在.NET中,以上测试代码输出为空,说明这里的“\10”被解析成第10个捕获组的反向引用,而这个表达式中是不存在第10个捕获组的,所以匹配结果为空。

<scripttype="text/javascript"> 
var str = "ab0cdebb0cde";
var reg = /([ab])\10/;
var arr = str.match(reg);
if(arr != null)
{
    document.write(arr[0]);
}
</script>

/*--------输出--------

bb0

*/

而在JavaScript中,由于浏览器解析引擎的不同,得到的结果也不一样,以上为IE下是可以得到匹配结果“bb0”,说明在IE的浏览器引擎中,“\10”被解析成第1个捕获组的反向引用加一个普通字符“0”。而在Firefox、Opera等浏览器中,得到的结果为空,说明“\10”被解析成第10个捕获组的反向引用,而这个表达式中是不存在第10个捕获组的。

string test = "ab0cdebb0cde";

richTextBox2.Text = Regex.Match(test, @"([ab])\10", RegexOptions.ECMAScript).Value;

/*--------输出--------

bb0

*/

而在.NET中,如果正则表达式加了RegexOptions.ECMAScript参数,则这里的“\10”被解析成第1个捕获组的反向引用加一个普通字符“0”。

至于正则表达式中确实有10个以上的捕获组时,“\10”的具体意义留给有兴趣的读者去测试了,因为在实际应用当中,如果你的正则表达式中用到了10个以上捕获组,而同时又用到了第10个以上捕获组的反向引用时,就要注意分析一下,你的正则是否需要进行优化,甚至于这里是否适合使用正则表达式了。

出于对现实应用场景的分析,第10个以上捕获组的反向引用几乎不存在,对它的研究通常仅存在于理论上。而对于10个以内捕获组反向引用后面还有数字,容易造成混淆的情况,可以通过非捕获组来解决。

([ab])\1(?:0)

这样就可以明确,是对第1个捕获组的反向引用,后面跟一个普通字符“0”。也就不会产生混淆了。

string test = "ab0cdebb0cde";

richTextBox2.Text = Regex.Match(test, @"([ab])\1(?:0)").Value;

/*--------输出--------

bb0

*/

而事实上,即使是这样用的场景也非常少,至今为止,只在日期正则表达式中用到过。

^(?:(?!0000)[0-9]{4}([-/.]?)(?:(?:0?[1-9]|1[0-2])\1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])\1(?:29|30)|(?:0?[13578]|1[02])\1(?:31))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2\2(?:29))$

这一节讨论的内容,了解一下就可以了,在实际应用当中,如果遇到,注意一下不要出现混淆而导致匹配结果错误就可以了。

3.反向引用应用场景分析

反向引用的作用通常是用来查找或限定重复、查找或限定指定标识配对出现等等。以下以实例进行场景分析及应用讲解。

3.1     查找重复

查找重复通常的应用场景是查找或验证源字符串中,是否有重复单词、重复项等等。

3.1.1  验证数字元素重复项

需求描述:

验证源字符串中以“,”分隔的数字是否有重复项。

代码实现:

string[] test = new string[] { "1,2,3,123,32,13", "12,56,89,123,56,98", "8,2,9,10,38,29,2,9","8,3,9,238,93,23" };

Regex reg = new Regex(@"\b(\d+)\b.*?\b\1\b");

foreach (string s in test)

{

     richTextBox2.Text += "源字符串: " + s.PadRight(20, ' ') + "验证结果: " + reg.IsMatch(s) + "\n";

}

/*--------输出--------

源字符串: 1,2,3,123,32,13     验证结果: False

源字符串: 12,56,89,123,56,98  验证结果: True

源字符串: 8,2,9,10,38,29,2,9  验证结果: True

源字符串: 8,3,9,238,93,23     验证结果: False

*/

源字符串的规则比较明确,就是用“,”分隔的数字,类似于这种查找是否有重复的需求,最简单的就是用反向引用来解决了。

由于要验证的是用“,”分隔的元素的整体是否有重复,所以“(\d+)”两侧的“\b”就是必须的,用来保证取到的数字子串是一个元素整体,而不是“123”中的“1”,当然,这里前后两个“\b”分别换成“(?<!\d)”和“(?!\d)”是一个效果,可能意义上更明确。后面的两个“\b”也是一样的作用。

3.1.2  验证连续数字是否有重复

参考 问两个正则表达式。

需求描述:

数据:

1985aaa1985bb

bcae1958fiefadf1955fef

atijc1944cvkd

df2564isdjfef2564d

实现1:匹配第一次出现的四个数字.然后后面也存在这四个数字的

如:

1985aaa1985bb

第一次出现的四个数字是1985.然后后面也存在这四个数字,所以这个匹配

bcae1958fiefadf1955fef

第一次出现的四个数字是1958.然后后面不存在这四个数字.所以不匹配

-----

所以实现1.应该匹配

1985aaa1985bb

df2564isdjfef2564d

代码实现:

//如果是验证第一个出现的连续4个数字是否有重复

string[] test = new string[] { "1985aaa1985bb", "bcae1958fiefadf1955fef", "atijc1944cvkd","df2564isdjfef2564d", "abc1234def5678ghi5678jkl" };

Regex reg = new Regex(@"^(?:(?!\d{4}).)*(\d{4})(?:(?!\1).)*\1");

foreach (string s in test)

{

     richTextBox2.Text += "源字符串:  " + s.PadRight(25, ' ') + "验证结果:  " + reg.IsMatch(s) + "\n";

}

/*--------输出--------

源字符串:  1985aaa1985bb            验证结果:  True

源字符串:  bcae1958fiefadf1955fef   验证结果:  False

源字符串:  atijc1944cvkd            验证结果:  False

源字符串:  df2564isdjfef2564d       验证结果:  True

源字符串:  abc1234def5678ghi5678jkl 验证结果:  False

*/

由于需求要求验证第一次出现的四个数字是否有重复,所以这里需要用“^(?:(?!\d{4}).)*(\d{4})”来保证捕获组取得的是第一次出现的四个数字。

这样写可能有些复杂,可读性较差,但这里需要用这种顺序环视结合贪婪模式,来达到匹配第一次出现的四个数字的目的,而不能使用非贪婪模式.

对于使用非贪婪模式的正则“^.*?(\d{4})(?:(?!\1).)*\1”,可以看一下它匹配的结果。

string[] test = new string[] { "1985aaa1985bb", "bcae1958fiefadf1955fef", "atijc1944cvkd","df2564isdjfef2564d", "abc1234def5678ghi5678jkl" };

Regex reg = new Regex(@"^.*?(\d{4})(?:(?!\1).)*\1");

foreach (string s in test)

{

     richTextBox2.Text += "源字符串:  " + s.PadRight(25, ' ') + "验证结果:  " + reg.IsMatch(s) + "\n";

}

/*--------输出--------

源字符串:  1985aaa1985bb            验证结果:  True

源字符串:  bcae1958fiefadf1955fef   验证结果:  False

源字符串:  atijc1944cvkd            验证结果:  False

源字符串:  df2564isdjfef2564d       验证结果:  True

源字符串:  abc1234def5678ghi5678jkl 验证结果:  True

*/

是的,最后一项的验证结果也是“True”,为什么会这样?当捕获组“(\d{4})”匹配到“1234”时,由于“1234”没有重复,所以后面的子表达式匹配失败,此时“.*?”会进行回溯,放弃当前状态,继续向前匹配,直到它匹配到“5678”前的“f”,由捕获组“(\d{4})”匹配到“5678”,后面的子表达式可以匹配成功,报告整个表达式匹配成功。

NFA引擎在有可供回溯的状态时,会一直尝试直到所有可能都尝试失败后才报告失败。上例中非贪婪模式在继续尝试时是可以找到匹配成功的位置的,而采用贪婪模式的正则“^(?:(?!\d{4}).)*(\d{4})”,由于“^(?:(?!\d{4}).)*”匹配到的内容不可能是连续的四个数字,所以无论怎么回溯,接下来的“(\d{4})”都不可能匹配成功,一直回溯到起始位置“^”,报告整个表达式匹配失败。

而后面的顺序环视+贪婪模式子表达式“(?:(?!\1).)*”则不存在以上问题,所以在源字符串比较简单时可以写作“.*?”,不会影响匹配结果。

而对于验证任意位置是否存在四个重复数字,则不需要加起始位置的限定。

//如果是验证任意位置出现的连续4个数字是否有重复,可以用我38楼的正则

string[] test = new string[] { "1985aaa1985bb", "bcae1958fiefadf1955fef", "atijc1944cvkd","df2564isdjfef2564d", "abc1234def5678ghi5678jkl" };

Regex reg = new Regex(@"(\d{4})(?:(?!\1).)*\1");

foreach (string s in test)

{

     richTextBox2.Text += "源字符串:  " + s.PadRight(25, ' ') + "验证结果:  " + reg.IsMatch(s) + "\n";

}

/*--------输出--------

源字符串:  1985aaa1985bb            验证结果:  True

源字符串:  bcae1958fiefadf1955fef   验证结果:  False

源字符串:  atijc1944cvkd            验证结果:  False

源字符串:  df2564isdjfef2564d       验证结果:  True

源字符串:  abc1234def5678ghi5678jkl 验证结果:  True

*/

3.2     限定指定标识配对

相对于查找重复来说,查找或指定标识配对出现这种应用场景要更多一些。尤其是对于HTML的处理中,这种应用更普遍。

3.2.1  限定标点配对

由于HTML语言的不规范性,导致以下三种写法可以被解析。

1.   <a href=www.csdn.net>CSDN</a>

2.   <a href='www.csdn.net'>CSDN</a>

3.   <a href="www.csdn.net">CSDN</a>

而这对于一些需要进行字符串解析的应用,造成很大的麻烦。在提取链接时,虽然两侧都用“[‘”]?”通常也可以得到正确结果,却不如用反向引用来得严谨、方便。

Regex reg = new Regex(@"(?is)<a(?:(?!href=).)*href=(['""]?)(?<url>[^""'\s>]*)\1[^>]*>(?<text>(?:(?!</a>).)*)</a>");

MatchCollection mc = reg.Matches(yourStr);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Groups["url"].Value + "\n";

     richTextBox2.Text += m.Groups["text"].Value + "\n";

}

/*--------输出--------

www.csdn.net

CSDN

www.csdn.net

CSDN

www.csdn.net

CSDN

*/

以下可以正确解析出三种形式的HTML代码中的链接和文本,下面把正则改一下

Regex reg = new Regex(@"(?is)<a(?:(?!href=).)*href=(['""])?(?<url>[^""'\s>]*)\1[^>]*>(?<text>(?:(?!</a>).)*)</a>");

看到区别了吗?只是把“([‘””]?)”改成了“([‘””])?”,结果会怎么样呢?

Regex reg = new Regex(@"(?is)<a(?:(?!href=).)*href=(['""])?(?<url>[^""'\s>]*)\1[^>]*>(?<text>(?:(?!</a>).)*)</a>");

MatchCollection mc = reg.Matches(yourStr);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Groups["url"].Value + "\n";

     richTextBox2.Text += m.Groups["text"].Value + "\n";

}

/*--------输出--------

www.csdn.net

CSDN

www.csdn.net

CSDN

*/

结果只取到了两组数据。这是因为对于情况1的HTML字符串,在“([‘””]?)”这种情况下,捕获组虽然匹配到的只是一个位置,但毕竟是匹配成功了,所以可以用“\1”进行反向引用;而改成“([‘””])?”,捕获组根本就没有进行匹配,所以也就无法进行反向引用。

当然,对于HTML来说,还有一些比较复杂的情况,如

<a href="javascript:alert(1 > 2)"/>

这种复杂情况涉及到的场景比较少,通常应用可以不予以考虑,否则考虑的场景太复杂,会影响匹配效率。写正则的一个一般原则就是,适用就好。这种场景如果遇到,需求根据具体情况,是否需要提取等进行分析,根据分析结果不同,写出的正则也是不一样的。

3.2.2          限定标签配对

这种应用一般是在取某几个特定标签,或是动态生成正则表达式时用到。

需求描述:

删除<script…>…</script>与<style…>…</style>标签及其中间的内容。

代码实现:

Regex reg = new Regex(@"(?is)<(script|style)\b[^>]*>(?(?!\1\b).)*</\1>");

string result = reg.Replace(yourStr, "");

因为这里要删除的标签不止一个,所以事先无法确定是哪个标签,需要用到反向引用来限定标签的配对。

当然,对于标签有嵌套的情况,就要用到平衡组了。可以参考 .NET正则基础之——平衡组。

3.2.3  取配对标签中的内容

需求描述:

[id]5554323[id!][destid]10657302023180404[destid!][srcterminalid]13518841197[srcterminalid!][msgcontent]好的[msgcontent!][receivetime]20090409165217[receivetime!]

源字符串中标签成对出现,无嵌套,分别提取标签和对应的内容。

代码实现:

string test = "[id]5554323[id!][destid]10657302023180404[destid!][srcterminalid]13518841197[srcterminalid!][msgcontent]好的[msgcontent!][receivetime]20090409165217[receivetime!]";

Regex reg = new Regex(@"(?s)\[([^\]]+)\]((?:(?!\[\1).)*)\[\1!\]");

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += "Tag: " + m.Groups[1].Value.PadRight(20, ' ') + "Content: " + m.Groups[2].Value + "\n";

}

/*--------输出--------

Tag: id                  Content: 5554323

Tag: destid              Content: 10657302023180404

Tag: srcterminalid       Content: 13518841197

Tag: msgcontent          Content: 好的

Tag: receivetime         Content: 20090409165217

*/

这种需求通常是由捕获组匹配到一个标签,然后向后匹配,直到与之配对的标签外为止,根据源字符串的特点,中间可以使用非贪婪模式,也可以使用顺序否定环视+贪婪模式。

3.3     反向引用的综合应用

3.3.1   12位数字,其中不能出现6位连续相同数字

需求描述:

只允许12位数字,并且其中不能出现6位连续相同数字。

例如,123456789012是允许的,而123333334567是不允许的。

正则表达式:^(?:([0-9])(?!\1{5})){12}$

类似这种需要判定是否有连续相同元素的需求,其实也是验证重复,也要用到反向引用。

说下分析过程,需求分解一下:

1、 一个数字

2、 它后面不能连续出现5个与它相同的数字

3、 满足以上两条的字符一共12个

那么根据需求分解写出相应的正则:

1、([0-9])

2、(?!\1{5})

3、.{12}

将以上三个分解后得出的正则,按需求逻辑关系,组合一下:

(([0-9])(?!\1{5})){12}

由于是验证整个字符串的规则,所以开始和结束标识“^”和“$”是少不了的,不需要用捕获组的地方,用非捕获代替,也就成了最后满足需求的正则:

^(?:([0-9])(?!\1{5})){12}$

其实这个例子的分析过程,也是一些正则问题解析的通用过程,先把复杂的需求由整到零的分解,再各个实现,然后把实现的正则由零到整,考虑一下相互间的逻辑关系,基本上就可以得出正确的正则表达式了。

3.3.2   A-Z以内不重复的10个字母

需求描述:A-Z以内不重复的10个字母

正则表达式1:^(?:([A-Z])(?!.*?\1)){10}$

正则表达式2:^(?:([A-Z])(?=((?!\1).)*$)){10}$

这个需求与上一个需求类似,分析过程也差不多。其实这个问题如果用正则来实现,思路是非常清晰的 。

首先因为是验证规则,所以“^”和“$”是必不可少的,分别匹配开始和结束的位置 。

然后是10个字母,那么([A-Z]){10},合起来就是^([A-Z]){10}$ 。

最后就是加一个规则,字母不能重复 。

如何保证不能重复,必然是用到反向引用 ,(一个字母)后面任意一个字母不能与这个字母重复,这样实现起来就有两种方式,当然,实质都是一样的

实现方式一:^(?:([A-Z])(?!.*?\1)){10}$

实现方式二:^(?:([A-Z])(?=(?:(?!\1).)*$)){10}$

在这个需求当中,由于可能出现的源字符串不会太长,也不会太复杂,所以这两个正则表达式在匹配效率上不会有明显的差异。

解释一下正则的含义,先解释一下方式一的正则:

^(?:([A-Z])(?!.*?\1)){10}$

“^”和“$”分别匹配开始和结束位置,“{10}”为量词,表示修饰的子串重复10次。

“(?:Expression)”是非捕获组,目的是不将“()”内的“Expression”匹配的内容保存到内存中,之所以要这样用,是因为后面的反向引用使用的是“\1”,如果不用非捕获组,那么“([A-Z])”就是编号为2的捕获组,后面的“\1”就要换成“\2”,来引用第二个捕获组,替换后对匹配结果当然不会有什么影响,但由于由“(([A-Z])(?!.*?\1))”捕获的内容我们并不关心,所以还是用非捕获组,可以提升匹配效率。

“([A-Z])”就是匹配A到Z之间的任意一个字母,并保存匹配结果到捕获组中。

“(?!.*?\1)”顺序环视,它是零宽度的,虽然进行匹配,但不保存匹配结果,可以理解为它就是在所在位置的右侧附加了一个条件,用在这里表示,它所在位置的右侧,不管间隔多少个字符,都不能出现之前匹配到的那个字符,也就是不能有重复的字母出现。

“(?:([A-Z])(?!.*?\1)){10}”就是匹配到这样一个字符 :

1、它首先是一个字母;

2、然后这个字母的右侧间隔任意多个字符,不能再出现同样的字母;

3、最后,符合以上两条规则的字符,一共有10个。

加上首尾限定字符“^”和“$”,就是满足需求的正则。

接下来讨论一下方式二的正则:

^(?:([A-Z])(?=(?:(?!\1).)*$)){10}$

思路和以及其余部分子表达式与方式一完全一样 ,只有“(?=(?:(?!\1).)*$)”这里不同,这个子表达式表示,它所在位置右侧,一直到结尾,都不能是之前匹配到的那个字符。方式一是非贪婪模式的实现,而这个就是贪婪模式的实现。

这里需要用到顺序肯定环视“(?=Expression)”,而不能用非捕获组“(?:(?:(?!\1).)*$)”,是因为这里的表达式不能占有字符,只能作为条件存在,由量词“{10}”修饰的子表达式最终只能匹配一个字符,否则就无法限定长度了。

3.3.3   提取指定单元长度字符串

需求描述 参考 求一正则表达式(c# ):

例如有字符串 string str = "w1w2w3w2w3w1w3w2w4w5w4w5w4w4w5w4w2w4w3w4w3w2w6w5w6w5w6w4w7",找出有且仅有两个单元(w+数字作为一个单元,例如:w1,w2)组成的长度大于等于4个单元的字串(必须包括这两个单元),这个例子,应输出:"w2w3w2w3","w4w5w4w5w4w4w5w4","w4w3w4w3","w6w5w6w5w6" 。

如果找出有且仅有三个单元长度大于等于6个单元的字串,该如何写正则表达式?

代码实现:

//第一个需求,两单元的

string str = "w7w7w7w5w7w1w2w3w2w3w1w3w2w4w5w4w5w4w4w5w4w2w4w3w4w3w2w6w5w6w5w6w4w7w7w7w5w7";

MatchCollection mc = Regex.Matches(str, @"(?i)(?=(w\d)\1*(w\d))(?:\1|\2){4,}");

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "\n";

}

/*--------输出--------

bb0w7w7w7w5w7

w2w3w2w3

w4w5w4w5w4w4w5w4

w4w3w4w3

w6w5w6w5w6

w4w7w7w7

*/

//第二个需求,三单元的

string str = "w7w7w7w5w7w1w2w3w2w3w1w3w2w4w5w4w5w4w4w5w4w2w4w3w4w3w2w6w5w6w5w6w4w7w7w7w5w7";

MatchCollection mc = Regex.Matches(str, @"(?i)(?=(w\d)\1*(w\d)(?:\1|\2)*(w\d))(?:\1|\2|\3){6,}");

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "\n";

}

/*--------输出--------

bb0w7w7w7w5w7w1

w2w3w2w3w1w3w2

w4w5w4w5w4w4w5w4w2w4

w2w6w5w6w5w6

w4w7w7w7w5w7

*/

这个实例可以认为是环视和反向引用综合运用的一个经典实例。主要是用到了环视零宽度,不占有字符的特性,先由环视来取得规定单元的捕获组的内容,再通过反向引用来进行实际的匹配。


零宽断言

地球人,是不是觉得这些术语名称太复杂,太难记了?我也有同感。知道有这么一种东西就行了,它叫什么,随它去吧!人若无名,便可专心练剑;物若无名,便可随意取舍……

接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像/b ,^ ,$ 那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言 。最好还是拿例子来说明吧:

断言用来声明一个应该为真的事实。正则表达式中只有当断言为真时才会继续进行匹配。

(?=exp) 也叫零宽度正预测先行断言 ,它断言自身出现的位置的后面能匹配表达式exp 。比如/b/w+(?=ing/b) ,匹配以ing结尾的单词的前面部分(除了ing以外的部分) ,如查找I'm singing while you're dancing. 时,它会匹配singdanc 。

(?<=exp) 也叫零宽度正回顾后发断言 ,它断言自身出现的位置的前面能匹配表达式exp 。比如(?<=/bre)/w+/b 会匹配以re开头的单词的后半部分(除了re以外的部分) ,例如在查找reading a book 时,它匹配ading 。

假如你想要给一个很长的数字中每三位间加一个逗号(当然是从右边加起了),你可以这样查找需要在前面和里面添加逗号的部分:((?<=/d)/d{3})+/b ,用它对1234567890 进行查找时结果是234567890 。

下面这个例子同时使用了这两种断言:(?<=/s)/d+(?=/s) 匹配以空白符间隔的数字(再次强调,不包括这些空白符)


负向零宽断言

前面我们提到过怎么查找不是某个字符或不在某个字符类里 的字符的方法(反义)。但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它 时怎么办?例如,如果我们想查找这样的单词--它里面出现了字母q,但是q后面跟的不是字母u,我们可以尝试这样:

/b/w*q[^u]/w*/b 匹配包含后面不是字母u的字母q 的单词 。但是如果多做测试(或者你思维足够敏锐,直接就观察出来了),你会发现,如果q出现在单词的结尾的话,像Iraq ,Benq ,这个表达式就会出错。这是因为[^u] 总要匹配一个字符,所以如果q是单词的最后一个字符的话,后面的[^u] 将会匹配q后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的/w*/b 将会匹配下一个单词,于是/b/w*q[^u]/w*/b 就能匹配整个Iraq fighting 。负向零宽断言 能解决这样的问题,因为它只匹配一个位置,并不消费 任何字符。现在,我们可以这样来解决这个问题:/b/w*q(?!u)/w*/b 。

 

零宽度负预测先行断言 (?!exp) ,断言此位置的后面不能匹配表达式exp 。例如:/d{3}(?!/d) 匹配三位数字,而且这三位数字的后面不能是数字 ;/b((?!abc)/w)+/b 匹配不包含连续字符串abc的单词 。

同理,我们可以用(?<!exp) ,零宽度负回顾后发断言 来断言此位置的前面不能匹配表达式exp :(?<![a-z])/d{7} 匹配前面不是小写字母的七位数字 。

请详细分析表达式(?<=<(/w+)>).*(?=<///1>) ,这个表达式最能表现零宽断言的真正用途。

一个更复杂的例子:(?<=<(/w+)>).*(?=<///1>) 匹配不包含属性的简单HTML标签内里的内容 。(?<=<(/w+)>) 指定了这样的前缀 :被尖括号括起来的单词 (比如可能是<b>),然后是.* (任意的字符串),最后是一个后缀 (?=<///1>)。注意后缀里的// ,它用到了前面提过的字符转义;/1 则是一个反向引用,引用的正是捕获的第一组 ,前面的(/w+) 匹配的内容,这样如果前缀实际上是<b>的话,后缀就是</b>了。整个表达式匹配的是<b>和</b>之间的内容(再次提醒,不包括前缀和后缀本身)。



原创粉丝点击