资讯专栏INFORMATION COLUMN

我所理解的正则表达式

K_B_Z / 2921人阅读

摘要:关于,新手理解起来可能比较困难,尤其是一些很牛逼的预查正则表达式。非贪婪与贪婪的问题贪婪出现在这种不确定数量的匹配中,所谓的贪婪,表示正则表达式在匹配的时候,尽可能多的匹配符合条件的内容。

学习了半年的正则表达式,也不能说一直学习吧,就是和它一直在打交道,如何用正则表达式解决自己的问题,并且还要考虑如何在匹配大量的文本时去优化它。慢慢的觉得正则已经成为自己的一项技能,逐渐的从一个正则表达式小白变成一个伪精通者。

那么,我不打算详细介绍正则表达式的使用,或者说这篇文章并不是入门教程,所以如果你对正则表达式一无所知或者处于入门阶段,建议你还是先去看看下面这些正则表达式入门的文章。

阮一峰老师的正则教程
MDN 正则介绍
胡子哥正则表达式 30 分钟入门
阮一峰 ES6 正则表达式扩展
百度百科 正则表达式 很详细,可以当作手册参考

当然正则的教程很多,不限于此,如果你对正则已经了解了,那么可以开始下面的内容了,文章中可能还会涉及一些效率的问题

new RegExp 和 // 正则对象创建区别

如果写过 Python 的同学,都一定会知道 Python 中可以在字符串前面加个小写的 r ,来表示防止转义。防止转义的意思就是说:str = r" " 等价于 str = " ",加了 r 会防止 被转义。

为什么要介绍这个,因为这就是 new RegExp// 的区别,因为我们知道在正则表达式中会频繁的使用转义字符 wsd 等,但是它们在内存中的是以 wsd 存储的,看个例子:

//推荐写法
var regex1 = /w+/g;
regex1 // /w+/g
//RegExp 写法
var regex2 = new RegExp("w+","g");
regex2 // /w+/g
//错误写法
var regex3 = new RegExp("w+","g");
regex3 // /w+/g

你也看出来了,错误写法只能匹配 wwwww 这样的字符串,曾经我就见过有人把他们弄混了,还说第一个第三个没有区别。第二种方法的输出,还是 /w+/g,中间还是要转换,所以推荐第一种写法。

当然,还有比较奇葩的:

var regex4 = new RegExp(/w+/g);
regex4 // /w+/g

MSDN 上关于 RegExp 的介绍。

那么,如何能像 Python 的 r"" 那样,实现一个防转义的功能呢?我这里有一种很蹩脚的方法(仅供娱乐!):

var str1 = "dws";
str1; // "dws"
var str2 = /dws/;
str2.source; // "dws"

没错,就是 srouce,不知道 source 的同学快去面壁吧。(这方法确实很抠脚!)

i、g、m 修饰符

这几个修饰符只是针对 JS 来说的,像 Python 中还有 re.S 表示 . 可以匹配换行符。

对于 i 表示忽略字母大小写,不是很常用,因为它有很多替代品,比如:/[a-zA-Z]/ 可以用来替代 /[a-z]/i,至于两者处理长文本的时间效率,我自己没有研究过,不下定论。

使用 i 需要注意的地方,就是 i 会对正则表达式的每一个字母都忽略大小写,当我们需要部分单词的时候,可以考虑一下/(?:t|T)he boy/

g 表示全局匹配,在印象中,可能很多人会觉得全局匹配就是当使用 match 的时候,把所有符合正则表达式的文本全部匹配出来,这个用途确实很广泛,不过 g 还有其他更有意思的用途,那就是 lastIndex 参数。

var str = "1a2b3c4d5e6f",
    reg = /dwd/g;
str.match(reg); //["1a2", "3c4", "5e6"]

