JS正则表达式否定匹配(正向前瞻)
无可厚非造句
引⾔
JS 正则表达式是 JS 学习过程中的⼀⼤难点,繁杂的匹配模式⾜以让⼈头⼤,不过其复杂性和其学习难度也赋予了它强⼤的功能。⽂章从JS 正则表达式的正向前瞻说起,实现否定匹配的案例。本⽂适合有⼀定 JS 正则表达式基础的同学,如果对正则表达式并不了解,还需先学习基础再来观摩这门否定⼤法。
⼀、标签过滤需求
不知道⼤家在写JS有没有遇到过这样的情况,当你要处理⼀串字符串时,需要写⼀个正则表达式来匹配当中不是 XXX 的⽂本内容。听起来好像略有些奇怪,匹配不是 XXX 的内容,不是 XXX 我匹配它⼲嘛啊,我要啥匹配啥不就完了。你还别说,这个玩意还真的有⽤,不管你遇没遇到过,反正我是遇到了。具体的需求例如:当你收到⼀串HTML代码,需要对这⼀串HTML代码过滤,将⾥⾯所有的⾮<p>标签都改为<p>。这⾥肯定有不少同学就要嫌弃了,“将所有标签都改为<p>,那就把任意标签都改为<p>不就完了?”,于是乎⼀⾏代码拍脑袋⽽⽣:
1var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';
2var reg = /<(\/?).*?>/g;
3var newStr = place(reg, "<$1p>");
4 console.log(newStr);//<p>,<p>,<p>,<p>,</p>,</p>,</p>,</p>
注意这个⽅法中有⼀个引⽤符 “$1” ,这个的意思引⽤正则的表达式的第1个分组,可以⽤$N来表⽰在正则表达式中的第N个捕获的引⽤。就那上⾯的例⼦来说,"(\/?)"这个⼀个表达式的含义是,"\/"这个字符出现0次或者1次,⽽$1这个引⽤呢就相当于和“\/”这个字符门当户对的⼤闺⼥,她已下定决⼼此⽣⾮"\/"不嫁。所以当匹配到有⼀个“\/”的时候,$1这个引⽤就把它捕获下来,从现在起,你的就是我的,我的就是你的啦,因此$1等价于"(\/?)"所匹配到的字符;反之如果没有匹配到"\/"这个字符,那$1这个引⽤就得空守闺房,独⽴熬过⼀个⼜⼀个漫长的夜晚,因为它内⼼极度的空虚,所以$1就等价于""(也就是空串)。
这⾥先聊了聊引⽤和捕获的概念,因为后⾯还会⽤到它。那么话说回来,刚才那⼀串正则,不是已经完美的实现了需求了吗?还研究什么否定匹配啊?各位看官别急,且听⼩⽣慢慢道来。我们都知道,需求这个东西,肯定是会改嘀(◐ˍ◑)。现在改⼀改需求:当你收到⼀串HTML 代码,需要对这⼀串HTML代码过滤,将⾥⾯所有的⾮<p>或者<div>标签都改为<p>。WTF?这算哪门⼦需求?话说我当时也是这种反应。我们现在分析⼀下这个需求到底要⼲嘛,也就是说,保留原HTML代码中的<p>
和<div>,将其他标签统⼀修改为<p>。咦...这下可不好弄了,刚才那串代码看上去貌似⾏不通了。所以说这时候就只能⽤排除法了,排除掉<p>和<div>,替换掉其他的标签。那么问题也就来了,如何排除?
⼆、正则前瞻表达式
在正则表达式当中有个东西叫做前瞻,有的管它叫零宽断⾔:
表达式名称描述
(?=exp)正向前瞻匹配后⾯满⾜表达式exp的位置
(?!exp)负向前瞻匹配后⾯不满⾜表达式exp的位置梦见钥匙断了
(?<=exp)正向后瞻匹配前⾯满⾜表达式exp的位置(JS不⽀持)
(?<!exp)负向后瞻匹配前⾯不满⾜表达式exp的位置(JS不⽀持)
由于 JS 原⽣不⽀持后瞻,所以这⾥就不研究它了。我们来看看前瞻的作⽤:
1var str = 'Hello, Hi, I am Hilary.';
2var reg = /H(?=i)/g;
3var newStr = place(reg, "T");
4 console.log(newStr);//Hello, Ti, I am Tilary.
炸千子的做法 在这个DEMO中我们可以看出正向前瞻的作⽤,同样是字符"H",但是只匹配"H"后⾯紧跟"i"的"H"。就相当于有⼀家公司reg,这时候有多名"H"⼈员前来应聘,但是reg公司提出了⼀个硬条件是必须掌握"i"这项技能,所以"Hello"就⾃然的被淘汰掉了。
那么负向前瞻呢?道理是相同的:
1var str = 'Hello, Hi, I am Hilary.';
2var reg = /H(?!i)/g;
3var newStr = place(reg, "T");
4 console.log(newStr);//Tello, Hi, I am Hilary.
搞笑英文 在这个DEMO中,我们把之前的正向前瞻换成了负向前瞻。这个正则的意思就是,匹配"H",且后⾯
不能跟着⼀个"i"。这时候"Hello"就可以成功的应聘了,因为reg公司修改了他们的招聘条件,他们说"i"这门技术会有损公司的企业⽂化,所以我们不要了。
三、前瞻的⾮捕获性
说到这⾥,让我们回到最初的那个需求,让我们先⽤负向前瞻来实现第⼀个需求:将所有⾮<p>标签替换为<p>。话说同学们刚学完了负向前瞻,了解到了JS 的博⼤精深,⼼中暗⽣窃喜,提笔⼀挥:
1var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';
2var reg = /<(\/?)(?!p)>/g;
3var newStr = place(reg, "<$1p>");
4 console.log(newStr);//<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>
What?为什么不起作⽤呢?说好的否定⼤法呢?这⾥就得聊⼀聊前瞻的⼀个特性,前瞻是⾮捕获性分组,什么玩意是⾮捕获性分组呢?还记得前⾯那位⾮"\/"不嫁的⼤闺⼥$1吗,⼈家为什么那么⼀往情深,是因为她早已将"\/"的⼼捕获了起来,⽽前瞻却是⾮捕获性分组,也就是你捕获不到⼈家。也就是说⽆法通过引⽤符"\n"或者"$n"来对其引⽤:
1var str = 'Hello, Hi, I am Hilary.';
2var reg = /H(?!i)/g;
3var newStr = place(reg, "T$1");
4 console.log(newStr);//T$1ello, Hi, I am Hilary.
注意其中输出的语句,前⾯我们可以看到,如果引⽤符没有匹配到指定的字符,那么就会显⽰空串"",可是这⾥是直接显⽰了整个引⽤符"$1"。这是因为前瞻表达式根本就没有捕获,没有捕获也就没有引⽤。
⾮捕获性是前瞻的⼀个基本特征,前瞻的另外⼀个特性是不吃字符,意思就是前瞻的作⽤只是为了匹配满⾜前瞻表达式的字符,⽽不匹配前瞻本⾝。也就是说前瞻不会修改匹配位置,这么说我⾃⼰都觉得晦涩,我们还是来看看代码吧︽⊙_⊙︽:
1var str = 'Hello, Hi, I am Handsome Hilary.';
2var reg = /H(?!i)e/g;
孩子优点3var newStr = place(reg, "T");
4 console.log(newStr);//Tllo, Hi, I am Handsome Hilary.
注意观察输出的字符串,前瞻的作⽤仅仅是匹配出满⾜前瞻条件的字符"H",匹配出了"Hello"和"Handsome"当中的H,但同时前瞻不会吃字符,也就是不会改变位置,接下来还是会紧接着"H"开始继续往下匹配,这时候匹配条件是"e",于是"Hello"中的"He"就匹配成功了,⽽"Handsome"中的"Ha"则匹配失败。
1. /H(?!i)/g --> H ello, Hi, I am H andsome Hilary.
2. /H(?!i)e/g --> He llo, Hi, I am Handsome Hilary.
四、⽤前瞻实现标签过滤
既然前瞻是⾮捕获性的,⽽且还不吃字符,那么了解到这些特征后我们现在终于可以完成我们的需求了吧?因为它不吃字符,所以具体的标签字符还得由我们⾃⼰来吃:
1var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';
2var reg = /<(\/?)(?!p|\/p).*?>/g;
3var newStr = place(reg, "<$1p>");
4 console.log(newStr);//<p>,<p>,<p>,<p>,</p>,</p>,</p>,</p>
诗王是谁 聊了这么半天,终于解决了咱们的第⼀个需求,注意当中的".*?",虽然这⾥匹配的是任意字符,但是别忘了,有了前⾯的负向前瞻,我们匹配到的都是后⾯不会紧跟着"p"或者"/p"的字符"<"。
/<(?!p|\/p)/g --> <div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>
注意在这⾥⽤了⼀个管道符"|"来匹配"\/p",虽然前⾯已经有了"(\/?)"匹配结束符,但是切记这⾥的分组选项不能省略,因为这⾥的量词是可以出现0次。我们来试想⼀下如果⽤"/<(\/?)(?!p).*?>/g"来匹配"</p>"这个标签,当量次匹配到"/"的时候,发现可以匹配,便记录下来,然后对"/"进⾏前瞻判断,但是后⾯却接着⼀个"p"于是不能匹配,丢掉;注意这时"(\/?)"的匹配字符是0个,于是乎转⽽对"<"进⾏前瞻判断,这⾥的" <"后⾯紧接着的是"/p"⽽不是"p",于是乎成功匹配,所以这个标签会被替换掉;⽽且,由于之前的分组匹配到的字符是0个,也就是没有匹配到字符,所以后⾯的引⽤是个空串。
1var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';
2var reg = /<(\/?)(?!p).*?>/g;
3var newStr = place(reg, "<$1p>");
4 console.log(newStr);//<p>,<p>,<p>,<p>,</p>,</p>,<p>,</p>
完成了第⼀个过滤需求,那么第⼆个过滤需求也就⾃然⽽然的完成了,这时候,就算有那么五六个标签需要保留,咱们也不⽤怕了:
1var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';
2var reg = /<(\/?)(?!p|\/p|div|\/div).*?>/g;
3var newStr = place(reg, "<$1p>");
4 console.log(newStr);//<div>,<p>,<p>,<p>,</p>,</p>,</p>,</div>
总结
美文文章
JS 的正向前瞻只是正则表达式当中⼀部分,没相当就这么⼀部分还有着这么多的奥妙呢。
在使⽤正向前瞻,我们需要注意的是:
前瞻是⾮捕获性的:其特征是⽆法引⽤。
前瞻不消耗字符:前瞻只匹配满⾜前瞻表达式的字符,⽽不匹配其本⾝。
话说,咱们的需求就到这了吗?真的就完了吗?同学们觉得过瘾不?有些同学觉得可能差不多了,需要消化⼀段时间,但是绝对有那么⼀部分同学还完全没过瘾呢,没关系,最后留给⼤家⼀道思考题,截⽌到我写这篇博客为⽌,我还没有想出⼀个解决办法呢( •_•)。
需求如下:当你收到⼀串HTML代码,需要对这⼀串HTML代码过滤,将⾥⾯所有的⾮<p>或者<div>标签都改为<p>,并且保留所有标签的
样式,要求只使⽤⼀个正则表达式,例如:
//输⼊
var input = '<div class="beautiful">,<p class="provocative">,<h1 class="attractive" id="header">,<span class="xy">,</span>,</h1>,</p>,</div>'; //输出
刚怀孕症状var output = '<div class="beautiful">,<p class="provocative">,<p class="attractive" id="header">,<p class="xy">,</p>,</p>,</p>,</div>';
如果你有好的解决⽅案,欢迎在评论区留⾔,⼤家⼀起学习。
参考⽂献:
devinran ——
Barret Lee ——