# 贪婪模式与非贪婪模式
# 贪婪匹配模式
# 定义
正则表达式去匹配时,会尽量多的匹配符合条件的内容
# 标识符
+,?,*,{n},{n,},{n,m}
匹配时,如果遇到上述标识符,代表是贪婪匹配,会尽可能多的去匹配内容
# 示例
|
|
上例中,匹配到第一个a后,开始匹配.*,由于是贪婪模式,它会一直往后匹配,直到最后一个满足条件的b为止,因此匹配结果是aacbacb
|
|
第一个匹配的是a,然后再匹配下一个字符a时,和正则不匹配,因此匹配失败,index挪到1,接下来匹配成功了ac,继续往下匹配,由于是贪婪模式,尽可能多的去匹配结果,直到最后一个符合要求的b为止,因此匹配结果是acbacb
# 非贪婪匹配模式
# 定义
正则表达式去匹配时,会尽量少的匹配符合条件的内容 也就是说,一旦发现匹配符合要求,立马就匹配成功,而不会继续匹配下去(除非有g,开启下一组匹配)
# 标识符
+?,??,*?,{n}?,{n,}?,{n,m}?
可以看到,非贪婪模式的标识符很有规律,就是贪婪模式的标识符后面加上一个?
# 示例
|
|
上例中,匹配到第一个a后,开始匹配.*?,由于是非贪婪模式,它在匹配到了第一个b后,就匹配成功了,因此匹配结果是aacb
为什么是aacb而不是acb呢? 因为前面有提到过一个正在匹配的优先规则: 最先开始的匹配拥有最高的优先权 第一个a匹配到了,只要之后没有发生匹配失败的情况,它就会一直匹配下去,直到匹配成功
|
|
先匹配的a,接下来匹配第二个a时,匹配失败了index变为1,继续匹配ac成功,继续匹配b,由于是非贪婪模式,此时acb已经满足了正则的最低要求了,因此匹配成功,结果为acb
|
|
这一个例子则是对示例1的补充,可以发现,当后面没有b时,由于是非贪婪模式,匹配到第一个a就直接匹配成功了 而后面一个贪婪模式的匹配则是会匹配所有
# 实例练习
在初步理解了贪婪模式与非贪婪模式后,可以通过练习加深理解
- 提取HTML中的Div标签
给出一个HTML字符串,如下
|
|
需求: 提取出div包裹的内容(包括div标签本身),并将各个结果存入数组
代码: 通过非贪婪模式的全局匹配来完成,如下
|
|
详解: 用到了两个知识点,.*?的非贪婪模式匹配以及g全局匹配
最后的g代表全局匹配,即第一次匹配成功后,会将匹配结果放入数组,然后从下一个index重新开始匹配新的结果
另外: 假设使用了/
["
- 提取两个"“中的子串,其中不能再包含”"
示例引用自: 正则表达式之 贪婪与非贪婪模式详解(http://www.jb51.net/article/31491.htm)
“The phrase "regular expression" is called "Regex" for short” 需求: 提取两个引号之间的子串,其中不能再包括引号,例如上述的提取结果应该是: “regular expression” 与 “Regex”(每一个结束的"后面都接空格)
|
|
可以看到,上述的匹配完全就是匹配错误了,这个正则匹配到第一个符合条件的"+空格后就自动停下来了
|
|
这个匹配中
从第一个"开始匹配,接下来到12位时(“r的”),不满足{FNXX==XXFN},也不满足之后的"+空格,因此匹配失败了,index挪到下一个,开始下一次匹配
第二个匹配从"r的"开始,一直匹配到n"空格的空格,这一组刚刚好匹配成功(因为最后符合了正则的"空格),匹配好了"regular expression"空格
第三个匹配匹配到了"Regex"空格(过程不再复述)
到最后时,仅剩一个"直接匹配失败(因为首先得符合"才能开始挣扎匹配)
至此,正则匹配结束,匹配成功,并且符合预期
最后: 这个例子相对来说复杂一点,如要更好的理解,可以参考引用来源中的文章,里面有就原理进行介绍 另外,参考文章中还有对非贪婪模式的匹配失败,回溯影响性能等特性进行原理分析与讲解
# 回溯现象与匹配失败
你真的已经理解了贪婪模式和非贪婪模式么?
# 回溯现象
不知道对上面最后例子中提到的回溯这词有没有概念? 这里仍然以上例引用来源中的示例来分析
原字符串
“Regex”
# 贪婪匹配过程分析
“.” 第一个"取得控制权,匹配正则中的",匹配成功,控制权交给.
.*取得控制权后,匹配接下来的字符,.代表匹配任何字符,*代表可匹配可不匹配,这属于贪婪模式的标识符,会优先尝试匹配,于是接下来从1位置处的R开始匹配,依次成功匹配了R,e,g,e,x,接着继续匹配最后一个字符",匹配成功,这时候已经匹配到了字符串的结尾,所以.*匹配结束,将控制符交给正则式中最后的"
“取得控制权后,由于已经是到了字符串的结尾,因此匹配失败,向前查找可供回溯的状态,控制权交给.*,.*让出一个字符”,再把控制权交给",此时刚好匹配成功
至此,整个正则表达式匹配完毕,匹配结果为"Regex",匹配过程中回溯了1次
# 非贪婪匹配表达式
|
|
第一个"取得控制权,匹配正则中的",匹配成功,控制权交给.*?
.*?取得控制权后,由于这是非贪婪模式下的标识符,因此在可匹配可不匹配的情况下会优先不匹配,因此尝试不匹配任何内容,将控制权交给",此时index在1处(R字符处)
“取得控制权后,开始匹配1处的R,匹配失败,向前查找可供回溯的状态,控制权交给.?,.?吃进一个字符,index到了2处,再把控制权交给”
“取得控制权后,开始匹配2处的e,匹配失败,重复上述的回溯过程,直到.*?吃进了x字符,再将控制权交给”
“取得控制权后,开始匹配6处的”,匹配成功
至此,整个正则表达式匹配完毕,匹配结果为"Regex”,匹配过程中回溯了5次
优化去除回溯
上述的贪婪匹配中,出现了一次回溯现象,其实也可以通过优化表达式来防止回溯的,比如
|
|
这个表达式中构建了一个子表达式-[]中的^",它的作用是排除"匹配,这样*的贪婪匹配就不会主动吃进",这样最后就直接是"匹配",匹配成功,不会进行回溯
# 总结
上述的分析中可以看出,在匹配成功的情况下,贪婪模式进行了更少的回溯(可以自行通过更多的实验进行验证),因此在应用中,在对正则掌握不是很精通的情况下,可以优先考虑贪婪模式的匹配,这样可以避免很多性能上的问题
匹配失败的情况
上述的回溯分析都是基于匹配成功的情况,那如果是匹配失败呢?
|
|
这个原字符中,没有最后的",因此匹配是会失败的,它的过程大致如下
“匹配”,接着[]的^“与*匹配R,e,g,e,x
接着到了最后,“获取控制权,由于到了最后,开始回溯
依次回溯的结果是让出x,e,g,e,R,直到已经无法再让出字符,第一轮匹配失败
接着index开始往下挪,依次用"匹配R,e,g,e,x都失败了,一直到最后也没有再匹配到结果,因此此次正则表达式的匹配失败,没有匹配到结果(或者返回null)
那非贪婪模式呢?
|
|
“匹配”,接着*尝试不匹配,“匹配R,失败,然后回溯,*吃进R
接下来类似于上一步,*依次回溯吃进e,g,e,x,一直到最后,*再次回溯想吃进时,已经到了字符串结尾了,无法继续,因此第一轮匹配失败
接着index开始往下挪,依次用"匹配R,e,g,e,x都失败了,返回null
# 总结
通过匹配失败的例子可以看出贪婪和非贪婪的模式区别。贪婪是先吃进,回溯再让出,非贪婪是先忽略,回溯再吃进
而且,在匹配失败的情况下,贪婪模式也会进行不少的回溯(非贪婪当然一直都很多回溯)
但是,实际情况中是可以通过子表达式优化的,比如构建^xxx,可以当匹配到不符合条件的时候提前匹配失败,这样就会少很多回溯
|
|
这个由于直接排除了c,因此*不会吃进它,直接就匹配失败了,减少了很多回溯(当然,上述只是最简单的例子,实际情况要更复杂)
# 非贪婪匹配的效率
可能有不少的人和我一样,有过这样的经历:当我们要匹配类似 “
当发现非贪婪匹配之时,恍然大悟,同样功能的表达式可以写得如此简单:"
然而,当一个表达式中,有多个非贪婪匹配时,或者多个未知匹配次数的表达式时,这个表达式将可能存在效率上的陷阱。有时候,匹配速度慢得莫名奇妙,甚至开始怀疑正则表达式是否实用。
# 效率陷阱的产生:
在本站基础文章里,对非贪婪匹配的描述中说到:“如果少匹配就会导致整个表达式匹配失败的时候,与贪婪模式类似,非贪婪模式会最小限度的再匹配一些,以使整个表达式匹配成功。”
具体的匹配过程是这样的:
“非贪婪部分” 先匹配最少次数,然后尝试匹配 “右侧的表达式”。 如果右侧的表达式匹配成功,则整个表达式匹配结束。如果右侧表达式匹配失败,则 “非贪婪部分” 将增加匹配一次,然后再尝试匹配 “右侧的表达式”。 如果右侧的表达式又匹配失败,则 “非贪婪部分” 将再增加匹配一次。再尝试匹配 “右侧的表达式”。 依此类推,最后得到的结果是 “非贪婪部分” 以尽可能少的匹配次数,使整个表达式匹配成功。或者最终仍然匹配失败。 当一个表达式中有多个非贪婪匹配,以表达式 “d(\w+?)d(\w+?)z” 为例,对于第一个括号中的 “\w+?” 来说,右边的 “d(\w+?)z” 属于它的 “右侧的表达式”,对于第二个括号中的 “\w+?” 来说,右边的 “z” 属于它的 “右侧的表达式”。
当 “z” 匹配失败时,第二个 “\w+?” 会 “增加匹配一次”,再尝试匹配 “z”。如果第二个 “\w+?” 无论怎样 “增加匹配次数”,直至整篇文本结束,“z” 都不能匹配,那么表示 “d(\w+?)z” 匹配失败,也就是说第一个 “\w+?” 的 “右侧” 匹配失败。此时,第一个 “\w+?” 会增加匹配一次,然后再进行 “d(\w+?)z” 的匹配。循环前面所讲的过程,直至第一个 “\w+?” 无论怎么 “增加匹配次数”,后边的 “d(\w+?)z” 都不能匹配时,整个表达式才宣告匹配失败。
其实,为了使整个表达式匹配成功,贪婪匹配也会适当的“让出”已经匹配的字符。因此贪婪匹配也有类似的情况。当一个表达式中有较多的未知匹配次数的表达式时,为了让整个表达式匹配成功,各个贪婪或非贪婪的表达式都要进行尝试减少或增加匹配次数,由此容易形成一个大循环的尝试,造成了很长的匹配时间。本文之所以称之为“陷阱”,因为这种效率问题往往不易察觉。
举例:“d(\w+?)d(\w+?)d(\w+?)z” 匹配 “ddddddddddd…” 时,将花费较长一段时间才能判断出匹配失败 。
# 效率陷阱的避免:
避免效率陷阱的原则是:避免“多重循环”的“尝试匹配”。并不是说非贪婪匹配就是不好的,只是在运用非贪婪匹配的时候,需要注意避免过多“循环尝试”的问题。
情况一:对于只有一个非贪婪或者贪婪匹配的表达式来说,不存在效率陷阱。也就是说,要匹配类似 “
情况二:如果一个表达式中有多个未知匹配次数的表达式,应防止进行不必要的尝试匹配。
比如,对表达式 “” 来说, 如果前面部分表达式在遇到 “” 却匹配失败,将导致第一个 “.?” 增加匹配次数再尝试。而对于表达式真正目的,让第一个 “.?” 增加匹配成“vbscript’>”是不对的,因此这种尝试是不必要的尝试。
因此,对依靠边界来识别的表达式,不要让未知匹配次数的部分跨过它的边界。前面的表达式中,第一个 “.?” 应该改写成 “[^’]"。后边那个 “.?” 的右边再没有未知匹配次数的表达式,因此这个非贪婪匹配没有效率陷阱。于是,这个匹配脚本块的表达式,应该写成:"<script language=’([^’])’>(.*?)” 更好。