硬刚正则表达式的心得总结

前端之家收集整理的这篇文章主要介绍了硬刚正则表达式的心得总结前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
近几日对自己一直不太擅长的正则表达式做了一次全面的扫盲。心疼自己之余还是有一些收获吧,在这里做一个比较零散的总结,整理一些对理解正则比较有利的点。

一、"?"

你没有看错,就是黑人问号中的问号,这个字符在正则里面算是一个入门中很容易被带偏的点了。首先要知道什么是正则中的量词。

1.量词

在正则中,通常要表示一个表达式匹配的数量,这个时候量词就登场了。
主要会使用以下几个量词

/(\w)*/.exec(str)  // 匹配任意次
/(\w)+/.exec(str)  // 匹配一次到多次
/(\w)?/.exec(str)  // 匹配零到一次(记住这里问号的用法!)
/(\w){2,4}/.exec(str) // 匹配两次到四次
/(\w){2,}/.exec(str)  // 匹配两次以上

我们可以发现,在这里"?"作为一个量词来使用,表示匹配零到一次

接下来要理解下一个概念:贪婪匹配

2.贪婪匹配

搜了一下wiki,貌似没有相关的词条,通俗的解释,贪婪匹配模式下,会尽可能多地匹配满足条件的字符。而正则默认是贪婪模式的。

举个例子。比如我要匹配"suuuuuuuuuuck"字符中的s和k中间的字符。并没有什么问题。

let result = "suuuuuuuuuuck".match(/s(.*)k/)[1]
// uuuuuuuuuuc

但是我现在要搞事情,要你在"suuuuuuuuuuck duck"字符串中匹配相同的字段,同样的表达式会匹配到以下的结果,因为此时的正则是贪婪的,它一定会匹配到无法匹配的时候才休止。

// uuuuuuuuuuck duc

这时候就需要手动开启非贪婪模式了

let result = "suuuuuuuuuuck duck".match(/s(.*?)k/)[1]
// uuuuuuuuuuc

区别是在量词后加了个问号,这时候该捕获组就算是开启了非贪婪模式了。

按照上面的理解,如果你是一个新手,肯定会有所疑惑,量词(*)后面跟着一个量词(?),这是什么鬼意思,这么反人类的?

其实,这里就涉及到"?"的第二个用法了,即当它跟在一个量词背后的时候,表示该表达式开启了非贪婪模式,即对表达式尽可能少地匹配结果。所以,对应的,配合量词使用,会产生以下结果。

  • "*?": 可以匹配任意多次,但是尽量少匹配。
  • "+?": 至少必须匹配一次,但是尽量少匹配。
  • "{m,n}?": 至少必须匹配m次,最多只能匹配n次,但是尽量少匹配。
  • "{m,}?": 至少必须匹配m次,但是尽量少匹配。
思考题:所以,"??" 应该如何匹配呢?

二、捕获组

正则匹配除了验证一个字符串是否符合条件外,还可以从中提取我们所需要的信息。比如,我们得到了一个"新中国成立于1949-10-01"的字符串,作为一个爱国人士,我们要把这个年月日提取出来谨记于心。所以我写了一个正则,获得的结果如下

这里提取的操作就需要通过小括号进行捕获。正则会默认对捕获组分配组数。

"新中国成立于1949-10-01".match(/(\d{4})-(\d{2})-(\d{2})/)
// ["1949-10-01","1949","10","01",index: 6,input: "新中国成立于1949-10-01",groups: undefined]

我们也可以忽略某些分组"(?:exp)",这样正则就不会为为其分配组数。

"新中国成立于1949-10-01".match(/(\d{4})-(\d{2})-(?:\d{2})/)
// ["1949-10-01",groups: undefined]

假如我们有一个叠词判断的需求,验证一个词语是不是"ABA"格式的,我们可以这么做

// 首先汉字的unicode范围是\u4e00-\u9fa5
// 这里我们首先对第一个字符进行了捕获,组数为1
// 然后我们后面通过"\1"的方式去复用捕获组,这样就意味着匹配到了相同的字符,也就达到了限制的目的。

/([\u4e00-\u9fa5])[\u4e00-\u9fa5]\1/.test("是不是")
// 当然是true

