Regular school from scratch (six)

 One ❀ lead

I was in the (E) zero-based school is described in this article the canonical structure and common operator after operator precedence understand, know how to split a seemingly complex regular expressions. In addition to the regular watch you can read, write a regular is often more important. Then go write a regular faced many problems, when to use regular? How to ensure the accuracy of the regular? Regular How to improve performance? So this article from three points of departure, let us write the premise of regular write better.

Said in front of a regular series of articles are learning to read I  send him "JavaScript regular mini-book" the study notes, text graphic are all the regular use regulex production. Then the beginning of this article!

 II ❀ should not use regular?

You see this title must be wondering learn is regular, how also should not use regular? But in the actual development, a problem can be solved with regular, in fact, can also use other methods to solve. We do not have to learn regular rigid want to use regular solve all the problems, perhaps even better use of other practices it?

For example, we now have a field 2019-12-24 I think are out date, you can use the regular match with the packet acquisition method to achieve:

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

There is no other thing to do? Do not forget to string split cutting methods, such as:

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

In contrast Which do you think is more simple?

Again we want to verify whether the string contains ":" We can use regular implementation:

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

A simpler approach, we can directly use the indexOf check index, or -1 if not, if there is to return the first matching character index.

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

See example off last field, compared to regular use, method string substr or substring will be much simpler. Of course, these two methods if you have doubts, you can read this article bloggers   usage and the difference between substr and substring and slice and splice of.

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.23、+1.23、-1.23
10、+10、-10
.2、+.2、-.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点了....晚安,圣诞快乐,本文结束。

Guess you like

Origin www.cnblogs.com/echolun/p/12090854.html