为什么不包括2b3,4d5,因为正则表达式匹配的时候,会用 lastIndex 来标记上次匹配的位置,正常情况下,已经匹配过的内容是不会参与到下次匹配中的。带有 g 修饰符时,可以通过正则对象的 lastIndex 属性指定开始搜索的位置,当然这仅仅局限于函数 exec 和 test(replace 没研究过,没听说过可以控制 lastIndex,match 返回的是数组,无法控制 lastIndex),针对这个题目修改如下:

var str = "1a2b3c4d5e6f",
  reg = /dwd/g;
var a;
var arr = [];
while(a = reg.exec(str)){
  arr.push(a[0]);
  reg.lastIndex -= 1;
}
arr //["1a2", "2b3", "3c4", "4d5", "5e6"]

m 表示多行匹配,我发现很多人介绍 m 都只是一行略过,其实关于 m 还是很有意思的。首先,来了解一下单行模式,我们知道 JavaScript 正则表达式中的 . 是无法匹配 (换行,各个系统使用不一样) 的,像 Python 提供 re.S 表示 . 可以匹配任意字符,包括 ,在 JS 中如果想要表示匹配任意字符,只能用[sS] 这种蹩脚的方式了(还有更蹩脚的 [dD],[.s])。这种模式叫做开启或关闭单行模式,可惜 JS 中无法来控制。

多行模式跟 ^ $ 两兄弟有关,如果你的正则表达式没有 ^$,即时你开启多行模式也是没用的。正常的理解/^123$/只能匹配字符串123,而开启多行模式/^123$/g能匹配["123"," 123","123 "," 123 "],相对于 ^$ 可以匹配 了。

var str = "
a";
/^a/.test(str); //false
/^a/m.test(str); //true

有人说,m 没用。其实在某些特殊的格式下,你知道你要匹配的内容会紧接着 或以 结尾,这个时候 m 就非常有用,比如 HTTP 协议中的请求和响应,都是以 划分每一行的,响应头和响应体之间以 来划分,我们需要匹配的内容就在开头,通过多行匹配,可以很明显的提高匹配效率。

原理性的东西,我们还是要知道的,万一以后会用到。

(?:) 和 (?=) 区别

在正则表达式中,括号不能乱用,因为括号就代表分组,在最终的匹配结果中,会被算入字匹配中,而 (?:) 就是来解决这个问题的,它的别名叫做非捕获分组。

var str = "Hello world!";
var regex = /Hello (w+)/;
regex.exec(str); //["Hello world", "world"]
var regex2 = /Hello (?:w+)/;
regex2.exec(str); //["Hello world"]
//replace 也一样
var regex3 = /(?:ab)(cd)/
"abcd".replace(regex3,"$1") //"cd"

可以看到 (?:) 并不会把括号里的内容计入到子分组中。

关于 (?=),新手理解起来可能比较困难,尤其是一些很牛逼的预查正则表达式。其实还有个 (?!),不过它和 (?=) 是属于一类的,叫做正向肯定(否定)预查,它还有很多别名比如零宽度正预测先行断言。但我觉得最重要的只要记住这两点,预查和非捕获。

预查的意思就是在之前匹配成功的基础上,在向后预查,看看是否符合预查的内容。正因为是预查,lastIndex 不会改变,且不会被捕获到总分组,更不会被捕获到子分组。

var str = "Hello world!";
var regex = /Hello (?=w+)/;
regex.exec(str); //["Hello "]

和 (?:) 区别是:我习惯的会把匹配的总结果叫做总分组,match 函数返回数组每一项都是总分组,exec 函数的返回数组的第一项是总分组。(?:) 会把括号里的内容计入总分组,(?=) 不会把括号里的内容计入总分组。

说白了,还是强大的 lastIndex 在起作用。(?:) 和 (?=) 差别是有的,使用的时候要合适的取舍。

说了这么多关于 (?=) 的内容,下面来点进阶吧!现在的需求是一串数字表示钱 "10000000",但是在国际化的表示方法中,应该是隔三位有个逗号 "10,000,000",给你一串没有逗号的,替换成有逗号的。

