美文网首页
不积跬步之第六章--正则的构建

不积跬步之第六章--正则的构建

作者: 雨飞飞雨 | 来源:发表于2021-06-19 10:19 被阅读0次

对于任何一门语言的掌握程度怎么样?可以有两个角度来衡量:读和写。不仅要看懂别人的解决方案,也要能独立地解决问题。代码是这样,正则也是这样。与读相比。写往往更加重要,这个道理是不言而喻的。对正则的应用,首重就是:如何针对问题,构建一个合适的正则表达式?这一篇我们就是为了解决这个问题:

  • 什么是平衡法则?
  • 构造正则前提?
  • 准确性
  • 怎么优化正则?
  • 正则的原理是什么?

平衡法则:

构建正则有一点非常重要,就是做到以下几点的平衡:

  • 匹配预期的字符串
  • 不匹配非预期的字符串
  • 可读性和可维护性
  • 效率

构建正则前提

是否能使用正则?

正则虽然强大,但是不是万能的,有一些有规律的字符串,我们就是匹配不了。例如:

1010010001...

虽然有规律,就是无能为力。

是否有必要使用正则

要认识到正则的局限,不要去研究根本无法完成的任务,同样,也不要走上另外一个极端,无所不用正则,能用字符串API解决的简单问题,不不该正则出马。

例如,从日期中提取出年月日,虽然可以使用正则:

var string = "2020-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来做。通过切割-就可以。

比如判断是否有问号,虽然可以使用:

var string = "?id=xx&act=search";
console.log( string.search(/\?/) );
// => 0

其实,我们完全可以使用indexOf方法。

比如获取子串:虽然可以使用正则:

var string = "JavaScript";
console.log( string.match(/.{4}(.+)/)[1] );
// => Script

其实,可以直接使用字符串的substringsubstr方法来做。

var string = "JavaScript";
console.log( string.substring(4) );
// => Script

是否有必要构建一个复杂的正则?

例如匹配密码问题,要求密码长度6-12位,由数字,小写字母,大写字母组成。但必须至少包括2种字符。

/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/

其实可以使用多个小正则来做:

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; }

准确性

所谓准确性,就是能匹配预期的目标,并且不匹配非预期的目标。

这里提到了预期二字,我们就需要知道目标的组成规则。

不然没法界定什么样的目标字符串是符合预期的,什么样的又不是符合预期的。

下面将举例,当目标字符串构成比较复杂时,该如何构建正则,并考虑哪些平衡。

匹配固定电话

比如我们匹配如下格式的固定电话

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}/

其可视化形式:


image.png

上面的正则构建过程略显罗嗦,但是这样做,能保证正则是准确的。

上述三种情形是或的关系,这一点很重要,不然很容易按字符是否出现的情形把正则写成:

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

虽然也能匹配上述目标字符串,但也会匹配 "(0551-88888888" 这样的字符串。
当然,这不是我们想要的。
其实这个正则也不是完美的,因为现实中,并不是每个 3 位数和 4 位数都是一个真实的区号。

这就是一个平衡取舍问题,一般够用就行。

效率

保证了准确性后,才需要是否要考虑要优化。大多数情形是不需要优化的,除非运行的非常慢。什么情形正
则表达式运行才慢呢?我们需要考察正则表达式的运行过程(原理)。

正则表达式的运行分为如下的阶段:

  1. 编译;
  2. 设置起始位置;
  3. 尝试匹配;
  4. 匹配失败的话,从下一位开始继续第3步;

下面以代码为例:我们来看看各个阶段都做了什么?

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"]

具体分析如下:

var regex = /\d+/g

当生成一个正则时,引擎会对其进行编译。报错于否就出现在这个阶段,还记得之前的量词连缀报错吗?

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阶段。

因此,主要优化手法也是针对这两阶段的。

1.使用具体型字符组来代替通配符,来消除回溯

在第三阶段,最大的问题就是回溯。

