引言

  JS 正则表达式是 JS 学习过程中的一大难点,繁杂的匹配模式足以让人头大,不过其复杂性和其学习难度也赋予了它强大的功能。文章从 JS 正则表达式的正向前瞻说起,实现否定匹配的案例。本文适合有一定 JS 正则表达式基础的同学,如果对正则表达式并不了解,还需先学习基础再来观摩这门否定大法。


一、标签过滤需求

  不知道大家在写JS有没有遇到过这样的情况,当你要处理一串字符串时,需要写一个正则表达式来匹配当中不是 XXX 的文本内容。听起来好像略有些奇怪,匹配不是 XXX 的内容,不是 XXX 我匹配它干嘛啊,我要啥匹配啥不就完了。你还别说,这个玩意还真的有用,不管你遇没遇到过,反正我是遇到了。具体的需求例如:当你收到一串HTML代码,需要对这一串HTML代码过滤,将里面所有的非<p>标签都改为<p>。这里肯定有不少同学就要嫌弃了,“将所有标签都改为<p>,那就把任意标签都改为<p>不就完了?”,于是乎一行代码拍脑袋而生:

     var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';
var reg = /<(\/?).*?>/g;
var newStr = str.replace(reg, "<$1p>");
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 原生不支持后瞻,所以这里就不研究它了。我们来看看前瞻的作用:

     var str = 'Hello, Hi, I am Hilary.';
var reg = /H(?=i)/g;
var newStr = str.replace(reg, "T");
console.log(newStr);//Hello, Ti, I am Tilary.

  在这个DEMO中我们可以看出正向前瞻的作用,同样是字符"H",但是只匹配"H"后面紧跟"i"的"H"。就相当于有一家公司reg,这时候有多名"H"人员前来应聘,但是reg公司提出了一个硬条件是必须掌握"i"这项技能,所以"Hello"就自然的被淘汰掉了。

  那么负向前瞻呢?道理是相同的:

     var str = 'Hello, Hi, I am Hilary.';
var reg = /H(?!i)/g;
var newStr = str.replace(reg, "T");
console.log(newStr);//Tello, Hi, I am Hilary.

  在这个DEMO中,我们把之前的正向前瞻换成了负向前瞻。这个正则的意思就是,匹配"H",且后面不能跟着一个"i"。这时候"Hello"就可以成功的应聘了,因为reg公司修改了他们的招聘条件,他们说"i"这门技术会有损公司的企业文化,所以我们不要了。

三、前瞻的非捕获性

  说到这里,让我们回到最初的那个需求,让我们先用负向前瞻来实现第一个需求:将所有非<p>标签替换为<p>。话说同学们刚学完了负向前瞻,了解到了JS的博大精深,心中暗生窃喜,提笔一挥:

     var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';
var reg = /<(\/?)(?!p)>/g;
var newStr = str.replace(reg, "<$1p>");
console.log(newStr);//<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>

  What?为什么不起作用呢?说好的否定大法呢?这里就得聊一聊前瞻的一个特性,前瞻是非捕获性分组,什么玩意是非捕获性分组呢?还记得前面那位非"\/"不嫁的大闺女$1吗,人家为什么那么一往情深,是因为她早已将"\/"的心捕获了起来,而前瞻却是非捕获性分组,也就是你捕获不到人家。也就是说无法通过引用符"\n"或者"$n"来对其引用:

     var str = 'Hello, Hi, I am Hilary.';
var reg = /H(?!i)/g;
var newStr = str.replace(reg, "T$1");
console.log(newStr);//T$1ello, Hi, I am Hilary.

  注意其中输出的语句,前面我们可以看到,如果引用符没有匹配到指定的字符,那么就会显示空串"",可是这里是直接显示了整个引用符"$1"。这是因为前瞻表达式根本就没有捕获,没有捕获也就没有引用。

  非捕获性是前瞻的一个基本特征,前瞻的另外一个特性是不吃字符,意思就是前瞻的作用只是为了匹配满足前瞻表达式的字符,而不匹配前瞻本身。也就是说前瞻不会修改匹配位置,这么说我自己都觉得晦涩,我们还是来看看代码吧︽⊙_⊙︽:

     var str = 'Hello, Hi, I am Handsome Hilary.';
var reg = /H(?!i)e/g;
var newStr = str.replace(reg, "T");
console.log(newStr);//Tllo, Hi, I am Handsome Hilary.

  注意观察输出的字符串,前瞻的作用仅仅是匹配出满足前瞻条件的字符"H",匹配出了"Hello"和"Handsome"当中的H,但同时前瞻不会吃字符,也就是不会改变位置,接下来还是会紧接着"H"开始继续往下匹配,这时候匹配条件是"e",于是"Hello"中的"He"就匹配成功了,而"Handsome"中的"Ha"则匹配失败。

1. /H(?!i)/g --> Hello, Hi, I am Handsome Hilary.
2. /H(?!i)e/g --> Hello, Hi, I am Handsome Hilary.

四、用前瞻实现标签过滤

  既然前瞻是非捕获性的,而且还不吃字符,那么了解到这些特征后我们现在终于可以完成我们的需求了吧?因为它不吃字符,所以具体的标签字符还得由我们自己来吃:

     var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';
var reg = /<(\/?)(?!p|\/p).*?>/g;
var newStr = str.replace(reg, "<$1p>");
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个,也就是没有匹配到字符,所以后面的引用是个空串。

     var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';
var reg = /<(\/?)(?!p).*?>/g;
var newStr = str.replace(reg, "<$1p>");
console.log(newStr);//<p>,<p>,<p>,<p>,</p>,</p>,<p>,</p>

  完成了第一个过滤需求,那么第二个过滤需求也就自然而然的完成了,这时候,就算有那么五六个标签需要保留,咱们也不用怕了:

     var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';