var str = "10000000";
var regex = /d(?=(d{3})+$)/g;
str.replace(regex, "$&,"); //"10,000,000"

我们分析一下 regex,/d(?=(d{3})+$)/g 它是全局 g,实际上它匹配的内容只有一个 d,(?=(d{3})+$) 是预判的内容,之前说过,预判的内容不计入匹配结果,lastIndex 还是停留在 d 的位置。(?=(d{3})+$) 到结尾有至少一组 3 个在一起的数字,才算预判成功。

d = 1 的时候,不满足预判,向后移一位,d = 0,满足预判,replace。

(?!) 前瞻判断

(?=) 和 (?!) 叫做正向预查,但往往是正向这个词把我们的思维给束缚住了。正向给人的感觉是只能在正则表达式后面来预判,那么预判为什么不能放在前面呢。下面这个例子也非常有意思。

一个简单密码的验证,要保证至少包含大写字母、小写字母、数字中的两种,且长度 8~20。

如果可以写多个正则,这个题目很简单,思路就是:/^[a-zA-Zd]{8,20}$/ && !(/[a-z]+/) && !(/[A-Z]+/) && !(/d+/),看着眼都花了,好长一串。

下面用 (?!) 前瞻判断来实现:

var regex = /^(?![a-z]+$)(?![A-Z]+$)(?!d+$)[a-zA-Zd]{8,12}$/;
regex.test("12345678"); //false
regex.test("1234567a"); //true

分析一下,因为像 (?!) 预判不消耗 lastIndex,完全可以放到前面进行前瞻。(?![a-z]+$) 的意思就是从当前 lastIndex (就是^)开始一直到 $,不能全是小写字母,(?![A-Z]+$) 不能全是大写字母,(?!d+$) 不能全是数字,[a-zA-Zd]{8,12}$ 这个是主体,判断到这里的时候,lastIndex 的位置仍然是 0,这就是 (?!) 前瞻带来的效率。

对 JS 正则不支持 (?<=) 个人看法

