想写出效率更高的正则表达式?试试固化分组和占有优先匹配吧

20200719 (1).png

上次咱们讲解了正则表达式量词匹配方式的贪婪匹配和懒惰匹配以后,一些同窗给个人公众号留言说但愿可以快点把量词匹配方式的下篇也写了。那么,此次咱们就来学习一下量词的另一种匹配方式,那就是占有优先的匹配方式。固然咱们这篇文章还讲解了跟占有优先匹配功能同样的固化分组,以及使用确定的顺序环视来模拟占有优先的匹配方式以及固化分组。准备好了吗,快来给本身的技能树上再添加一些技能吧。javascript

咱们若是能够掌握这种匹配方式的原理的话,那么咱们就有能力写出效率更高的正则表达式。在进行深刻的学习以前,但愿你至少对正则表达式的贪婪匹配有所了解,若是还不怎么了解的话,能够花费几分钟的时间看一下我上一篇关于贪婪匹配和懒惰匹配的文章。php

占有优先的匹配方式:(表达式)*+

首先,占有优先的匹配方式跟贪婪匹配的方式很像。它们之间的区别就是占有优先的匹配方式,不会归还已经匹配的字符,这点很重要。这也是为何占有优先的匹配方式效率比较高的缘由。由于它做用的表达式在匹配结束以后,不会保留备用的状态,也就是不会进行回溯。可是,贪婪匹配会保留备用的状态,若是以前的匹配没有成功的话,它会回退到最近一次的保留状态,也就是进行回溯,以便正则表达式总体可以匹配成功
那么占有优先的表示方式是怎样的?占有优先匹配就是在原来的量词的后面添加一个+,像下面展现的这样。html

.?+
.*+
.++
.{3, 6}+
.{3,}+

由于正则表达式有不少流派,有一些流派是不支持占有优先这种匹配方式的,好比JavaScript就不支持这种匹配的方式(前端同窗表示不是很开心😂),可是咱们可使用确定的顺序环视来模拟占有优先的匹配。PHP就支持这种匹配方式。因此接下来咱们一些正则的演示,就会选择使用PHP流派进行演示。前端

咱们来写一个简单的例子来加深一下你们对于占有优先匹配方式的了解。有这么一个需求,你须要写一个正则表达式来匹配以数字9结尾的数字。你会怎么写呢?固然,对于已经有正则表达式基础的同窗来讲,这应该是很容易的事情。咱们会写出这么一个正则表达式\d*9,这样就知足了上面所说的需求了。java

d*9

让咱们把上面的贪婪匹配方式修改成占有优先的匹配方式,你以为咱们还可以匹配相同的结果吗?来让咱们看一下修改后的匹配结果。node

d*+9

答案是不能,你也许会好奇,为何就不能够了。让我来好好给你们解释一下为何不可以匹配了。git

咱们知道正则表达式是从左向右匹配的,对于\d*+这个总体,咱们在进行匹配的时候能够先把\d*+看做是\d*进行匹配。对于\d*+这部分表达式来讲它在开始匹配的时候会匹配尽量多的数字,对于咱们给出的测试用例,\d*+都是能够匹配的,因此\d*+直接匹配到了每一行数字的结尾处。而后由于\d*+是一个总体,表示占有优先的匹配。因此\d*+匹配完成以后,这个总体便再也不归还已经匹配的字符了。可是咱们正则表达式的后面还须要匹配一个字符9,可是前面已经匹配到字符串的结尾了,再没有字符给9去匹配,因此上面的测试用例都匹配失败了。github

在开始匹配的过程当中咱们能够把占有优先当作贪婪匹配来进行匹配,可是一旦匹配完成就跟贪婪匹配不同了,由于它再也不归还匹配到的字符。因此对于占有优先的匹配方式,咱们只须要牢记占有优先匹配方式匹配到的字符再也不归还就能够了。正则表达式

固化分组:(?>表达式)

咱们了解了占有优先的匹配以后,再来看看跟占有优先匹配做用同样的固化分组。那什么是固化分组呢?固化分组的意思是这样的,当固化分组里面的表达式匹配完成以后,再也不归还已经匹配到的字符。固话分组的表示方式是(?>表达式),其中里面的表达式就是咱们要进行匹配的表达式。好比(?>\d*)里面的表达式就是\d*,表示的意思就是当\d*这部分匹配完成以后,再也不归还\d*已经匹配到的字符。express

因此,对于\d*+来讲,咱们若是使用固化分组的话能够表示为(?>\d*)。其实,占有优先固化分组的一种简便的表示方式,若是固化分组里面的表达式是一个很简单的表达式的话。那么使用占有优先量词,比使用固化分组更加的直观。

咱们将上面使用占有优先的表达式替换为使用固化分组的方式表示,下面两张图片展现了使用固化分组后的匹配结果。

(?>d*)

(?>d*)9

还有一些须要注意的是,支持占有优先量词的正则流派也支持固化分组,可是对于支持固化分组的正则流派来讲,不必定支持占有优先量词。因此在使用占有优先量词的时候,要确保你使用的那个流派是支持的。

使用确定顺序环视模拟固化分组:(?=(表达式))1

对于不支持固化分组的流派来讲,若是这些流派支持确定的顺序环视捕获的括号的话,咱们可使用确定的顺序环视来模拟固化分组。若是对于正则表达式的环视还不熟悉的话,能够花几分钟的时间看一下我以前写的这篇文章距离弄懂正则的环视,你只差这一篇文章,保证你能够快速的理解正则的环视。

