有点复杂,是转载的文章
贪婪模式与非贪婪模式讲解
Javascript中的正则贪婪与非贪婪模式的区别是:
- 被量词修饰的子表达式的匹配行为;
- 贪婪模式在整个表达式匹配成功的情况下尽可能多的匹配;
- 非贪婪模式在整个表达式匹配成功的前提下,尽可能少的匹配;
一些常见的修饰贪婪模式的量词如下:
1. 什么是贪婪模式?
var str = "longen<p>我是中国人</p>yunxi<p>我是男人</p>boyboy";
// 贪婪模式 匹配所有字符
console.log(str.match(/<p>.*<\/p>/)[0]);
// <p>我是中国人</p>yunxi<p>我是男人</p>
// 后面加问号,变成非贪婪模式
console.log(str.match(/<p>.*?<\/p>/)[0]); // <p>我是中国人</p>
我们来理解下匹配的基本原理;
首先正则是 /<p>.*</p>/ 匹配;<p>匹配字符串第一个字符匹配失败,接着往下,直接匹配到<p>时候,匹配成功,接着把匹配的控制权交给.*,从匹配到的<p>位置开始匹配,一直到</p>之前,接着把控制权交给</p>,接着在当前位置下往下匹配,因此匹配到</p>,匹配成功;由于它是贪婪模式,在匹配成功的前提下,仍然会尝试向右往下匹配,因此会匹配到两个<p>标签结束;但是非贪婪模式,也就是第二个console.log();他匹配到第一个p标签成功后,它就不会再进行匹配,因此打印的值为一个p标签的内容了;
理解匹配成功前提下的含义: 上面我们解释了,贪婪模式是要等一个表达式匹配成功后,再往下匹配;比如我们现在接着再看下们的表达式匹配代码:
var str = "longen<p>我是中国人</p>yunxi<p>我是男人</p>boyboy";
// 贪婪模式
console.log(str.match(/<p>.*<\/p>yunxi/)[0]);
//打印 <p>我是中国人</p>yunxi
如上代码,打印出一个p标签内容,我们知道.*是贪婪模式匹配,如果按照上面的解释会打印出2个p标签的内容,因为这里在匹配到p标签后,会继续匹配后面的yunxi字符串,直到整个表达式匹配成功,才提示匹配成功,因此当匹配到第一个yunxi后,第一个子表达式匹配成功,接着尝试往右继续匹配,<p></p>都匹配成功,但是yunxi不会匹配boyboy,所以第二次尝试匹配的子表达式匹配失败,因此只返回匹配成功后的第一个p标签的内容了;
我们现在再来看看非贪婪模式的含义:
测试代码:
var str = "longen<p>我是中国人</p>yunxi<p>我是男人</p>boyboy<p>我是中国人2</p>yunxi<p>我是男人</p>boyboy";
// 非贪婪模式1
console.log(str.match(/<p>.*?<\/p>boyboy/)[0]);
//<p>我是中国人</p>yunxi<p>我是男人</p>boyboy
// 贪婪模式
console.log(str.match(/<p>.*<\/p>yunxi/)[0]);
//<p>我是中国人</p>yunxi<p>我是男人</p>boyboy<p>我是中国人2</p>yunxi
我们先看表达式1,/<p>.*?</p>boyboy/ 匹配str字符串,首先先匹配到p标签内容;但是由于boyboy字符串一直没有匹配到,因此会一直尝试往后匹配,直到匹配到boyboy字符串后,才匹配成功,否则匹配失败;由于它是非贪婪模式,因此这时候它不会再往下进行匹配,所以匹配就结束了;因此第一个console输出为<p>我是中国人</p>yunxi<p>我是男人</p>boyboy;
我们可以再来看看贪婪模式 第二个console.log()输出的; 正则表达式 /<p>.*</p>yunxi/ 匹配到第一个p标签yunxi后,由于它是贪婪的,它还想接着向右继续匹配,直到匹配完成后,匹配成功,才结束,因此把所有p标签后面跟随yunxi的字符串都匹配到,且之间的所有的字符串都被返回;
理解正则表达式匹配原理
我们先来理解下占有字符和零宽度的含义。
1. 占有字符和零宽度
在正则表达式匹配的过程中,如果子表达式匹配到的是字符内容,而非位置的话,并被保存在匹配的结果当中,那么就认为该子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者说匹配中的内容不保存到匹配的结果当中,那么就认为该子表达式是零宽度的。我们先来理解下零宽度的列子,最常见的就是环视~ 它只匹配位置;比如顺序环视;环视待会就讲;
正则匹配方式 /abc/
匹配过程:首先由字符a取得控制权,从位置0开始进行匹配,a匹配a,匹配成功;接着往下匹配,把控制权交给b,那么现在从位置1开始,往下匹配,匹配到字符串b,匹配成功,接着继续往下匹配,位置是从2开始,把控制权交给c,继续往下匹配,匹配到字符串c,匹配成功,所以整个表达式匹配成功;匹配结果为 abc 匹配的开始位置为0,结束位置为3;
含有匹配优先量词的匹配过程
源字符串abc,正则表达式为ab?c ;量词?可以理解为匹配优先量词,在可匹配可不匹配的时候,会优先选择匹配;当匹配不到的时候,再进行不匹配。先匹配b是否存在,如果不存在的话,就不匹配b;因此结果可以匹配的有 abc,ac等
匹配过程:
首先由字符a取得控制权,从位置0开始匹配,a匹配到字符串a,匹配成功;接着继续匹配,把控制权交给b,b现在就从位置1开始匹配;匹配到字符串b,匹配成功;接着就把控制权交给c,c从位置2开始继续匹配,匹配字符串c,匹配成功;整个表达式匹配成功;假如b那会儿匹配不成功的话,它会忽略b,继续匹配字符串c,也就是如果匹配成功的话,结果是ac;
因此abc匹配字符串abc,匹配的位置从0开始,到3结束。
如果匹配的结果为ac的话,那么匹配的位置从0开始,到2结束;
假如我们把字符串改为abd,或者abe等其他的,那么当匹配到最后一个字符的时候,就匹配失败;
含有忽略优先量词的匹配过程
量词?? 含义是 忽略优先量词,在可匹配和可不匹配的时候,会选择不匹配,这里的量词是修饰b字符的,所以b?? 是一个整体的。匹配过程如下
首先由字符a取得控制权,从位置0开始匹配,有”a”匹配a,匹配成功,控制权交给b?? ;首先先不匹配b,控制权交给c,由c来匹配b,匹配失败,此时会进行回溯,由b??来进行匹配b,匹配成功,然后会再把控制权交给c,c匹配c,匹配成功,因此整个表达式都匹配成功;
理解正则表达式----环视
环视只进行子表达式匹配,不占有字符,匹配到的内容不保存到最终的匹配的结果,是零宽度的,它匹配的结果就是一个位置;环视的作用相当于对所在的位置加了一个附加条件,只有满足了这个条件,环视子表达式才能匹配成功。环视有顺序和逆序2种,顺序和逆序又分为肯定和否定,因此共加起来有四种;但是javascript中只支持顺序环视,因此我们这边来介绍顺序环视的匹配过程;
如下说明:
1.(?=Expression): 顺序肯定环视,含义是所在的位置右侧位置能够匹配到regexp.
2.(?!Expression): 顺序否定环视,含义是所在的位置右侧位置不能匹配到regexp
顺序肯定环视
首先我们需要明白的是:^和$ 是匹配的开始和结束位置的;?= 是顺序肯定环视,它只匹配位置,不会占有字符,因此它是零宽度的。这个正则的含义是:
以字母或者数字组成的,并且第一个字符必须为小写字母开头;
匹配过程如下:
首先由元字符^取得控制权,需要以字母开头,接着控制权就交给 顺序肯定环视 (?=[a-z]); 它的含义是:要求它所在的位置的右侧是有a-z小写字母开头的才匹配成功,字符a12,第一个字符是a,因此匹配成功;我们都知道环视都是匹配的是一个位置,不占有字符的,是零宽度的,因此位置是0,把控制权交给[a-z0-9]+,它才是真正匹配字符的,因此正则[a-z0-9]+从位置0开始匹配字符串a12,且必须以小写字母开头,第一个字母是a匹配成功,接着继续从1位置匹配,是数字1,也满足,继续,数字2也满足,因此整个表达式匹配成功;最后一个$符合的含义是以字母或者数字结尾的;
顺序否定环视
当顺序肯定环视匹配成功的话,顺序否定环视就匹配失败,当顺序肯定环视匹配失败的话,那么顺序否定环视就匹配成功;
源字符串:aa<p>one</p>bb<div>two</div>cc
正则:<(?!/?p\b)[^>]+>
正则的含义是:匹配除<p>之外的其余标签;
如下图:
匹配过程如下:
首先由”<” 取得控制权,从位置0开始匹配,第一个位置和第二个位置都是字符a,因此匹配失败~ 接着从位置2匹配,匹配到<, 匹配成功了,现在控制权就交给(?!/?p\b);?!是顺序否定环视,只匹配一个位置,不匹配字符,这个先不用管,首先是 /? 取得控制权,它的含义是:可匹配/,或者不匹配/, 接着往下匹配的是p字符,匹配失败,进行回溯,不匹配,那么控制权就到一位了p字符,p匹配p,匹配成功,控制权就交给\b; \b的含义是匹配单词的边界符,\b就匹配到了 > ,结果就是匹配成功,子表达式匹配就完成了;/?p\b 就匹配成功了;所以(?!/?p\b) 这个就匹配失败了;从而使表达式匹配失败;我们继续往下匹配,从b字符开始,和上面一样匹配失败,当位置是从14开始的时候 < 字符匹配到”<”,匹配成功,把控制权又交给了(?!/?p\b), 还是/?取得控制权,和上面匹配的逻辑一样,最后?p\b匹配失败了,但是(?!/?p\b) 就匹配成功了,因此这一次表达式匹配成功;如下代码匹配:
var str = "aa<p>one</p>bb<div>two</div>cc";
// 匹配的结果为div,位置从14开始 19结束
console.log(str.match(/<(?!/?p\b)[^>]+>/)[0]);
理解正则表达式---捕获组
捕获组就是把正则表达式中子表达式匹配的内容,保存到内存中以数字编号或显示命名的组里,方便后面使用;可以在正则表达式内部使用,也可以在外部使用;
捕获组有2种,一种是捕获性分组,另一种是 非捕获性分组;
我们先来看看捕获性的分组语法如下:
捕获组分组语法:(Expression)
我们都知道中括号是表示范围内选择,大括号表示重复次数,小括号的含义是允许重复多个字符;
捕获性分组的编号规则:编号是按照”(”出现的顺序,从左到右,从1开始进行编号;
比如如下代码:
// 分组的列子
console.log(/(longen){2}/.test("longen")); // false
console.log(/(longen){2}/.test("longenlongen")); //true
// 分组的运用 RegExp.$1 获取小括号的分组
var str = 11122;
/(\d+)/.test(str);
console.log(RegExp.$1); // 11122
// 使用replace替换 使用分组 把内容替换
var num = "11 22";
var n = num.replace(/(\d+)\s*(\d+)/,"$2 $1");
console.log(n); // 22 11
反向引用
反向引用标识由正则表达式中的匹配组捕获的子字符串。每个反向引用都由一个编号或名称来标识;并通过 ”\编号” 表示法来进行引用;
// 反向引用
console.log(/(longen)\1/.test("longen")); // false
console.log(/(longen)\1/.test("longenlongen")); // true
理解非捕获性分组
并不是所有分组都能创建反向引用,有一种分组叫做非捕获性分组,它不能创建反向引用,要创建一个非捕获性分组,只要在分组的左括号的后面紧跟一个问号与冒号就ok;非捕获分组的含义我们可以理解为如下:子表达式可以作为被整体修饰但是子表达式匹配的结果不会被存储;如下:
// 非捕获性分组
var num2 = "11 22";
/#(?:\d+)/.test(num2);
console.log(RegExp.$1); //""
我们再来看下使用 非捕获性分组来把页面上的所有标签都去掉,如下代码:
// 把页面上所有的标签都移除掉
var html = "<p><a href='http://baidu.com'>我来测试下</a>by <em>龙恩</em></p>";
var text = html.replace(/<(?:.|\s)*?>/g, "");
console.log(text); // 我来测试下by 龙恩
如上:我们来分析下:正则/<(?:.|\s)?>/g 的含义是:g是修饰符,全局匹配的含义;使用非捕获性分组?: 的含义是 子表达式可以作为被整体修饰但是子表达式匹配的结果不会被存储;因此:正则/<(?:.|\s)?>/g 的含义变为:匹配以< 开头 及 > 结束的所有字符;(?:.|\s)? 含义是:. 代表任意字符,| 含义是或者的意思,\s 是匹配空格的意思;号修饰符的含义是零个或者多个的意思;后面的?(问号)代表可匹配,可不匹配的含义;优先是可匹配;总起来的意思是:全局匹配字符串html 中的 以<开头 以>结尾的所有字符 替换成 空字符串,因此留下来就是文本;当然我们使用捕获性分组也可以得到同样的结果~
反向引用详细讲解
捕获性分组取到的内容,不仅可以在正则表达式外部通过程序进行引用,也可以在正则表达式内部进行引用,这种引用方式就叫做反向引用。
反向引用的作用是:是用来查找或限定重复,查找或限定指定标识配对出现等。
捕获性分组的反向引用的写法如:\number
Number是十进制数字,即捕获组的编号。
反向引用的匹配原理
捕获分组在匹配成功时,会将子表达式匹配到的内容,保存到内存中一个以数字编号的组里,可以简单的认为是对一个局部变量进行了赋值,这时就可以通过反向引用,引用这个局部变量的值。一个捕获分组在匹配成功之前,它的内容可以是不确定的,一旦匹配成功了,它的内容就确定了,反向引用的内容也就确定了。
比如如下代码:
var str = "longenaabcd";
console.log(str.match(/([ab])\1/)[0]);//aa
代码分析:对于如上代码中的正则 /([ab])\1/, 捕获组中子表达式[ab];可以匹配a,也可以匹配b,但是如果匹配成功的话,那么它的反向引用也就确定了,如果捕获分组匹配到的是a,那么它的反向引用就只能匹配a,如果捕获分组匹配到的是b,那么它的反向引用就只能匹配到b;\1的含义是 捕获分组匹配到是什么,那么它必须与捕获分组到是相同的字符;也就是说 只能匹配到aa或者bb才能匹配成功;
该正则匹配的过程我们可以来分析下:
字符串匹配正则/([ab])\1/, 在位置0处开始匹配,0处字符是l,很明显不满足,把控制权就交给下一个字符,一直到第6个字符,才匹配到a,匹配成功,把控制权交给\1, 也就是反向引用和分组中是相同的字符,因此也匹配a,字符串中下一个字符也是a,因此匹配成功,因此整个表达式找到匹配的字符,匹配的位置开始于6,结束与8;我们再可以匹配b,原理和上面一样,这里就不再多解释了;