学习了半年的正则表达式,也不能说一直学习吧,就是和它一直在打交道,如何用正则表达式解决本身的问题,而且还要考虑如何在匹配大量的文本时去优化它。慢慢的以为正则已经成为本身的一项技能,逐渐的从一个正则表达式小白变成一个伪精通者。javascript
那么,我不打算详细介绍正则表达式的使用,或者说这篇文章并非入门教程,因此若是你对正则表达式一无所知或者处于入门阶段,建议你仍是先去看看下面这些正则表达式入门的文章。html
阮一峰老师的正则教程
MDN 正则介绍
胡子哥正则表达式 30 分钟入门
阮一峰 ES6 正则表达式扩展
百度百科 正则表达式 很详细,能够看成手册参考java
固然正则的教程不少,不限于此,若是你对正则已经了解了,那么能够开始下面的内容了,文章中可能还会涉及一些效率的问题。es6
若是写过 Python 的同窗,都必定会知道 Python 中能够在字符串前面加个小写的 r ,来表示防止转义。防止转义的意思就是说:str = r"\t' 等价于 str = '\\t',加了 r 会防止 \ 被转义。正则表达式
为何要介绍这个,由于这就是 new RegExp
和 //
的区别,由于咱们知道在正则表达式中会频繁的使用转义字符 \w\s\d 等,可是它们在内存中的是以 \\w\\s\\d 存储的,看个例子:数组
//推荐写法 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,中间仍是要转换,因此推荐第一种写法。ide
固然,还有比较奇葩的:函数
var regex4 = new RegExp(/\w+/g); regex4 // /\w+/g
MSDN 上关于 RegExp 的介绍。性能
那么,如何能像 Python 的 r''
那样,实现一个防转义的功能呢?我这里有一种很蹩脚的方法(仅供娱乐!):学习
var str1 = '\d\w\s'; str1; // "dws" var str2 = /\d\w\s/; str2.source; // "\d\w\s"
没错,就是 srouce
,不知道 source 的同窗快去面壁吧。(这方法确实很抠脚!)
这几个修饰符只是针对 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 = /\d\w\d/g; str.match(reg); //["1a2", "3c4", "5e6"]
为何不包括2b3,4d5
,由于正则表达式匹配的时候,会用 lastIndex
来标记上次匹配的位置,正常状况下,已经匹配过的内容是不会参与到下次匹配中的。带有 g 修饰符时,能够经过正则对象的 lastIndex 属性指定开始搜索的位置,固然这仅仅局限于函数 exec 和 test(replace 没研究过,没据说过能够控制 lastIndex,match 返回的是数组,没法控制 lastIndex),针对这个题目修改以下:
var str = '1a2b3c4d5e6f', reg = /\d\w\d/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 正则表达式中的 .
是没法匹配 \r\n (换行,各个系统使用不同) 的,像 Python 提供 re.S 表示 .
能够匹配任意字符,包括 \r\n,在 JS 中若是想要表示匹配任意字符,只能用[\s\S] 这种蹩脚的方式了(还有更蹩脚的 [\d\D],[.\s])。这种模式叫作开启或关闭单行模式,惋惜 JS 中没法来控制。
多行模式跟 ^ $
两兄弟有关,若是你的正则表达式没有 ^$,即时你开启多行模式也是没用的。正常的理解/^123$/
只能匹配字符串123
,而开启多行模式/^123$/g
能匹配['123','\n123','123\n','\n123\n'],相对于 ^$ 能够匹配 \r\n 了。
var str = '\na'; /^a/.test(str); //false /^a/m.test(str); //true
有人说,m 没用。其实在某些特殊的格式下,你知道你要匹配的内容会紧接着 \r\n 或以 \r\n 结尾,这个时候 m 就很是有用,好比 HTTP 协议中的请求和响应,都是以 \r\n 划分每一行的,响应头和响应体之间以 \r\n\r\n 来划分,咱们须要匹配的内容就在开头,经过多行匹配,能够很明显的提升匹配效率。
原理性的东西,咱们仍是要知道的,万一之后会用到。
在正则表达式中,括号不能乱用,由于括号就表明分组,在最终的匹配结果中,会被算入字匹配中,而 (?:) 就是来解决这个问题的,它的别名叫作非捕获分组。
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-Z\d]{8,20}$/ && !(/[a-z]+/) && !(/[A-Z]+/) && !(/\d+/),看着眼都花了,好长一串。
下面用 (?!) 前瞻判断来实现:
var regex = /^(?![a-z]+$)(?![A-Z]+$)(?!\d+$)[a-zA-Z\d]{8,12}$/; regex.test('12345678'); //false regex.test('1234567a'); //true
分析一下,由于像 (?!) 预判不消耗 lastIndex,彻底能够放到前面进行前瞻。(?![a-z]+$)
的意思就是从当前 lastIndex (就是^)开始一直到 $,不能全是小写字母,(?![A-Z]+$)
不能全是大写字母,(?!\d+$) 不能全是数字,[a-zA-Z\d]{8,12}$ 这个是主体,判断到这里的时候,lastIndex 的位置仍然是 0,这就是 (?!) 前瞻带来的效率。
咱们都知道,JS 中的正则表达式是不支持正回顾后发断言的 (?<=)
,固然也不支持 (?<!)
。有时候会以为这种正回顾后发断言确实颇有帮助,它可让咱们的思惟更清晰,哪些是真正匹配的正则,哪些是断言的正则。在 Python 中咱们就能够轻松的使用 (?<=),可是在 JS 中不行。
缘由多是采用的正则引擎不同致使,既然不支持,那咱们也只能经过现有的条件来改进咱们所写的正则,下面就说一说个人理解。
对于一个非全局匹配的正则表达式,彻底能够经过 (?:)
来实现。好比对于 /(?<=Hello) (.*)$/
(这个在 JS 中是不支持的),可使用 /(?:Hello) (.*)$/
做为一个简单的替代,这两个正则的差异就在于最终的匹配分组上面,总分组略有不一样,但总有办法能够解决。但要注意,这是非全局匹配,反正只匹配一次。
那若是是全局匹配呢?又该如何实现 (?<=)
?
var str = 'a1b2c3d'; //var regex = /(?<=\w)\d\w/g //str.match(regex) => ['1b','2c','3d'] var regex2 = /(?:\w)\d\w/g str.match(regex2); //["a1b", "c3d"]
很明显,只经过 (?:)
就显得有点力不从心了,咱们想要的结果是 ['1b','2c','3d']
,却返回其中的第一和第三个,少了第二个。
这时候,又要拿出强大的 lastIndex
:
var str = 'a1b2c3d'; var regex = /(?:\w)(\d\w)/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 标签的正则表达式,/<html>[\s\S]*?<head>[\s\S]*?</head>[\s\S]*?<body>[\s\S]*?</body>[\s\S]*?</html> (感受这样写也很傻)。若是一切都 OK,匹配一个正常的 HTML 页面,工做良好。可是若是不是以 </html>
结尾,每个 [\s\S]*? 就会扩大其范围,一次一次回溯查找知足的一个字符串,这个时候可怕的回溯就来了。
在说到回溯的同时,有时候仍是要考虑一下 . * {}
查询集合的问题,反正个人建议是尽可能避免使用匹配任何字符的 [\s\S],这真的是有点太暴力了。由于咱们写正则的时候,都是以正确匹配的思路去写的,同时还须要考虑若是匹配不成功,该如何尽快的让 [a-zA-Z]*
集合尽快中止。好比一般在匹配 HTML 标签的时候正则若是这样写 /<([^>]+)>[sS]*?<\/\1>/ (匹配一个不带 class 等属性的标签),匹配成功时,一切都好说,若是匹配失败,或者匹配的文本中刚好只有左半个 < ,因为范围 [^>] 范围太大,根本停不下来,相比来讲 /<(\w+)>[\s\S]*?<\/\1>/` 要好一些。又好比 [^\r\n]* 在匹配单行时效果不错,即时匹配失败也能够快速中止。
感受这篇文章写的很乱,东扯西扯的,大概把我这几个月以来所学到的正则表达式知识都写在了这里,固然这并不包括一些基础的知识。我以为学习正则最主要的仍是去练习,只有在实际项目中总结出来的正则经验,才算本身正在掌握的,若是只是简单的扫一眼,时间久了,终究会忘记。共勉!
如何找出文件名为 ".js" 的文件,但要过滤掉 ".min.js" 的文件。
代码以下:
欢迎来个人博客参考代码。