前言
贪婪模式和非贪婪模式是正则匹配中的重要特性 在理解贪婪和非贪婪的区别时,可以根据实例,一步一步的循序渐进
大纲
- 匹配规则简介
- 贪婪模式与非贪婪模式快速理解
- 实例练习
- 回溯现象与匹配失败
匹配规则简介
var str='aabcab';
var reg=/ab/;
var res=str.match(reg);
// ab index 为 1
console.log(res);
要快速理解正则的匹配规则,可以先尝试理解上述的例子
匹配步骤是这样的:
- 初始
index=0 ,匹配到了字符a
- 接下来匹配下一个字符
a ,但是由于aa 和/ab/ 不匹配,因此此次匹配失败
-
index 挪到下一个,从1 开始,又重新匹配了a
- 接下来匹配下一个字符
b ,刚好和/ab/ 匹配,因此此次匹配成功,返回了ab ,index=1
- 如果正则的匹配后面有
g 这种关键字,则会继续开始下一组的匹配(但是本例中没有g ,因此只有一组结果)
要点
这一个要点的详细解释是: 例如第一个匹配的字符是a ,假设之后的匹配没有出现匹配失败的情况。则它将一直匹配下去,直到匹配完成,也就是说index=0 不会变,直到匹配完成(如果出现匹配失败并且无法回溯,index 才会继续往下挪)
这一点适用于下面的贪婪模式与非贪婪模式中(并且优先级高于它们),因此请谨记
贪婪模式与非贪婪模式快速理解
贪婪匹配模式
定义
正则表达式去匹配时,会尽量多的匹配符合条件的内容
标识符
+ ,? ,* ,{n} ,{n,} ,{n,m}
匹配时,如果遇到上述标识符,代表是贪婪匹配,会尽可能多的去匹配内容
示例
var str='aacbacbc';
var reg=/a.*b/;
var res=str.match(reg);
// aacbacb index为0
console.log(res);
上例中,匹配到第一个a 后,开始匹配.* ,由于是贪婪模式,它会一直往后匹配,直到最后一个满足条件的b 为止,因此匹配结果是aacbacb
示例2
var str='aacbacbc';
var reg=/ac.*b/;
var res=str.match(reg);
// acbacb index为1
console.log(res);
第一个匹配的是a ,然后再匹配下一个字符a 时,和正则不匹配,因此匹配失败,index 挪到1 ,接下来匹配成功了ac ,继续往下匹配,由于是贪婪模式,尽可能多的去匹配结果,直到最后一个符合要求的b 为止,因此匹配结果是acbacb
非贪婪匹配模式
定义
正则表达式去匹配时,会尽量少的匹配符合条件的内容 也就是说,一旦发现匹配符合要求,立马就匹配成功,而不会继续匹配下去(除非有g ,开启下一组匹配)
标识符
+? ,?? ,*? ,{n}? ,{n,}? ,{n,m}?
可以看到,非贪婪模式的标识符很有规律,就是贪婪模式的标识符后面加上一个?
示例
var str='aacbacbc';
var reg=/a.*?b/;
var res=str.match(reg);
// aacb index为0
console.log(res);
上例中,匹配到第一个a 后,开始匹配.*? ,由于是非贪婪模式,它在匹配到了第一个b 后,就匹配成功了,因此匹配结果是aacb
为什么是aacb 而不是acb 呢? 因为前面有提到过一个正在匹配的优先规则: 最先开始的匹配拥有最高的优先权 第一个a 匹配到了,只要之后没有发生匹配失败的情况,它就会一直匹配下去,直到匹配成功
示例2
var str='aacbacbc';
var reg=/ac.*?b/;
var res=str.match(reg);
// acb index为1
console.log(res);
先匹配的a ,接下来匹配第二个a 时,匹配失败了index 变为1 ,继续匹配ac 成功,继续匹配b ,由于是非贪婪模式,此时acb 已经满足了正则的最低要求了,因此匹配成功,结果为acb
示例3
var str='aacbacbc';
var reg=/a.*?/;
var res=str.match(reg);
// a index为0
console.log(res);
var reg2=/a.*/;
var res2=str.match(reg2);
// aacbacbc index为0
console.log(res2);
这一个例子则是对示例1的补充,可以发现,当后面没有b 时,由于是非贪婪模式,匹配到第一个a 就直接匹配成功了 而后面一个贪婪模式的匹配则是会匹配所有
实例练习
在初步理解了贪婪模式与非贪婪模式后,可以通过练习加深理解
提取HTML 中的Div 标签
给出一个HTML 字符串,如下
其它元素
<div><span>用户:<span/><span>张三<span/></div>
<div><span>密码:<span/><span>123456<span/></div>
其它元素
__需求__: 提取出div 包裹的内容(包括div 标签本身),并将各个结果存入数组
__代码__: 通过非贪婪模式的全局匹配来完成,如下
var reg=/<div>.*?</div>/g;
var res=str.match(reg);
// ["<div><span>用户:<span/><span>张三<span/></div>","<div><span>密码:<span/><span>123456<span/></div>"]
console.log(res);
__详解__: 用到了两个知识点,.*? 的非贪婪模式匹配以及g 全局匹配
-
<div>.*?</div> 代表每次只会匹配一次div ,这样可以确保每一个div 不会越界
- 最后的
g 代表全局匹配,即第一次匹配成功后,会将匹配结果放入数组,然后从下一个index 重新开始匹配新的结果
__另外__: 假设使用了/<div>.*</div>/g 进行贪婪模式的匹配,结果则是
["<div><span>用户:<span/><span>张三<span/></div><div><span>密码:<span/><span>123456<span/></div>"]
因为贪婪模式匹配了第一个<div> 后会无限贪婪的匹配接下来的字符,直到最后一个符合条件的</div> 为止,导致了将中间所有的div 标签都一起匹配上了
提取两个"" 中的子串,其中不能再包含""
示例引用自: 正则表达式之 贪婪与非贪婪模式详解
"The phrase "regular expression" is called "Regex" for short"
__需求__: 提取两个引号之间的子串,其中不能再包括引号,例如上述的提取结果应该是: "regular expression" 与 "Regex" (每一个结束的" 后面都接空格)
__错误解法__: 通过如下的非贪婪匹配(请注意空格)
var str='"The phrase "regular expression" is called "Regex" for short"';
var reg=/".*?" /g;
var res=str.match(reg);
// ['"The phrase "regular expression" ','"Regex" ']
console.log(res);
可以看到,上述的匹配完全就是匹配错误了,这个正则匹配到第一个符合条件的"+空格 后就自动停下来了
__正确解法__: 使用贪婪模式进行匹配
var reg=/"[^"]*" /g;
var res=str.match(reg);
// ['"regular expression" ','"Regex" ']
console.log(res);
这个匹配中
- 从第一个
" 开始匹配,接下来到12 位时("r 的" ),不满足[^"] ,也不满足之后的"+空格 ,因此匹配失败了,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 次
优化去除回溯
上述的贪婪匹配中,出现了一次回溯现象,其实也可以通过优化表达式来防止回溯的,比如
"[^"]*"
这个表达式中构建了一个子表达式-[] 中的^" ,它的作用是排除" 匹配,这样* 的贪婪匹配就不会主动吃进" ,这样最后就直接是" 匹配" ,匹配成功,不会进行回溯
总结
上述的分析中可以看出,在匹配成功的情况下,贪婪模式进行了更少的回溯(可以自行通过更多的实验进行验证),因此在应用中,在对正则掌握不是很精通的情况下,可以优先考虑贪婪模式的匹配,这样可以避免很多性能上的问题
匹配失败的情况
上述的回溯分析都是基于匹配成功的情况,那如果是匹配失败呢?
var str = '"Regex'
var reg = /"[^"]*"/g;
这个原字符中,没有最后的" ,因此匹配是会失败的,它的过程大致如下
-
" 匹配" ,接着[] 的^" 与* 匹配R ,e ,g ,e ,x
- 接着到了最后,
" 获取控制权,由于到了最后,开始回溯
- 依次回溯的结果是
* 让出x ,e ,g ,e ,R ,直到* 已经无法再让出字符,第一轮匹配失败
- 接着
index 开始往下挪,依次用" 匹配R ,e ,g ,e ,x 都失败了,一直到最后也没有再匹配到结果,因此此次正则表达式的匹配失败,没有匹配到结果(或者返回null )
那非贪婪模式呢?
/"[^"]*?"/g
-
" 匹配" ,接着* 尝试不匹配," 匹配R ,失败,然后回溯,* 吃进R
- 接下来类似于上一步,
* 依次回溯吃进e ,g ,e ,x ,一直到最后,* 再次回溯想吃进时,已经到了字符串结尾了,无法继续,因此第一轮匹配失败
- 接着
index 开始往下挪,依次用" 匹配R ,e ,g ,e ,x 都失败了,返回null
总结
通过匹配失败的例子可以看出贪婪和非贪婪的模式区别。贪婪是先吃进,回溯再让出 ,非贪婪是先忽略,回溯再吃进
而且,在匹配失败的情况下,贪婪模式也会进行不少的回溯(非贪婪当然一直都很多回溯)
但是,实际情况中是可以通过子表达式优化的,比如构建^xxx ,可以当匹配到不符合条件的时候提前匹配失败,这样就会少很多回溯
var str = '"cccccc'
var reg = /"[^"c]*"/g;
这个由于直接排除了c ,因此* 不会吃进它,直接就匹配失败了,减少了很多回溯(当然,上述只是最简单的例子,实际情况要更复杂)
写在最后的话
正则匹配中,贪婪模式与非贪婪模式乍看之下一看便知,很容易理解,但是真正的深入理解需要掌握正则的原理才行,并且,真正理解它们后,就不仅仅只是写出普通的正则表达式,而是高性能的正则表达式了,比如理解非贪婪模式中的回溯特性后更容易写出高性能的表达式
本文也只是做一些浅显的分析与引导,更多是起到抛砖引玉的作用,要深入理解还请去了解正则的原理
附录
博客
初次发布2017.07.06 于个人博客
http://www.dailichun.com/2017/07/06/regularExpressionGreedyAndLazy.html
参考资料
- 正则表达式元字符
- 正则表达式之 贪婪与非贪婪模式详解
- js中的正则表达式入门
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|