Fork me on GitHub

读懂那些正则表达式:捕获元与非捕获元

想读懂世间所有的那些变态正则表达式?做梦,年纪轻轻,想啥呢,尽自己最大努力学就行

引子

JS学了用了也快一两年了,对象啥的找到了也会用了,继承啥的也入门了,但看别人的框架代码,总是会随时卡壳,有一个重大的原因,就是那看不懂的一串串火星文字(正则表达式),学习吗,就是查缺补漏,不怕你不懂,就怕你觉得自己全懂了。说正事之前,先推荐一款软件:RegexBuddy,无论是做正则的测试还是过程的研究,都是一款利器。

知识汇总

语法复习,重点三块知识:

  1. 贪婪匹配(? 0=<x<=1, x>=1,* any)与那些相关的限定符({n},{n,m});
  2. 特殊字符:^ $ . * ? = ! : | / ( ) [ ] { }
    ,火星文,基本就是他们组成的,要想匹配字符的本意,字面量表达式在特殊字符前加,用new声明的需要加\;
  3. 非捕获元字符:?:,?=(正向预查),?!(负向预查);
  4. 回溯引用,前面的字符匹配基本都和他有关;
  5. 其他,什么字符边界啊,括号啊,中括号啊,等等 ;

正则表达式解析原理:这个不算我等渣暂时能写出来的,推荐一篇

层层递进剖析

贪婪匹配

先理解贪婪匹配,正则表达式的日常应用基本也就满足了,在菜鸟教程的语法开篇就已经提的很详细了,比如有一个regex:/Chapter[1-9]/,这个字符串我们只能匹配到Chapter1-Chapter9,也就是Chapter的一级标题,但我们想匹配到二级或者三级标题怎么办,这里就用到了贪婪匹配,就是在目标字符串中最大化的匹配结果,将前面的regex:/Chapter[1-9]/改成/Chapter[1-9] /,这样我们就能匹配Chapter1,Chapter12,Chapter123,但如果我们将其改为/Chapter[1-9]?/,这个无论/Chapter后面输入多少个数字,都只能最多匹配一个数字,这里就是Chapter1,但与最初的表达式不同的是,这个表达式也能匹配裸的Chapter,这就是所谓(X?),问号前面的X可出现0次或者1次,当我们将其改为/Chapter[1-9]星号(避开markdown语法)/,这个最后可以达到?和 共同的结果,也就是所谓的,x出现任意次数。上面这些我们也可以通过[n,m]即n=<x<=m来匹配x出现的次数,{0,1}实现的效果等价与?,而{1,}等价于 ,{0,}等价于星号(避开markdown语法)。

懒惰匹配

与贪婪匹配成对的另一个叫懒惰匹配,在前面出现的所有贪婪匹配后面加上一个?,这样整个表达式就成了懒惰匹配,可以理解为最小化匹配,比如/Chapter[1-9] /匹配Chapter12345的结果是Chapter12345,但/Chapter[1-9] ?/匹配的结果就是Chapter1;/Chapter[1-9]{2,4}/匹配结过是Chapter1234,而/Chapter[1-9]{2,4}?/结过是Chapter1234,这就是所谓的最小化去匹配结果,取下限,通常称为懒惰模式。

捕获元与非捕获元

以前看到什么?:,?=,?!,用的少,也就没留意,最近大面积灾荒,经常看到,甚是恐惧,以至于前面在读gulp里面碰见个regex表达式:/-[0-9a-f]{8,10}-?/(匹配app-7ef5d9ee29.css这一类表达式中的md5值),就一头栽进去,’-?’到底又有什么特殊的含义,最后才发现,那TMD就是一个贪婪匹配,你个蠢货,但确实搞不懂源码作者在想啥,也许是我没碰到app-7ef5d9ee29-any.css这样的文件名,要不非得多加个’-?’干啥,让我直往坑里跳。
回到正题,先搞懂什么叫捕获组,概括起来就是,用括号如‘(pattern)’这样的形式,匹配满足括号中的,就是一个捕获组。先看一张来自于菜鸟教程的定义:图片描述
四种形式,加?和不加有什么区别,区别就是捕获元与非捕获元,表现形式就是用exec方法去匹配,捕获组会单纯保存在一组变量中。理论太枯燥,直接看例子,来源于JS高设page106,略有改动:

var str ='mom and dad and baby';
var pattern = /mom( and dad( and baby))/; //捕获元形式
var pat= /mom(?: and dad(?: and baby))/; //非捕获元形式
var mat = pattern.exec(str);
var match = pat.exec(str);
console.log(mat);
console.log(match);

图片描述
看着devtools打印的结果,是不是有点眉目,是的,匹配的结果虽一致,但捕获组匹配时,将满足捕获元形式的单元单独保存为一个匹配结果,而非捕获元不单独保存,只保存完整匹配结果。我们常见的Regexp.$1,$2其实就是对捕获组结果的引用。
捕获元与非捕获元搞懂了,那(?:pattern)与(?=pattern)啥区别呢,答案,两个区别。区别一:前者匹配的结果包含捕获元,后者匹配的结果则不包含;区别二:前者匹配捕获元时,消耗字符(索引),而后者不消耗。还是来看一个例子:

var str ='ababa';
var pattern = /ab(?:a)/g;
var pat=  /ab(?=a)/g;
var mat = pattern.exec(str);
var match = pat.exec(str);
console.log(mat);
console.log(match);
 mat = pattern.exec(str); //全局模式,第二次匹配
 match = pat.exec(str); //全局模式,第二次匹配
console.log(mat);
console.log(match);

图片描述

从上面代码运行的截图可以看出区别一,也就是(?:pattern)的形式的捕获元匹配的结果会保存在最终的结果中,而(?=pattern);区别二看的不是很明显,这时我们需要依靠RegexBuddy,这个过程中到底发生了什么?看运行截图,如果你够仔细,你可以发现区别,第一次匹配到结果,开始第二次匹配时,?:是从字符索引3开始,而?=是从2开始,这就是前面所说的消耗字符与不消耗字符。
图片描述
好了,最后一个问题,整箱预查(?=pattern)与负向预查(?!pattern),其实从中文单纯来理解负向预查,是会带来歧义的。这里的负向其实单单就是正向预查的取反,即要匹配的字符不满足捕获的条件,才能匹配到结果。
如果文章有什么描述不正确或模糊的地方,还请及时指正。
好了,先就说这么多嘛,虽然是无业游民,那也应该有享受周末的权利吧,毕竟找工作的压力那么大,还是要自我缓解一下,see you last week。

-------------本文结束感谢您的阅读-------------