Java充电社
专辑
博文
联系我
本人继续续收门徒,亲手指导
正则表达式专题(JS版)
-> 正则表达式的构建
1、JS正则表达式完整教程
2、正则表达式字符匹配攻略
3、正则表达式位置匹配攻略
4、正则表达式括号的作用
5、正则表达式回溯法原理
6、正则表达式的拆分
7、正则表达式的构建
8、正则表达式编程
9、下载PDF版
上一篇:正则表达式的拆分
下一篇:正则表达式编程
<div style="display:none"></div> 对于一门语言的掌握程度怎么样,可以有两个角度来衡量:读和写。 不仅要看懂别人的解决方案,也要能独立地解决问题。代码是这样,正则表达式也是这样。 与“读”相比,“写”往往更为重要,这个道理是不言而喻的。 对正则的运用,首重就是:如何针对问题,构建一个合适的正则表达式?本章就解决该问题。 ## 内容包括 1. 平衡法则 2. 构建正则前提 3. 准确性 4. 效率 ## 1. 平衡法则 构建正则有一点非常重要,需要做到下面几点的平衡: 1. 匹配预期的字符串 2. 不匹配非预期的字符串 3. 可读性和可维护性 4. 效率 ## 2. 构建正则前提 ### 2.1 是否能使用正则 正则太强大了,以至于我们随便遇到一个操作字符串问题时,都会下意识地去想,用正则该怎么做。但我们始终要提醒自己,正则虽然强大,但不是万能的,很多看似很简单的事情,还是做不到的。 比如匹配这样的字符串:1010010001.... 虽然很有规律,但是只靠正则就是无能为力。 ### 2.2 是否有必要使用正则 要认识到正则的局限,不要去研究根本无法完成的任务。同时,也不能走入另一个极端:无所不用正则。能用字符串API解决的简单问题,就不该正则出马。 - 比如,从日期中提取出年月日,虽然可以使用正则: ```javascript var string = "2017-07-01"; var regex = /^(\d{4})-(\d{2})-(\d{2})/; console.log( string.match(regex) ); // => ["2017-07-01", "2017", "07", "01", index: 0, input: "2017-07-01"] ``` 其实,可以使用字符串的`split`方法来做,即可: ```javascript var string = "2017-07-01"; var result = string.split("-"); console.log( result ); // => ["2017", "07", "01"] ``` - 比如,判断是否有问号,虽然可以使用: ```javascript var string = "?id=xx&act=search"; console.log( string.search(/\?/) ); // => 0 ``` 其实,可以使用字符串的`indexOf`方法: ```javascript var string = "?id=xx&act=search"; console.log( string.indexOf("?") ); // => 0 ``` - 比如获取子串,虽然可以使用正则: ```javascript var string = "JavaScript"; console.log( string.match(/.{4}(.+)/)[1] ); // => Script ``` 其实,可以直接使用字符串的`substring`或`substr`方法来做: ```javascript var string = "JavaScript"; console.log( string.substring(4) ); // => Script ``` ### 2.3 是否有必要构建一个复杂的正则 比如密码匹配问题,要求密码长度6-12位,由数字、小写字符和大写字母组成,但必须至少包括2种字符。 在第2章里,我们写出了正则是: `/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/` 其实可以使用多个小正则来做: ```javascript var regex1 = /^[0-9A-Za-z]{6,12}$/; var regex2 = /^[0-9]{6,12}$/; var regex3 = /^[A-Z]{6,12}$/; var regex4 = /^[a-z]{6,12}$/; function checkPassword(string) { if (!regex1.test(string)) return false; if (regex2.test(string)) return false; if (regex3.test(string)) return false; if (regex4.test(string)) return false; return true; } ``` ## 3. 准确性 所谓准确性,就是能匹配预期的目标,并且不匹配非预期的目标。 这里提到了“预期”二字,那么我们就需要知道目标的组成规则。 不然没法界定什么样的目标字符串是符合预期的,什么样的又不是符合预期的。 下面将举例说明,当目标字符串构成比较复杂时,该如何构建正则,并考虑到哪些平衡。 ### 3.1 匹配固定电话 比如要匹配如下格式的固定电话号码: > 055188888888 > > 0551-88888888 > > (0551)88888888 第一步,了解各部分的模式规则。 上面的电话,总体上分为区号和号码两部分(不考虑分机号和+86的情形)。 区号是0开头的3到4位数字,对应的正则是:`0\d{2,3}` 号码是非0开头的7到8位数字,对应的正则是:`[1-9]\d{6,7}` 因此,匹配055188888888的正则是:`/^0\d{2,3}[1-9]\d{6,7}$/` 匹配0551-88888888的正则是:`/^0\d{2,3}-[1-9]\d{6,7}$/` 匹配(0551)88888888的正则是:`/^\(0\d{2,3}\)[1-9]\d{6,7}$/` 第二步,明确形式关系。 这三者情形是或的关系,可以构建分支: `/^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$/` 提取公共部分: `/^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/` 进一步简写: `/^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/` 其可视化形式: ![](https://itsoku.oss-cn-hangzhou.aliyuncs.com/itsoku/blog/article/176/80d8e36f-44a5-4e9b-ac60-4aa2c11637e2.png) 上面的正则构建过程略显罗嗦,但是这样做,能保证正则是准确的。 上述三种情形是或的关系,这一点很重要,不然很容易按字符是否出现的情形把正则写成: `/^\(?0\d{2,3}\)?-?[1-9]\d{6,7}$/` 虽然也能匹配上述目标字符串,但也会匹配(0551-88888888这样的字符串。当然,这不是我们想要的。 其实这个正则也不是完美的,因为现实中,并不是每个3位数和4位数都是一个真实的区号。 这就是一个平衡取舍问题,一般够用就行。 ### 3.2 匹配浮点数 要求匹配如下的格式: > 1.23、+1.23、-1.23 > > 10、+10、-10 > > .2、+.2、-.2 可以看出正则分为三部分。 符号部分:`[+-]` 整数部分:`\d+` 小数部分:`\.\d+` 上述三个部分,并不是全部都出现。如果此时很容易写出如下的正则: `/^[+-]?(\d+)?(\.\d+)?$/` 此正则看似没问题,但这个正则也会匹配空字符""。 因为目标字符串的形式关系不是要求每部分都是可选的。 要匹配1.23、+1.23、-1.23,可以用`/^[+-]?\d+\.\d+$/` 要匹配10、+10、-10,可以用`/^[+-]?\d+$/` 要匹配.2、+.2、-.2,可以用`/^[+-]?\.\d+$/` 因此整个正则是这三者的或的关系,提取公众部分后是: `/^[+-]?(\d+\.\d+|\d+|\.\d+)$/` 其可视化形式是: ![](https://itsoku.oss-cn-hangzhou.aliyuncs.com/itsoku/blog/article/176/0456e3d2-f30d-4ec2-a66a-29875785d753.png) 如果要求不匹配+.2和-.2,此时正则变成: ![](https://itsoku.oss-cn-hangzhou.aliyuncs.com/itsoku/blog/article/176/596334d2-5f8f-467f-9458-85ee27d00d55.png) 当然,`/^[+-]?(\d+\.\d+|\d+|\.\d+)$/`也不是完美的,我们也是做了些取舍,比如: - 它也会匹配012这样以0开头的整数。如果要求不匹配的话,需要修改整数部分的正则。 - 一般进行验证操作之前,都要经过trim和判空。那样的话,也许那个错误正则也就够用了。 - 也可以进一步改写成:`/^[+-]?(\d+)?(\.)?\d+$/`,这样我们就需要考虑可读性和可维护性了。 ## 4. 效率 保证了准确性后,才需要是否要考虑要优化。大多数情形是不需要优化的,除非运行的非常慢。什么情形正则表达式运行才慢呢?我们需要考察正则表达式的运行过程(原理)。 正则表达式的运行分为如下的阶段: 1. 编译 2. 设定起始位置 3. 尝试匹配 4. 匹配失败的话,从下一位开始继续第3步 5. 最终结果:匹配成功或失败 下面以代码为例,来看看这几个阶段都做了什么: ```javascript var regex = /\d+/g; console.log( regex.lastIndex, regex.exec("123abc34def") ); console.log( regex.lastIndex, regex.exec("123abc34def") ); console.log( regex.lastIndex, regex.exec("123abc34def") ); console.log( regex.lastIndex, regex.exec("123abc34def") ); // => 0 ["123", index: 0, input: "123abc34def"] // => 3 ["34", index: 6, input: "123abc34def"] // => 8 null // => 0 ["123", index: 0, input: "123abc34def"] ``` 具体分析如下: ```javascript var regex = /\d+/g; ``` 当生成一个正则时,引擎会对其进行编译。报错与否出现在这个阶段。 ```javascript regex.exec("123abc34def") ``` 当尝试匹配时,需要确定从哪一位置开始匹配。一般情形都是字符串的开头,即第0位。 但当使用`test`和`exec`方法,且正则有`g`时,起始位置是从正则对象的`lastIndex`属性开始。 因此第一次`exec`是从第0位开始,而第二次是从3开始的。 设定好起始位置后,就开始尝试匹配了。 比如第一次`exec`,从0开始,去尝试匹配,并且成功地匹配到3个数字。此时结束时的下标是2,因此下一次的起始位置是3。 而第二次,起始下标是3,但第3个字符是“a”,并不是数字。但此时并不会直接报匹配失败,而是移动到下一位置,即从第4位开始继续尝试匹配,但该字符是b,也不是数字。再移动到下一位,是c仍不行,再移动一位是数字3,此时匹配到了两位数字34。此时,下一次匹配的位置是d的位置,即第8位。 第三次,是从第8位开始匹配,直到试到最后一位,也没发现匹配的,因此匹配失败,返回`null`。同时设置`lastIndex`为0,即,如要再尝试匹配的话,需从头开始。 从上面可以看出,匹配会出现效率问题,主要出现在上面的第3阶段和第4阶段。 因此,主要优化手法也是针对这两阶段的。 ### 4.1 使用具体型字符组来代替通配符,来消除回溯 而在第三阶段,最大的问题就是回溯。 例如,匹配双引用号之间的字符。如,匹配字符串123"abc"456中的"abc"。 如果正则用的是:`/".*"/`,,会在第3阶段产生4次回溯(粉色表示`.*`匹配的内容): ![](https://itsoku.oss-cn-hangzhou.aliyuncs.com/itsoku/blog/article/176/90488d03-e683-4205-a398-db974b95c986.png) 如果正则用的是:`/".*?"/`,会产生2次回溯(粉色表示`.*?`匹配的内容): ![](https://itsoku.oss-cn-hangzhou.aliyuncs.com/itsoku/blog/article/176/726251a4-3706-45ce-a14e-9a41ec50d8ac.png) 因为回溯的存在,需要引擎保存多种可能中未尝试过的状态,以便后续回溯时使用。注定要占用一定的内存。 此时要使用具体化的字符组,来代替通配符`.`,以便消除不必要的字符,此时使用正则`/"[^"]*"/`,即可。 ### 4.2 使用非捕获型分组 因为括号的作用之一是,可以捕获分组和分支里的数据。那么就需要内存来保存它们。 当我们不需要使用分组引用和反向引用时,此时可以使用非捕获分组。例如: `/^[+-]?(\d+\.\d+|\d+|\.\d+)$/` 可以修改成: `/^[+-]?(?:\d+\.\d+|\d+|\.\d+)$/` ### 4.3 独立出确定字符 例如`/a+/`,可以修改成`/aa*/`。 因为后者能比前者多确定了字符a。这样会在第四步中,加快判断是否匹配失败,进而加快移位的速度。 ### 4.4 提取分支公共部分 比如/`^abc|^def/`,修改成`/^(?:abc|def)/`。 又比如`/this|that/`,修改成`/th(?:is|at)/`。 这样做,可以减少匹配过程中可消除的重复。 ### 4.5 减少分支的数量,缩小它们的范围 `/red|read/`,可以修改成`/rea?d/`。此时分支和量词产生的回溯的成本是不一样的。但这样优化后,可读性会降低的。 ## 小结 本章涉及的内容并不多。 一般情况下,针对某问题能写出一个满足需求的正则,基本上就可以了。 至于准确性和效率方面的追求,纯属看个人要求了。我觉得够用就行了。 关于准确性,本章关心的是最常用的解决思路: 针对每种情形,分别写出正则,然用分支把他们合并在一起,再提取分支公共部分,就能得到准确的正则。 至于优化,本章没有为了凑数,去写一大堆。了解了匹配原理,常见的优化手法也就这么几种。 ```html 作者:老姚 链接:https://juejin.im/post/5965943ff265da6c30653879 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 ``` <a style="display:none" target="_blank" href="https://mp.weixin.qq.com/s/_S1DD2JADnXvpexxaBwLLg" style="color:red; font-size:20px; font-weight:bold">继续收门徒,亲手带,月薪 4W 以下的可以来找我</a> ## 最新资料 1. <a href="https://mp.weixin.qq.com/s?__biz=MzkzOTI3Nzc0Mg==&mid=2247484964&idx=2&sn=c81bce2f26015ee0f9632ddc6c67df03&scene=21#wechat_redirect" target="_blank">尚硅谷 Java 学科全套教程(总 207.77GB)</a> 2. <a href="https://mp.weixin.qq.com/s?__biz=MzkwOTAyMTY2NA==&mid=2247484192&idx=1&sn=505f2faaa4cc911f553850667749bcbb&scene=21#wechat_redirect" target="_blank">2021 最新版 Java 微服务学习线路图 + 视频</a> 3. <a href="https://mp.weixin.qq.com/s?__biz=MzkwOTAyMTY2NA==&mid=2247484573&idx=1&sn=7f3d83892186c16c57bc0b99f03f1ffd&scene=21#wechat_redirect" target="_blank">阿里技术大佬整理的《Spring 学习笔记.pdf》</a> 4. <a href="https://mp.weixin.qq.com/s?__biz=MzkwOTAyMTY2NA==&mid=2247484544&idx=2&sn=c1dfe907cfaa5b9ae8e66fc247ccbe84&scene=21#wechat_redirect" target="_blank">阿里大佬的《MySQL 学习笔记高清.pdf》</a> 5. <a href="https://mp.weixin.qq.com/s?__biz=MzkwOTAyMTY2NA==&mid=2247485167&idx=1&sn=48d75c8e93e748235a3547f34921dfb7&scene=21#wechat_redirect" target="_blank">2021 版 java 高并发常见面试题汇总.pdf</a> 6. <a href="https://mp.weixin.qq.com/s?__biz=MzkwOTAyMTY2NA==&mid=2247485664&idx=1&sn=435f9f515a8f881642820d7790ad20ce&scene=21#wechat_redirect" target="_blank">Idea 快捷键大全.pdf</a> ![](https://itsoku.oss-cn-hangzhou.aliyuncs.com/itsoku/blog/article/1/2883e86e-3eff-404a-8943-0066e5e2b454.png)
#custom-toc-container