从零开始学正则(六)

 壹 ❀ 引html

我在 从零开始学正则(五)这篇文章中介绍了正则常见结构与操做符,在了解操做符的优先级后,知晓了如何去拆分一个看似复杂的正则表达式。正则除了会看会读,会写一个正则每每更重要。那么要去写一个正则就面临了诸多问题,何时该用正则?怎么保证正则的准确性?正则如何提高性能?那么本篇文章将从这三个点出发,让咱们在会写正则的前提下写的更好。git

说在前面,正则学习系列文章均为我阅读 老姚《JavaScript正则迷你书》的读书笔记,文中全部正则图解均使用regulex制做。那么本文开始!github

 贰 ❀ 该不应使用正则?正则表达式

看到这个标题你确定纳闷,学的就是正则,怎么还该不应用正则?但在实际开发中,一个问题能够用正则解决,其实也可使用其它方法解决。咱们学正则不必定要死板的想要用正则解决全部问题,或许使用其它作法更棒呢?性能

好比咱们如今有字段 2019-12-24 ,我想分别取出年月日,使用正则可使用match方法配合分组获取实现:学习

var result = '2019-12-24'.match(/^(\d{4})-(\d{2})-(\d{2})$/);
console.log(RegExp.$1, RegExp.$2, RegExp.$3); //2019 12 24

有没有其它作法呢?别忘了字符串的 split 切割方法,好比:测试

var arr = '2019-12-24'.split('-');
console.log(arr[0], arr[1], arr[2]);//2019 12 24

相比之下你以为哪一种更简单呢?优化

再如咱们想验证字符串中是否包含“:”,咱们可使用正则实现:this

var result = /\:/.exec('12:34');
console.log(result); //[":", index: 2, input: "12:34", groups: undefined]

更简单的作法,咱们能够直接使用indexOf检查索引,若是没有返回-1,若是有返回第一个匹配的字符下标。spa

var result = '12:34'.indexOf(":");
console.log(result); //2

最后看个截止字段的例子,相比使用正则,使用字符串方法substrsubstring都会简单不少。固然若你对这两个方法有疑惑,能够读读博主这篇文章  substring和substr以及slice和splice的用法和区别。

var string = "hello,听风是风";
var result = /.{6}(.+)/.exec(string)[1];
console.log(result); //听风是风
var result = string.substr(6);
console.log(result); //听风是风
var result = string.substring(6);
console.log(result); //听风是风

经过以上三个例子能够看出,在一些更偏于字符操做的状况下,该使用字符串方法就得用,学会灵活变通。

 叁 ❀ 正则的准确性

何为准确性,一段正则除了能匹配咱们所须要的,还得保证不会匹配那些咱们不须要的,假设咱们如今要匹配以下三种座机(固定电话)号码,该如何写这个正则呢:

var num1 = '055188888888';
var num2 = '0551-88888888';
var num3 = '(0551)88888888';

科普一下,座机号码由 区号+座机号 组成,且区号长度为3-4位数字且首位数字必须为0,而座机号由7-8位数字组成,且首数字不能为0。

尝试分析上面三种座机号码格式,第一种为区号直接拼号码,第二种使用了拼接符 - ,第三种使用了圆括号包裹区号,很明显这是三种分支状况,因此咱们能够先写匹配数字的正则,再加分支条件。

只是匹配数字这也太简单了,不假思索的写出  /^\d{3,4}\d{7,8}$/ ,那么这段正则就是不具有准确性的正则,别忘了咱们在前面有提到区号与号码首数字的问题,因此改改应该是这样:

var regexp = /^0\d{2,3}[1-9]\d{6,7}$/;

固然这个正则只能匹配区号直接紧接号码的状况,有拼接符的状况就是这样:

var regexp = /^0\d{2,3}-[1-9]\d{6,7}$/;

带圆括号的格式就是这样:

var regexp = /^\(0\d{2,3}\)[1-9]\d{6,7}$/;

咱们仔细对比这三段正则,能够发现正则后半段是彻底相同的,区别也只是在前半段,因此将前部分以分支表示,改写正则后应该是这样:

var regexp = /^(?:0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/;

还能不能简写?仔细观察前两种分支状况,一个是无拼接符一个是有拼接符,除此以外其它部分都同样,这不又能够组合成拼接符无关紧要的状况了,因此咱们再次简化:

var regexp = /^(?:0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/;

咱们简单测试下,发现彻底没问题

console.log(regexp.test(num1)); //true
console.log(regexp.test(num2)); //true
console.log(regexp.test(num3)); //true

说到拼接符无关紧要,可能有的同窗就想到了,我圆括号也能够写成无关紧要,这样正则不是看着更精简了,像这样:

var regexp = /^\(?0\d{2,3}\)?-?[1-9]\d{6,7}$/;

但这样就形成了一个问题,你会发现同时有括号和拼接符,或者说有一半括号的格式都能匹配:

console.log(regexp.test('(0551-88888888')); //true
console.log(regexp.test('(0551)-88888888')); //true
console.log(regexp.test('0551)88888888')); //true

很明显这不是咱们想要的状况,这段正则就缺失了很重要的精准性。

咱们来看第二个例子,写一个匹配浮点数的正则,要求能匹配以下几种数据类型:

1.2三、+1.2三、-1.23
十、+十、-10
.二、+.二、-.2

咱们结合这三种数据来作个分析,首先关于正负符号很明显是无关紧要,毋庸置疑能够写成 [+-]?;而后是整数部分,多是多位整数也可能没有,因此是 (\d+)?;最后是小数点部分,由于可能不存在小数点,因此能够写成 (\.\d+)?,因此结合起来就是:

var regexp = /^[+-]?(\d+)?(\.\d+)?$/;

这个正则有个最大的弊端,由于三个条件后面都有?表示无关紧要,极端一点,三个都为无,因此这个正则能够匹配空白:

/^[+-]?(\d+)?(\.\d+)?$/.test("");//true

可能有同窗敏锐的发现了,.2,+.2这种状况都是整数部分为0的状况,那能不能为写成这样 /^[+-]?(0?|[1-9]+)(\.\d+)?$/ ,很明显也不行,好比10,+10这种整数用到了0,因此没法经过分支来控制0的显示隐藏。

那怎么作呢?仍是与匹配座机号码同样,咱们针对三种状况分开写正则,好比匹配 "1.23"、"+1.23"、"-1.23",正则能够这样写:

var regexp = /^[+-]?\d+\.\d+$/;

匹配 "10"、"+10"、"-10" 的正则能够写成:

var regexp = /^[+-]?\d+$/;

匹配 ".2"、"+.2"、"-.2" 正则能够写成:

var regexp = /^[+-]?\.\d+$/;

咱们提取三个正则的共用部分,很明显就是 [+-]? 这一部分,其它部分采用分支表示,综合起来就是这样:

var regexp = /^[+-]?(\d+\.\d+|\d+|\.\d+)$/;

简单测试,彻底没问题:

regexp.test("+.2"); //true
regexp.test("-.2"); //true
regexp.test("10.2"); //true
regexp.test("+10.2"); //true

虽然这种分状况写,再抽出共用部分,将非共用分支表示的作法有点繁琐,但对于正则新手来讲确实是最为稳妥保证精准性的作法。

 肆 ❀ 正则的效率

在确保正则的精准性以后,剩下的就是如何提高正则的效率性能了(固然对于我这样的新手,能写出来就不错了...)。

如何提高正则性能,咱们通常从正则的运行阶段下手,正则完整的运行分为以下几个阶段:编译 --- 设定起始位置 --- 尝试匹配 --- 匹配失败的话,从下一位开始继续第 3 步 --- 最终结果:匹配成功或失败。

咱们能够经过下面这个例子模拟这个过程:

var regex = /\d+/g;
console.log(regex.lastIndex, regex.exec("123abc34def")); //0 ["123", index: 0, input: "123abc34def", groups: undefined]
console.log(regex.lastIndex, regex.exec("123abc34def")); //3 ["34", index: 6, input: "123abc34def", groups: undefined]
console.log(regex.lastIndex, regex.exec("123abc34def")); //8 null
console.log(regex.lastIndex, regex.exec("123abc34def")); //0 ["123", index: 0, input: "123abc34def", groups: undefined]

是的你没看过,明明都是输出相同的东西,每次输出的内容竟然还不同。这是由于当使用 test 或者 exec 方法且正则尾部有 g 时,好比像上面执行屡次,下次执行时匹配的起始位置是从上次失败的位置。说直白点,使用这两个方法就像有记忆功能同样,每次执行都是从上次结束的位置开始,好比咱们用match方法就不会有这个问题:

var regex = /\d+/g;
console.log(regex.lastIndex, "123abc34def".match(regex));//0 ["123", "34"]
console.log(regex.lastIndex, "123abc34def".match(regex));//0 ["123", "34"]
console.log(regex.lastIndex, "123abc34def".match(regex));//0 ["123", "34"]
console.log(regex.lastIndex, "123abc34def".match(regex));//0 ["123", "34"]

咱们就经过上面exec来分析正则执行阶段。第一次执行匹配从字符串索引0开始,由于是全局匹配,因此一直匹配到了3,因此匹配结果为123,匹配到a时由于不知足,因此失败了。

第二次开始就是从上次失败的地方开始,因此是从索引3开始,在经历了abc三次失败后,终于遇到了数字34,匹配成功,再往下走时是d,因此又失败了。

第三次匹配开始的起点就是索引8,但由于def都是字母,所有不符合,匹配结果,最后返回了一个null,此时索引被重置为0。

由于起始位置被重置,因此第四次匹配重复了第一次匹配的操做,又是一轮新的开始。

其实看上面exec的例子就反应出了一个问题,每次执行正则都有记录最后匹配失败的位置供下次匹配使用,回溯也是如此,正则会记录多种可能中何尝试过的状态以便回溯使用,这是很是消耗内存的。咱们来综合给出几点优化建议:

1.尽可能使用具体的字符来替代通配符,减小回溯

好比咱们想匹配 123"abc"456 中的 "abc",使用正则 /"[^"]*"/ 的性能要远高于 /".*"/,使用/"\w{3}"/固然更好。

2.使用非捕获型分组

在介绍分组时咱们已经说过,正则会记录每一个分组的匹配结果。若是咱们的分组只是为了单纯起到匹配的做用,而不喜欢正则默认去帮咱们记录分组的匹配结果,可使用非捕获型分组。

'123abc456'.match(/(\w{3})/);
console.log(RegExp.$1);//134

//使用非捕获型分组
'123abc456'.match(/(?:\w{3})/);
console.log(RegExp.$1);//为空,未记录

3.独立出肯定字符

好比咱们有正则 /a+/ 能够修改成 /aa*/,由于后者在匹配时能比前者多肯定一个字符,不论是失败仍是成功,都能更快一部=步确认。

4.提取分支

咱们在介绍匹配座机号码与浮点数已经有阐述这一点,将正则共用部分抽离出来,不一样部分做为分支,好比将 /this|that/ 修改成 /th(?:is|at)/,这样能减小重复匹配。

5.减小分支数量,缩小匹配范围

虽然推荐抽出共用后使用分支,但有些特殊分支状况能简写复用的仍是推荐简写,好比 /red|read/ 能够修改为 /rea?d/。由于分支若是匹配失败,切换到另外一条分支时也须要回溯。

 伍 ❀ 总

那么到这里,第六章节全部知识所有介绍完毕了。这一章节主要是站在能写正则的基础上,进一步优化正则写法,提高正则匹配的精准性,以及正则运行的性能。共用部分正则,将不一样进行分支算是我读下来最大感触的地方,对于优化而言仍是须要必定的实战积累,不过先创建优化的观念也不是坏事。那么就说到这里了,今天圣诞节,原本想早点睡觉,结果又写到12点了....晚安,圣诞快乐,本文结束。

相关文章
相关标签/搜索