看到这里,你可能要问,为何确定的顺序环视能够模拟固化分组呢?咱们要知道固化分组的特性就是匹配完成以后,丢弃了固化分组内表达式的备用状态,而后不会进行回溯。又由于环视一旦匹配成功以后也是不会进行回溯的,因此咱们能够利用确定的顺序环视来模拟固化分组。

咱们可使用(?=(表达式))\1这个表达式来模拟固化分组(?>表达式)。我来解释一下上面这个模拟的正则表达式,首先是一个确定的顺序环视,须要在当前位置的后面找到知足表达式的匹配,若是找到的话,接下来\1会匹配环视中已经匹配到的那部分字符。由于在顺序环视中的正则表达式不会受到顺序环视后面表达式的影响,因此顺序环视内部的表达式在匹配完成以后不会进行回溯。而后后面的\1再次匹配环视里面表达式匹配到的内容,这样就模拟了一个固化分组。

咱们再将上面的表达式替换为使用模拟固化分组的方式表示,下面两张图片展现了使用模拟固化分组后的匹配结果。

(?=(d*))1

(?=(d*))19

模拟的固化分组在效率上要比真正的固化分组慢一些,由于\1的匹配也是须要花费时间的。不过对于贪婪匹配所形成的的回溯来讲,这点匹配的时间通常仍是很短的。

贪婪匹配和占有优先效率的比较

咱们上面说过,由于占有优先不会回溯,因此在一些状况下,使用占有优先的匹配要比使用匹配优先的匹配效率高不少。那么下面咱们就使用代码来验证一下贪婪匹配和占有优先匹配的效率是怎样的。

代码以下所示:

// 匹配优先(贪婪匹配)匹配一行中的数字,后面紧跟着字符b
const greedy_reg = /\d*b/;
// 占有优先(使用确定顺序环视模拟)
const possessive_reg = /(?=(\d*))\1b/;
// 测试的字符串 000...(共有1000个0)...000a
const str = `${new Array(1000).fill(0).join('')}a`;

console.time('匹配优先');
greedy_reg.test(str);
console.timeEnd('匹配优先');

console.time('模拟的占有优先');
possessive_reg.test(str);
console.timeEnd('模拟的占有优先');

在上面的测试代码中,咱们生成了一个长度为1001的字符串,最后一位是一个小写字母a。由于贪婪匹配在匹配到最后一个数字后,发现最后一个字符是a,不可以知足b的匹配,因此开始进行回溯。虽然咱们知道就算进行了回溯也不会匹配成功了,可是运行的程序是不知道的,因此程序会不断的回溯,一直回溯到\d*什么也不匹配,而后再次检查b,发现仍是不能够匹配。最终报告匹配失败。中间进行了大量的回溯,因此匹配的效率下降了。

对于占有优先的匹配,在第一次\d*匹配成功后,发现后面的a不可以知足b的匹配,因此当即报告失败,匹配效率比较高。可是由于JavaScript不支持占有优先固化分组,因此咱们使用了确定的顺序环视来替代,可是由于\1须要进行接下来的匹配,也会消耗一些时间。因此这个测试的结果不可以严格意义上代表占有优先贪婪匹配在这种状况下的效率高,可是若是模拟的占有优先消耗的时间比较短,那就能够说明占有优先确实比贪婪匹配在这种状况下的效率高。

我首先在node.js环境中运行,我本地的node.js版本为v12.16.1,系统为macOS。程序运行的结果以下:

匹配优先: 1.080ms
模拟的占有优先: 0.702ms

这个结果只是其中一次的运行结果,运行不少次后发现匹配优先的耗时要比咱们模拟的占有优先多一些,但也有几回的运行时间是小于模拟的占有优先的。我把相同的代码也放在了Chrome浏览器中运行了屡次,发现匹配优先的耗时有时比模拟占有优先高,有时比模拟占有优先低,不是很好作判断。

JavaScript中不可以很好地反应这两种匹配方式的效率高低,因此咱们须要在PHP中再次进行试验。由于PHP是原生的支持占有优先匹配的,因此比较的结果是有说服力的。咱们使用PHP的代码实现上面相同的逻辑,代码以下:

// 贪婪匹配
$greedy_reg     = '/\d*b/';
// 占有优先
$possessive_reg = '/\d*+b/';

// 待测试字符串
$str = implode(array_fill(0, 1000, 0)) . 'a';

// 计算贪婪匹配花费的时间
$t1 = microtime(true);
preg_match($greedy_reg, $str);
$t2 = microtime(true);
echo '贪婪匹配运行的时间:' . ($t2 - $t1) * 1e3 . 'ms';

echo PHP_EOL;

// 计算占有优先匹配花费的时间
$t3 = microtime(true);
preg_match($possessive_reg, $str);
$t4 = microtime(true);
echo '占有优先匹配运行的时间:' . ($t4 - $t3) * 1e3 . 'ms';

能够看到运行的结果以下:

贪婪匹配运行的时间:0.025033950805664ms
占有优先匹配运行的时间:0.0071525573730469ms

若是你将这段代码运行屡次的话,能够看到占有优先匹配所花费的时间的确是比贪婪匹配要少一些的,因此上面的代码能够说明,在这种状况下占有优先匹配的效率是比贪婪匹配的效率高的。

关于正则表达式的占有优先匹配和固化分组的讲解到这里就结束啦,若是你们有什么疑问和建议均可以在这里提出来。欢迎你们关注个人公众号关山不难越,咱们一块儿学习更多有用的正则知识,一块儿进步。

参考资料:

相关文章
相关标签/搜索