贪婪模式与正则匹配过程

本文介绍了正则表达式的基本概念,并重点探讨了贪婪模式与非贪婪模式的差异,包括它们在匹配过程中的回溯行为。通过实例分析了两种模式在匹配字符串时的不同效果,强调了根据需求选择合适模式的重要性,同时提到了正则表达式在性能优化方面的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

正则表达式是计算机的一个概念,也称为规则表达式,用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,表达对字符串的一种过滤逻辑,通常被用来检索、替换符合规则的文本。
正则表达式即记录文本规则的代码。
关于正则表达式的基础知识就不在此赘述,网上教程很多,大家动动小手就能搜索到从古至今的各类资料,本文主要整理了一点关于正则引擎的知识,希望能给大家一点灵感吧。

一、什么是贪婪模式与非贪婪模式?

  • 贪婪模式

定义:默认情况下, 所有的限定词都是贪婪模式, 表示尽可能多的去捕获字符,能匹配多少就匹配多少。
从正则语法的角度来讲,被匹配优先量词修饰的子表达式使用的就是贪婪模式,如“/a+/”;
标识符:+,?,*,{n},{n,},{n,m}

  • 非贪婪模式

定义:在限定词后增加?, 则是非贪婪模式, 表示尽可能少的去捕获字符。
从正则语法的角度来讲,被忽略优先量词修饰的子表达式使用的就是非贪婪模式,如“/a+?/”;
标识符:+?,??,*?,{n}?,{n,}?,{n,m}?

示例如下:
例1:

  const str = "aaab",
        reg1 = /a+/, //贪婪模式
        reg2 = /a+?/;//非贪婪模式
  console.log(str.match(reg1));  
  // ["aaa"]
  console.log(str.match(reg2));
 // ["a"]
   贪婪模式下,用字符a匹配待检索字符串中的a,匹配成功,由于正则表达式为贪婪模式,所以用正则表达式中a去匹配字符串中下一个字符a,匹配成功,继续用a匹配字符串中下一个字符a,成功之后保持贪婪特性,用字符a匹配下一字符b,匹配失败,返回上一步结果aaa。

   非贪婪模式下,用字符a匹配待检索字符串中的a,由于正则表达式为非贪婪模式,所以尽可能少地匹配字符,返回匹配结果a。

例2:

/*表达式1*/

'aaaaaaab'.match(/a*b/); 
// ["aaaaaaab", index: 0, input: "aaaaaaab", groups: undefined]

/*表达式2*/

'aaaaaaab'.match(/a*?b/);
// ["aaaaaaab", index: 0, input: "aaaaaaab", groups: undefined]l;

可能关于表达式2我们理想的结果是ab,然而事实跟我们所想不大一致。

在本题中,有限自动机根据正则表达式2,用字符a匹配待检索字符串中的a,由于正则表达式为非贪婪模式,所以用正则表达式中b去匹配字符串中下一个字符a,没有匹配成功,继续用a匹配字符串中下一个字符a,成功之后保持非贪婪特性,用字符b匹配下一字符b,成功之后返回匹配结果。

因为在正则表达式中有个优先规则:最先开始的匹配拥有最高的优先权

看完上面的例子,再结合贪婪模式与非贪婪模式的定义,可能会产生一些疑惑,什么时候用贪婪,什么时候用非贪婪呢?不着急,让我们先来看看正则的具体匹配过程~

二、 回溯与匹配过程

  • 什么是回溯?
    当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
  • 什么是有限状态自动机?
    正则表达式是记录文本规则的代码,那它是靠什么运作的呢?
    正则表达式依靠正则引擎来发挥作用,传统正则引擎分为确定性有限状态自动机(DFA)和非确定性有限状态自动机(NFA)。自动机的实质是编译原理中词法分析中完成词法分析任务的程序,即词法分析器(扫描器)。

NFA指的是非确定自动机,对任意的字符,有多个状态可以转换。
DFA指的是确定自动机,对任意的字符,只根据当前的状态和当前输入来决定下一状态,最多只有一个状态可以转换。

  • DFA和NFA的区别
    1)由NFA、DFA的定义可知,DFA是确定性有限自动机,DFA在编译阶段要遍历出表达式中所有的可能,必须提前知道所有的可能,才能配出最优的结果。所以在编译过程NFA需要的内存更少,速度更快;

    2)同上述相对应的,在运行阶段,DFA匹配的速度更快,NFA在是否匹配之前必须尝试正则表达式的所有变体,所以引擎的运行时间一般更长(非简单文本测试);

    3)NFA引擎能提供一些DFA不支持的功能,比如回溯、非匹配优先的量词、反向引用和固化分组等,NFA引擎表达能力更强;

    不难看出,非贪婪模式、反向引用、零宽断言等只有NFA引擎才有,而DFA引擎不支持回溯,POSIX NFA引擎不支持非贪婪模式,所以JavaScript的正则引擎是传统型NFA引擎。