要记住下标对人类来说还是挺麻烦的,可以说完全没啥可读性,当然正则也提供了为分组命名的方式

"新中国成立于1949-10-01".match(/(?<year>\d{4})-(?<month>\d{2})-(?<date>\d{2})/)
// 我们可以发现,这时候捕获组不仅拥有组数,同时groups属性不为空了。
// ["1949-10-01",groups: {…}]
// 展开groups 是这样的
// {year: "1949",month: "10",date: "01"}

/** 当然命名捕获组也是可以使用的 */
// (?<name>exp) 命名捕获组
// \k<name> 引用

// 还是叠词的那个例子
/(?<thx>[\u4e00-\u9fa5])[\u4e00-\u9fa5]\k<thx>/.test("是不是")

现在有一个需求,匹配出英文语句"I'm singing while you're dancing"中所有带有ing后缀的单词(不包含ing)。要想拿到danc 和 sing,我们需要用到零宽断言。

三、零宽断言

零宽断言用于查找某些内容之前或之后的东西,只指定一个位置,本身并不占据字符,这也是为什么我们称之为零宽度

对于表达式表示肯定,我们称之为正向,反之称之为负向,(注意,这里的正负指的是对条件的肯定和否定,而不是匹配的方向。)

而对于匹配的方向而言,我们有另外一种称呼,对向后匹配的称之为预测先行,向前匹配的称之为回顾后发

所以,对应的四种组合分别是

  • (?=exp) 零宽度正预测先行断言(断言自身出现的位置后面能匹配exp)
  • (?!exp) 零宽度负预测先行断言(断言自身出现的位置后面不能匹配exp)
  • (?<=exp) 零宽度正回顾后发断言(断言自身出现的位置前面能匹配exp)
  • (?<!exp) 零宽度负回顾后发断言(断言自身出现的位置前面不能匹配exp)

目前的js引擎对回顾后发断言的实现还不完全,就我所知在chrome能成功使用,但是在nodejs环境下是不识别的。

现在我们从引言中的例子来实践一下

"I am singing while you're dancing".match(/\b([a-zA-Z]+)(?=ing\b)/g)
// 我们忽略前面不满足的匹配,直到index = 4时,s为单词边界,满足条件
// 而第一个捕获组是贪婪的,他会首先匹配到整个singing,然后将掌控权交给(?=ing\b),singing不满足匹配 "singinging"
// 于是开始回溯到单词 singin,继续断言, 匹配到的下一个字符为"g",不满足"singining",又开始回溯到"singi"...
// 直到回溯到"sing"时,断言后面有一个ing,并且是一个单词边界,于是"singing"满足条件,这时候我们的正则匹配到了第一个结果。
// 由于零宽断言是不消费字符的,所以我们得到整个表达式匹配的第一个结果是"sing"
// 于是引擎以同样的方式向后面的位置查找,得到了danc
// ["sing","danc"]

我们现在看一下怎么使用负向断言,假如我们有一个系统,3月25号要进行维护,不能使用了,这时候有用户要办理业务,选择日期的时候我们要过滤3月25日这一天,所以产品经理要你临时加上一条规则限定。

选择后日期输出的格式是"yyyy-mm-dd",这时候我们可以这么写正则

/(?!2018-03-25)(\d{4})-(\d{2})-(\d{2})/.test("2018-03-11")
// true 通过验证
/(?!2018-03-25)(\d{4})-(\d{2})-(\d{2})/.test("2018-03-25")
// false

用(?<=exp) 找出 "beep name=wanglihong abcdefg"

"beep name=wanglihong abcdefg".match(/(?<=\bname=)(\w+\b)/)
// ["wanglihong","wanglihong",index: 10,input: "beep name=wanglihong abcdefg",groups: undefined]

提取a标签属性的同时,通过(?<!exp) 过滤style属性

var template = '<a href="/bee" target="_blank" id="o" style="color: black;">点击跳转</a>'
template.match(/(\w+)=(?<!style=)"([^"]+)"/g)
// [href="/bee",target="_blank",id="o"]

摸透了零宽断言,正则的能力也就算上了一个台阶了,当然还有平衡组这种操作,因为在js不支持,所以就暂时不讨论了。

猜你在找的正则表达式相关文章