var reg = /<(\/?)(?!p|\/p|div|\/div).*?>/g;
var newStr = str.replace(reg, "<$1p>");
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="sexy">,</span>,</h1>,</p>,</div>';
//输出
var output = '<div class="beautiful">,<p class="provocative">,<p class="attractive" id="header">,<p class="sexy">,</p>,</p>,</p>,</div>';

  如果你有好的解决方案,欢迎在评论区留言,大家一起学习。

参考文献:

  devinran —— 《相爱相杀——正则与浏览器的爱恨情仇》

  Barret Lee —— 《进阶正则表达式》

JS 正则表达式否定匹配(正向前瞻)的更多相关文章

  1. js正则表达式中的正向肯定预查和正向否定预查

    对于没有使用过这几个表达式的人,应该对这个概念都有点不太理解,下面就以实际例子说明这几个表达式的用户. 一.?:pattern——匹配检验:会作为匹配校验,是一个非获取匹配,并出现在匹配字符结果里面, ...

  2. js正则表达式中匹配反引号

    直接用反引号就可以了~ /`[\W\w\*]+`\.`[\W\w\*]+`/

  3. [转]js 正则表达式

    一.正则表达式中包括的元素 1.原子(普通字符:a-z A-Z 0-9 .原子表. 转义字符) 2.元字符 (有特殊功能的字符) 3.模式修正符 (系统内置部分字符 i .m.S.U…) 二.正则表达 ...

  4. js正则表达式匹配字符串与优化过程

    前言 有时候需要实现对js源文件中的url字符串做拦截预处理,或者前端js语法高亮,或者需要对动态加载的关键源码做混淆保护,在某些步骤实现之前,有一个步骤是需要提炼出所有的合法字符串. 目标:检测源文 ...

  5. javascript 正则表达式之分组与前瞻匹配详解

    本文主要讲解javascript 的正则表达式中的分组匹配与前瞻匹配的,需要对正则的有基本认识,本人一直对两种匹配模棱不清.所以在这里总结一下,如有不对,还望大神指点. 1.分组匹配: 1.1捕获性分 ...

  6. 正则表达式 js 怎么获取匹配的变量

    正则表达式 js 怎么获取匹配的变量 $0 ~ $9 refs xgqfrms 2012-2020 www.cnblogs.com 发布文章使用:只允许注册用户才可以访问!

  7. JS高级---识别正则表达式是否匹配

    识别正则表达式是否匹配 console.log(/[a-zA-Z]+/.test("hello")); console.log(/./.test("除了回车换行以为的任意 ...

  8. javascript -- js正则表达式

    正则表达式可以:  1.测试字符串的某个模式.例如,可以对一个输入字符串进行测试,看在该字符串是否存在一个电话号码模式或一个信用卡号码模式.这称为数据有效性验证 2.替换文本.可以在文档中使用一个正则 ...

  9. JS正则表达式进阶

    贪婪量词   惰性量词    支配量词                      描述--------------------------------------------------------- ...

随机推荐

  1. MySQL索引统计信息更新相关的参数

    MySQL统计信息相关的参数: 1. innodb_stats_on_metadata(是否自动更新统计信息),MySQL 5.7中默认为关闭状态 仅在统计信息配置为非持久化的时候生效. 也就是说在i ...

  2. Linux学习---vi/vim命令

    Vim是从 vi 发展出来的一个文本编辑器.代码补完.编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用. 所以本文直接用Vim编辑器 基本上 vi/vim 共分为三种模式,分别是命令模式( ...

  3. JavaWeb开发之HttpServletResponse

    1. HttpServletResponse简介 Web服务器回送给Web客户端的HTTP响应消息分为三个部分:状态行,响应消息头,响应体. Servlet API中定义了ServletRespons ...

  4. 第三章 Struts2配置详解

    3.1 Struts2执行过程    1.获取Struts2资源    2.在应用程序中导入Struts2的类库    3.在web.xml中配置StrutsPrepareAndExecuteFilt ...

  5. 串口屏与触摸屏人机界面组态软件HMIMaker介绍

    串口屏与触摸屏人机界面组态软件HMIMaker介绍 触摸屏人机界面组态软件HMIMaker,是一款基于ARM架构的嵌入式控制系统开发的嵌入式软件,专业应用于触摸屏的二级界面开发,具有单片机协议,mod ...

  6. 谈谈一些有趣的CSS题目(十五)-- 谈谈 CSS 关键字 initial、inherit 和 unset

    开本系列,谈谈一些有趣的 CSS 题目,题目类型天马行空,想到什么说什么,不仅为了拓宽一下解决问题的思路,更涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题 ...

  7. mysql 优化方法

    1.选取最适用的字段属性 MySQL可以很好的支持大数据量的存取,但是一般说来,数据库中的表越小,在它上面执行的查询也就会越快.因此,在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设得尽 ...

  8. Android之AIDL知识总结

    1.AIDL介绍 AIDL是一个缩写,全称是Android Interface Definition Language,翻译为Android接口定义语言.主要用于线程之间的通信,本文主要以不同应用之间 ...

  9. PHP 分支与循环

    一.概述: 上面一章我们讲解了PHP当中的运算符和表达式,通过上面的知识点我们就可以完成一些基本的运算操作了.但是涉及到一些比较复杂的逻辑,分支与循环就必不可少了.通过分支和循环的结合使用可以使业务更 ...

  10. split()方法

    split()方法用于把一个字符串分隔成字符串数组. 它有两个参数: separator:从参数指定的地方分隔字符串,必需: howmany:该参数可指定返回的数组的最大长度.如果设置了该参数,返回的 ...