例如,匹配双引用号之间的字符。如,匹配字符串123"abc"456中的"abc".

如果正则用的是/.*/,会在第3 阶段产生4次回溯粉色表示.*匹配的内容。

image.png

如果正则使用的是:/.*?/。会产生2次回溯。(粉色表示.*?匹配的内容)

image.png

因为回溯的存在,需要引擎保存多种可能未尝试过的状态,以便后续回溯时使用,

注定要占用一定的内存。此时要使用具体化的字符组来代替通配符,以便消除不必要的字符。

此时使用正则/"[^"]*/,就可以。

2.使用非捕获型分组

因为括号的作用之一就是,客户捕获分组和分支里的数据,那么就需要内存来保存它们。

当我们不需要使用分组引用和反向引用时,此时可以使用非捕获分组。

例如:

/^[-]?(\d\.\d+|\d+|\.\d+)$/ 可以修改为:/^[-]?(?:\d\.\d+|\d+|\.\d+)$/

3.独立出确定符

例如:/a+/可以修改成/aa*/.

因为后者能比前者多确定了字符a,这样会在第四步中,加快判断是否匹配失败,进行加快移位的速度。

4.提取分支公共部分

比如:/^abc|^def/修改成/^(?:abc|def)/

又比如:/this|that/修改成/th(?:is:at)/.

这样做,可以减少匹配过程中可消除的重复。

5.减少分支的数量,缩小它们的范围

/red|read/可以修改成/rea?d/.
此时分支和量词产生的回溯的成本是不一样的。但这样优化后,可读性会降低的。

本章小结

一般情况下,针对某问题能写出来一个满足需求的正则,就可以了。

至于准确性和效率方面的追求,就是个人的追求了。

关于准确性,本文讲的是最常用的解决思路:

针对每一种情形,分别写出正则,然后分支把他们合并一起,再提取分支公共的部分,

就能得到准确的正则。

至于优化,了解了匹配原理,常见的优化手法也就那么几种。

相关文章

  • 不积跬步之第六章--正则的构建

    对于任何一门语言的掌握程度怎么样?可以有两个角度来衡量:读和写。不仅要看懂别人的解决方案,也要能独立地解决问题。代...

  • 不积跬步之正则的终章整理

    终于学习完了这一本《JavaScript正则表达式迷你书》,全书只有八十多页,而我却差不多学习了两周时间。从不熟悉...

  • 不积跬步

    2018/10/25 星期四 晴 没想到二姐的高价小收音机比手机的辐射要强,放在床边睡觉,...

  • 不积跬步

    “不积跬步无以至千里”常用来激励。 可在自己身上发掘这句名言,却只有垃圾、肥肉和慢。 一个上午,断断续续收拾了两个...

  • 不积跬步

    生活中看起不起眼的事情,对一些人来说就是财富机遇 朋友在我们单位上班,也属于从别的单位挖过来的,老板慧眼识珠,两人...

  • 不积跬步之第五章--正则的拆分

    对于一门语言的掌握程序怎么样?可以有两个角度来衡量:读和写。不仅要求自己能解决问题,还要能看懂别人的解决方案,代码...

  • 点滴积累成就精彩演讲

    冰冻三尺,非一日之寒。 ——王充不积跬步,无以...

  • 不积跬步之快速排序

    快速排序使用了分治思想来实现。 和冒泡排序一样,快速排序也属于交换排序,通过元素直接的比较和交换位置来达到排序的目...

  • 不积跬步之选择排序

    选择排序 假如我们有一组歌的数据,我们需要按照播放量从大往小排序。最简单的做法就是,先从这组数据里找到最大的那个,...

  • 先让孩子“完成一个小目标”

    不积跬步,无以至千里;不积小流,无以成江海。 ——荀子《劝学篇》 做父母的都有望子成龙之...

网友评论

      本文标题:不积跬步之第六章--正则的构建

      本文链接:https://www.haomeiwen.com/subject/hixkyltx.html