我们都知道,JS 中的正则表达式是不支持正回顾后发断言的 (?<=),当然也不支持 (?。有时候会觉得这种正回顾后发断言确实很有帮助,它可以让我们的思维更清晰,哪些是真正匹配的正则,哪些是断言的正则。在 Python 中我们就可以轻松的使用 (?<=),但是在 JS 中不行。

原因可能是采用的正则引擎不一样导致,既然不支持,那我们也只能通过现有的条件来改进我们所写的正则,下面就说一说我的理解。

对于一个非全局匹配的正则表达式,完全可以通过 (?:) 来实现。比如对于 /(?<=Hello) (.*)$/(这个在 JS 中是不支持的),可以使用 /(?:Hello) (.*)$/作为一个简单的替代,这两个正则的差别就在于最终的匹配分组上面,总分组略有不同,但总有办法可以解决。但要注意,这是非全局匹配,反正只匹配一次。

那如果是全局匹配呢?又该如何实现 (?<=)?

var str = "a1b2c3d";
//var regex = /(?<=w)dw/g
//str.match(regex) => ["1b","2c","3d"]
var regex2 = /(?:w)dw/g
str.match(regex2); //["a1b", "c3d"]

很明显,只通过 (?:) 就显得有点力不从心了,我们想要的结果是 ["1b","2c","3d"],却返回其中的第一和第三个,少了第二个。

这时候,又要拿出强大的 lastIndex

var str = "a1b2c3d";
var regex = /(?:w)(dw)/g;
var m,arr = [];
while(m = regex.exec(str)){
  arr.push(m[1]);
  regex.lastIndex --;
}
arr; //["1b", "2c", "3d"]

和前面的例子很类似,通过重写 lastIndex 的值,达到模仿 (?<=) 的作用。

非贪婪与贪婪的问题

贪婪出现在 + * {1,} 这种不确定数量的匹配中,所谓的贪婪,表示正则表达式在匹配的时候,尽可能多的匹配符合条件的内容。比如 /hello.*world/ 匹配"hello world,nice world"会匹配到第二个 world 结束。

鉴于上面的情况,可以使用 ? 来实现非贪婪匹配。? 在正则表达式中用途很多,正常情况下,它表示前面那个字符匹配 0 或 1 次,就是简化版的 {0,1},如果在一些不确定次数的限制符后面出现,表示非贪婪匹配。/hello.*?world/ 匹配"hello world,nice world" 的结果是 hello world

我刚开始写正则的时候,写出来的正则都是贪婪模式的,往往得到的结果和预想的有些偏差,就是因为少了 ? 的原因。

我初入正则的时候,非贪婪模式还给我一种错觉。还是前面的那个例子,被匹配的内容换一下,用/hello.*?world/ 匹配"hello word,nice world",因为 word 不等于 world,在第一次尝试匹配失败之后,应该返回失败,但结果却是成功的,返回的是 "hello word,nice world"

一开始我对于这种情况是不理解的,但仔细想想也对,这本来就应该返回成功。至于如何在第一次尝试匹配失败之后,后面就不再继续匹配,只能通过优化 .*。如果我们把 .*?end 这样子来看,.* 会把所有字符都吞进去,慢慢吐出最后几个字符,和 end 比较,如果是贪婪,吐到第一个满足条件的就停止,如果是非贪婪,一直吐到不能吐为止,把离自己最近的结果返回。

所以,贪婪是返回最近的一次成功匹配,而不是第一次尝试

避免回溯失控

回溯可以杀死一个正则表达式,这一点都不假。关于正则表达式回溯也很好理解,就是正则引擎发现有两条路可以走时,它会选择其中的一条,把另一条路保存以便回溯时候用。

比如正则 /ab?c/ 在成功匹配到 a 之后,后面可以有 b,也可以没有 b,这时候要提供两种选择。还有其他类型的回溯,比如 /to(night|do)/。当然影响性能的回溯就要和 .* .+ .{m} 有关。

所谓的回溯失控,就是可供选择的路径太多,看一个常见回溯失控的例子,正则 /(A+A+)+B/ ,如果匹配成功,会很快返回,那么匹配失败,非常可怕。比如来匹配 10 个 A AAAAAAAAAA,假设第一个 A+ 吞了 9 个 A,整个正则吐出最后一个字符发现不是 B,这一轮吐完,还不能返回 false,因为还有其他路可以选择;第一个 A+ 吞 8 个 A,....一直这样回溯下去,回溯次数的复杂度大概是 2 的 n 次方吧。

当然你可能会说,自己不会写这样傻的正则表达式。真的吗?我们来看一个匹配 html 标签的正则表达式,/[sS]*?[sS]*?[sS]*?[sS]*?[sS]*? (感觉这样写也很傻)。如果一切都 OK,匹配一个正常的 HTML 页面,工作良好。但是如果不是以 结尾,每一个 [sS]*? 就会扩大其范围,一次一次回溯查找满足的一个字符串,这个时候可怕的回溯就来了。

在说到回溯的同时,有时候还是要考虑一下 . * {} 查询集合的问题,反正我的建议是尽量避免使用匹配任何字符的 [sS],这真的是有点太暴力了。因为我们写正则的时候,都是以正确匹配的思路去写的,同时还需要考虑如果匹配不成功,该如何尽快的让 [a-zA-Z]* 集合尽快停止。比如通常在匹配 HTML 标签的时候正则如果这样写 /<([^>]+)>[sS]*?/ (匹配一个不带 class 等属性的标签),匹配成功时,一切都好说,如果匹配失败,或者匹配的文本中恰好只有左半个 < ,由于范围 [^>] 范围太大,根本停不下来,相比来说 /<(w+)>[sS]*?/` 要好一些。又比如 [^ ]* 在匹配单行时效果不错,即时匹配失败也可以快速停止。

总结

感觉这篇文章写的很乱,东扯西扯的,大概把我这几个月以来所学到的正则表达式知识都写在了这里,当然这并不包括一些基础的知识。我觉得学习正则最主要的还是去练习,只有在实际项目中总结出来的正则经验,才算自己正在掌握的,如果只是简单的扫一眼,时间久了,终究会忘记。共勉!

参考

RegExp对象 - 阮一峰
MSDN RegExp
进阶正则表达式

如何找出文件名为 ".js" 的文件,但要过滤掉 ".min.js" 的文件。

代码如下:

欢迎来我的博客参考代码。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/44233.html

相关文章

  • 我所理解正则达式

    摘要:关于,新手理解起来可能比较困难,尤其是一些很牛逼的预查正则表达式。非贪婪与贪婪的问题贪婪出现在这种不确定数量的匹配中,所谓的贪婪,表示正则表达式在匹配的时候,尽可能多的匹配符合条件的内容。 学习了半年的正则表达式,也不能说一直学习吧,就是和它一直在打交道,如何用正则表达式解决自己的问题,并且还要考虑如何在匹配大量的文本时去优化它。慢慢的觉得正则已经成为自己的一项技能,逐渐的从一个正则表...

    _Zhao 评论0 收藏0
  • JAVA笔记 - 收藏集 - 掘金

    摘要:动态代理个经纪人如何代理个明星掘金在代理模式女朋友这么漂亮,你缺经纪人吗中我们用宝强的例子介绍了静态代理模式的概念。掘金使用从头创建一个,这种方法比较简单。 动态代理:1 个经纪人如何代理 N 个明星 - Android - 掘金在 代理模式:女朋友这么漂亮,你缺经纪人吗? 中我们用宝强的例子介绍了静态代理模式的概念。 本来我的目的是通过大家耳熟能详的例子来加深理解,但是有些网友指责...

    kamushin233 评论0 收藏0
  • 查漏补缺 - 收藏集 - 掘金

    摘要:酝酿许久之后,笔者准备接下来撰写前端面试题系列文章,内容涵盖浏览器框架分钟搞定常用基础知识前端掘金基础智商划重点在实际开发中,已经非常普及了。 这道题--致敬各位10年阿里的前端开发 - 掘金很巧合,我在认识了两位同是10年工作经验的阿里前端开发小伙伴,不但要向前辈学习,我有时候还会选择另一种方法逗逗他们,拿了网上一道经典面试题,可能我连去阿里面试的机会都没有,但是我感受到了一次面试1...

    YuboonaZhang 评论0 收藏0
  • 基础知识 - 收藏集 - 掘金

    摘要:本文是面向前端小白的,大手子可以跳过,写的不好之处多多分钟搞定常用基础知识前端掘金基础智商划重点在实际开发中,已经非常普及了。 JavaScript字符串所有API全解密 - 掘金关于 我的博客:louis blog SF专栏:路易斯前端深度课 原文链接:JavaScript字符串所有API全解密 本文近 6k 字,读完需 10 分钟。 字符串作为基本的信息交流的桥梁,几乎被所有的编程...

    wdzgege 评论0 收藏0
  • Python 之父撰文回忆:为什么要创造 pgen 解析器?

    摘要:花下猫语近日,之父在上开通了博客,并发布了一篇关于解析器的文章参见我翻的全文译文。是的简称,用来生成词法分析器是的简称,用来生成语法分析器。 showImg(https://segmentfault.com/img/remote/1460000019910963?w=5184&h=3456); 花下猫语: 近日,Python 之父在 Medium 上开通了博客,并发布了一篇关于 PEG...

    RaoMeng 评论0 收藏0

发表评论

0条评论

K_B_Z

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<