回顾上述例2:

/*表达式1*/
'aaaaaaab'.match(/a*b/); 
// ["aaaaaaab", index: 0, input: "aaaaaaab", groups: undefined]

/*表达式2*/
'aaaaaaab'.match(/a*?b/);
// ["aaaaaaab", index: 0, input: "aaaaaaab", groups: undefined]l;

该贪婪匹配表达式在引擎中的回溯表现如下:

  • 第一个a取得控制权,匹配正则中的a,匹配成功,控制权交给a*;
  • a*取得控制权后,由于这是贪婪模式下的标识符,因此在可匹配可不匹配的情况下会优先匹配,因此尝试匹配1处的字符a;
  • 依次成功匹配a、a、a、a、a、a,接下来依旧保持贪婪特性匹配字符串中b,匹配失败,向前查找可供回溯的状态,将控制权交给a*b;
  • a*b取得控制权后,开始匹配末尾处的b,匹配成功;
  • 至此,整个正则表达式匹配完毕,匹配结果为"aaaaaaab",匹配过程中回溯了1次。

该非贪婪匹配表达式在引擎中的回溯表现如下:

  • 第一个a取得控制权,匹配正则中的a,匹配成功,控制权交给a*?;
  • a*?取得控制权后,由于这是非贪婪模式下的标识符,因此在可匹配可不匹配的情况下会优先不匹配,因此尝试不匹配任何内容,将控制权交给b;
  • b取得控制权后,开始匹配1处的a,匹配失败,向前查找可供回溯的状态,控制权交给a*?,a*?吃进一个字符,index到了2处,再把控制权交给b;
  • b取得控制权后,开始匹配2处的b,匹配失败,重复上述的回溯过程,直到a*?匹配了6处的a字符,再将控制权交给b;
  • b取得控制权后,开始匹配7处的b,匹配成功;
  • 至此,整个正则表达式匹配完毕,匹配结果为"aaaaaaab",匹配过程中回溯了6次。

现在我们已经了解了贪婪和非贪婪的特性,在匹配成功的前提下,贪婪模式进行了更少的回溯,在确定字符不会溢出的情况下我们可以通过消除非贪婪模式来防止回溯,从而达到优化正则表达式的目的,避免过多回溯导致的性能问题。
⚠️注意:因此,使用贪婪模式还是非贪婪模式的讨论,需根据需求决定,贪婪模式经常会由于元字符范围限制不严谨而导致匹配越界,得到非预期结果,消除全部非贪婪模式并不一定是最优正则,可以在确定的数据结构里使用贪婪模式替换非贪婪模式,提升匹配效率。

三、性能问题:
1.多分支表达式匹配
现在我们已经明白了贪婪模式和非贪婪模式时, 匹配过程中的回溯,那多分支的情况下,正则引擎是怎么运作的呢?再看个例子吧。

例3:

'It will be confirmed in a subsequent console message'
.match(/message|in|console/);
// ["in", index: 21, input: "It will be confirmed in a subsequent console message", groups: undefined]

在本例中,匹配过程如下:

  • 正则多选分支/message|in|console/从左边开始匹配,第一个message取得控制权,匹配字符串中的I,匹配失败,返回检查点进行回溯,控制权交给in;
  • in取得控制权后,匹配字符串中的I,匹配成功,在1处匹配失败,进行回溯,控制权交给console;
  • 同上,console匹配失败,则在待匹配字符串中后移一位,即1处字符n重新进行第一步匹配;
  • 同上,所有分支均匹配失败;
  • 由待匹配字符串和上述匹配规则可知,in将在字符串n处最早匹配成功,至此,整个正则表达式匹配完毕,匹配结果为"in"。

2.正则性能问题
综上,正则表达式的性能问题主要表现如下:

  1)在使用非贪婪模式时, 匹配过程中可能会进行多次的回溯, 回溯越多, 正则表达式的运行效率就越低;

  2)过多嵌套的分支和范围将导致生成的变体呈现指数级增长,回溯过多,导致浏览器卡死;

  所以优化正则表达式的终极策略是控制回溯。

  一则是控制表达式变体的数量;

  二则是调整表达式的顺序,越明确越好,避免模糊匹配、贪婪匹配等带来的回溯问题。

最后以《精通正则表达式》中的序言结下尾吧。
“如果罗列计算机软件领域的伟大发明, 我相信绝对不会超过二十项, 在这个名单当中, 当然应该包括分组交换网络, Web, Lisp, 哈希算法, UNIX, 编译技术, 关系模型, 面向对象, XML这些大名鼎鼎的家伙, 而正则表达式也绝对不应该被漏掉.”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值