贪婪vs.勉强vs.占有量词


357

我找到了这本关于正则表达式的出色教程,尽管我直观地理解“贪婪”,“勉强”和“可能”量词的作用,但我的理解似乎还存在严重的漏洞。

具体来说,在以下示例中:

Enter your regex: .*foo  // greedy quantifier
Enter input string to search: xfooxxxxxxfoo
I found the text "xfooxxxxxxfoo" starting at index 0 and ending at index 13.

Enter your regex: .*?foo  // reluctant quantifier
Enter input string to search: xfooxxxxxxfoo
I found the text "xfoo" starting at index 0 and ending at index 4.
I found the text "xxxxxxfoo" starting at index 4 and ending at index 13.

Enter your regex: .*+foo // possessive quantifier
Enter input string to search: xfooxxxxxxfoo
No match found.

解释提到了吃掉整个输入字符串,字母被消耗,匹配器退回,最右端出现的“ foo”已被反省,等等。

不幸的是,尽管有很好的隐喻,但我仍然不知道被谁吃掉了什么...您知道另一本教程(准确地)解释正则表达式引擎如何工作吗?

或者,如果有人可以用不同的措辞解释下一段,那将不胜感激:

第一个示例使用贪婪量词。*查找“任何”,零次或更多次,后跟字母“ f”,“ o”,“ o”。由于量词是贪婪的,因此表达式的。*部分首先会吃掉整个输入字符串。此时,整体表达式无法成功,因为最后三个字母(“ f”,“ o”,“ o”)已经被消耗(由谁来使用?)。因此,匹配器一次(从右到左?)缓慢退回一个字母,直到最右边的“ foo”出现反响(这是什么意思?),这时匹配成功并且搜索结束。

但是,第二个示例是不情愿的,因此它从首先消耗(由谁?)“什么都没有”开始。因为“ foo”没有出现在字符串的开头,所以它被迫吞下(吞了?)第一个字母(“ x”),从而触发0和4处的第一个匹配。我们的测试工具继续进行此过程直到输入字符串用尽。它在4和13找到另一个匹配项。

第三个示例找不到匹配项,因为量词是所有格。在这种情况下,整个输入字符串将由。* +(如何使用)消耗掉,而在表达式末尾不留任何内容来满足“ foo”。对于希望在不退缩的情况下抓住所有东西的情况,请使用所有格量词(backful意味着什么?);如果没有立即找到匹配项,它将胜过等效的贪婪量词。


22
最大量词喜欢*+?贪婪的。 最小的量词喜欢*?+???懒惰。 占有欲量词喜欢*+++?+粘性。
tchrist 2012年

6
该问题已添加到“ 堆栈溢出正则表达式常见问题 ”中的“量词>关于差异的更多信息...”下。
aliteralmind 2014年

有趣的是:Java™教程- 贪婪,勉强和所有格量词之间的差异 -向下滚动以查看相关部分。
Guy Coder

Answers:


495

我会试一试。

一个贪婪的量词第一场比赛尽可能。因此,.*匹配整个字符串。然后,匹配器尝试匹配f以下内容,但没有剩余字符。因此它“回溯了”,使贪婪的量词与一个少字符匹配(在字符串末尾的“ o”不匹配)。这仍然f与正则表达式中的不匹配,因此它又回退了一步,使贪婪的量词再次匹配少一个字符(在字符串末尾保留“ oo”不匹配)。这仍然f与正则表达式中的不匹配,因此它又回退了一步(使字符串末尾的“ foo”不匹配)。现在,匹配器最终f在正则表达式中匹配,oo也被匹配。成功!

一个不愿意或“非贪婪”量词第一场比赛尽可能少。因此,.*匹配一开始没有任何匹配,整个字符串都不匹配。然后,匹配器尝试匹配f以下内容,但字符串的不匹配部分以“ x”开头,因此不起作用。因此,匹配器回溯,使非贪婪的量词再匹配一个字符(现在它匹配“ x”,而使“ fooxxxxxxfoo”不匹配)。然后,它尝试匹配f成功的,正则表达式中的o和下一个o也匹配。成功!

在您的示例中,然后按照相同的过程,以字符串“ xxxxxxfoo”的其余不匹配部分重新开始该过程。

一个占有欲量词就像贪婪的量词,但它不会走回头路。因此,它从.*匹配整个字符串开始,没有任何不匹配的内容。这样就没有任何东西可以与f正则表达式中的匹配。由于所有格量词不会回溯,因此匹配失败。


15
+1好答案。我只会添加:去阅读Mastering Regular Expressions(第3版)
ridgerunner 2011年

@Anomie有点晚了,但是在所有格部分,我想你的意思是,所以它开始于 .*+(请注意“ +”)
RD

3
所有格量词究竟是做什么的?如果不匹配呢?(我的意思是,如果它后面不能有字符,那么它的意义是什么)
2013年

4
@relipse:您会在知道回溯无济于事的情况下使用它,可能无法满足.*+所有要求。例如,如果您有一个pattern [xyz]*foo,则无法通过回溯该[xyz]*位匹配的x,y和z来匹配后面的foo位,因此您可以通过使其具有占有性来加快处理速度。
Anomie

4
@moodboom,曾经有过零种情况(数学事实),所有格量词将产生一个匹配,而简单的贪婪量词不会产生这种匹配。有时候,当贪婪的量词产生匹配时,它们将产生匹配。 对于所有其他情况(贪婪和所有格产生相同的结果),所有格量词都会提高性能。
通配符

49

这只是我的练习输出,可以将场景可视化-

视觉影像


3
除了我认为最后一种情况(所有格),不应该进行n次传递-只需立即抓住整个字符串。
好好对待您的mods

@phyzome我认为现在可以了吗?
SIslam '17

1
感谢您的直观说明:)
Lars Moelleken '19

EXPRESSION .*?foo()中,[f] [o] [o]矩形不应该在黄色处显示5th pass吗?
tonix

1
@tonix是的!表达.*?foo和匹配的部分需要黄色.*+foo
SIslam

24

我之前从未听说过“反省”或“退缩”的确切术语;替换这些词组的短语是“回溯”,但“反驳”似乎与“在回溯之前将其暂定地将其再次丢弃的内容”一样好。

对于大多数正则表达式引擎而言,重要的一点是它们正在回溯:它们将暂时接受潜在的部分匹配,同时尝试匹配正则表达式的全部内容。如果正则表达式不能完全在第一次尝试匹配,则正则表达式引擎将回溯它的比赛之一。它会尽量匹配*+?,交替,或{n,m}反复不同,然后再试一次。(是的,此过程可能需要很长时间。)

第一个示例使用贪婪量词。*查找“任何”,零次或多次,后跟字母“ f”,“ o”,“ o”。由于量词是贪婪的,因此表达式的。*部分首先会占用整个输入字符串。此时,整体表达式无法成功,因为最后三个字母(“ f”,“ o”,“ o”)已经被消耗(由谁来使用?)。

最后三个字母,fo,并且o已经由最初的消耗.*规则的一部分。但是,正则表达式中的下一个元素在f输入字符串中没有剩余任何内容。引擎将被迫在其初始匹配项上回溯.*,并尝试匹配最后一个字符。(这可能很聪明,并且可以追溯到最后三个,因为它有三个字面意义,但是我不知道该级别的实现细节。)

因此,匹配器一次(从右到左?)缓慢退回一个字母,直到最右端的“ foo”出现反响(这是什么意思?),此时

这意味着foo已经初步得到了包括时匹配.*。由于该尝试失败,因此正则表达式引擎尝试接受中的少一个字符.*。如果曾有过一个成功的比赛之前,.*这个例子中,那么发动机可能会尝试缩短.*匹配(从右到左,正如你所指出的,因为它是一个贪婪的资格),如果无法匹配整个的投入,那么它可能会被迫重新评估它匹配了之前.*在我假设的例子。

点匹配成功,搜索结束。

但是,第二个示例是不情愿的,因此它从首先消耗(由谁来)“什么都没有”开始。因为“ foo”

最初没有消耗任何东西.?*,这将消耗最短数量的任何东西,使正则表达式的其余部分匹配。

没有出现在字符串的开头,而是被强制吞下(吞了?)

.?*在回溯初始失败以使最短匹配项匹配整个正则表达式之后,再次消耗第一个字符。(在这种情况下,正则表达式引擎将匹配.*?从左到右扩展,因为.*?它很不情愿。)

第一个字母(“ x”),它会在0和4处触发第一个匹配项。我们的测试工具将继续该过程,直到输入字符串用尽。它在4和13找到另一个匹配项。

第三个示例找不到匹配项,因为量词是所有格。在这种情况下,整个输入字符串将由。* +使用((如何?

当正则表达式整体上找不到匹配项时.*+,A 将消耗尽可能多的内容,并且不会回溯以找到新的匹配项。由于所有格形式不执行回溯,因此您可能不会看到用途很多.*+,而会遇到字符类或类似的限制:account: [[:digit:]]*+ phone: [[:digit:]]*+

这可以大大加快正则表达式的匹配速度,因为您要告诉正则表达式引擎,如果输入不匹配,则绝不应该回退潜在的匹配项。(如果必须手动编写所有匹配的代码,这类似于从未使用putc(3)过“推回”输入字符。这与人们可能在第一次尝试中编写的幼稚代码非常相似。除了regex引擎是比回推的单个字符更好,它们可以将所有回推到零,然后重试。:)

但是,除了潜在的加速效果之外,这还可以使您编写与需要匹配的正则表达式完全匹配的正则表达式。我在举一个简单的示例时遇到了麻烦:),但是使用所有格与贪婪量词编写正则表达式可以为您提供不同的匹配项,而另一种可能更合适。

在表达式的末尾不留任何内容来满足“ foo”。对于希望在不退缩的情况下抓住所有东西的情况,请使用所有格量词(backful意味着什么?);它将胜过

在这种情况下,“退避”意味着“回溯”-丢弃暂定的部分比赛以尝试另一个可能成功也可能不成功的部分比赛。

如果没有立即找到匹配项,则是等效的贪婪量词。


2
我怀疑永远不会有所有格量词与贪婪量词不能匹配的东西匹配。我相信以下内容可以证明这一点:贪婪的量词始终尽可能匹配,如果找不到匹配项,则回溯。所有格量词尽可能匹配,如果找不到匹配项则退出。因此,贪婪的量词可能会与所有格的量词匹配,而反之则不是,因为它们都以相同的顺序搜索“树”,所以所有格的量词更容易放弃。;)
通配符

2
证实:“这就是原子分组和所有格修饰符的作用:通过禁止回溯来提高效率。” 从regular-expressions.info 所以在这个答案的声明“但更重要的潜在速度提升,这也可以让你写匹配你需要匹配什么是regexs。” 实际上不是很准确。
通配符

1
@Wildcard,感谢您的评论;这可以解释为什么我在举一个例子时遇到麻烦。呵呵。
萨尔诺德

19

http://swtch.com/~rsc/regexp/regexp1.html

我不确定这是Internet上最好的解释,但是它写得不错并且适当详细,我会继续讨论。您可能需要检查一下。

如果您需要更高级别的解释(较少详细说明),则对于简单的正则表达式(例如您正在查看的正则表达式),正则表达式引擎可以通过回溯来工作。本质上,它选择(“吃”)字符串的一部分,然后尝试将正则表达式与此部分匹配。如果匹配,那就太好了。如果不是,引擎将更改其对字符串部分的选择,并尝试将正则表达式与该部分匹配,依此类推,直到尝试了所有可能的选择。

此过程是递归使用的:为了尝试将字符串与给定的正则表达式匹配,引擎会将正则表达式拆分为多个片段,并将算法分别应用于每个片段。

贪婪的,勉强的和所有格的量词之间的区别是在引擎选择要尝试匹配的字符串的哪一部分以及如果第一次不起作用时如何修改该选择时输入的。规则如下:

  • 贪婪的量词告诉引擎以整个字符串(或至少所有尚未与正则表达式的前面部分匹配的字符串)开头,并检查其是否与正则表达式匹配。如果是这样,那就太好了;引擎可以继续进行其余的正则表达式。如果不是,它将再次尝试,但是从要检查的字符串部分修剪一个字符(最后一个)。如果这不起作用,则会修剪另一个字符,依此类推。因此,贪婪的量词会按照从最长到最短的顺序检查可能的匹配项。

  • 勉强的量词告诉引擎从尽可能短的字符串开始。如果匹配,则引擎可以继续;否则,引擎可以继续运行。如果不是,它将在要检查的字符串部分添加一个字符并尝试执行该操作,依此类推,直到找到匹配项或整个字符串已用完。因此,勉强的量词按从最短到最长的顺序检查可能的匹配项。

  • 所有格量词就像第一次尝试时的贪婪量词:它告诉引擎通过检查整个字符串来启动。不同之处在于,如果它不起作用,所有格量词会立即报告匹配失败。引擎不会更改正在查看的字符串部分,也不会进行任何尝试。

这就是为什么所有格修饰符匹配在您的示例中失败的原因:.*+针对匹配的整个字符串检查了获取,但是随后引擎继续寻找其他字符foo-但当然找不到它们,因为您已经在字符串末尾了。如果它是贪婪的量词,它将回溯并尝试将.*唯一的匹配项匹配到倒数第二个字符,然后再匹配第三个字符到最后一个字符,然后再匹配第四个字符,这会成功,因为只有这样在“吃掉”字符串的较早部分foo之后,还有一个左键.*


1
这是一个很好的来源。我喜欢状态机图。:)
Regex Rookie

@Regex Rookie:很高兴您喜欢它:)但是,在查看了该站点之后,我想我应该明确其目的是促进正则表达式引擎的替代实现。我(部分地)和其他答案描述的回溯算法是缓慢的方法;这是与网页中描述的NFA / DFA想法完全不同的算法。回溯只是更容易理解,这就是为什么正则表达式通常向初学者解释的原因。
David Z

@David Zaslavsky:很好的解释。重要的是,您在“贪婪的量词告诉引擎从整个字符串开始(或至少所有尚未与正则表达式的前面部分匹配的字符串)开始”一栏中的注释非常重要。它们也适用于勉强和所有格的量词。这使您的解释与我们将示例模式从(“。* foo”;“。*?foo”;和“。* + foo”)更改为(“ foo。*”;“ foo。*? ”和“ foo。* +”)。
约翰·本特利

实际上,xfooxxxxxxfoo在正则表达式的正常(计算机科学意义)中确实匹配。* foo。NFA会是一个状态,它会在任意字符之间循环,然后可以跳转到foo。DFA是该NFA的直接翻译。它可以在8个州中完成。
user4951 2015年

@JimThio是的,因为那不是所有格量词。
David Z

12

这是我使用单元格和索引位置的看法(请参见此处图,以区分单元格和索引)。

贪婪-尽可能匹配贪婪量词和整个正则表达式。如果没有匹配项,则在贪婪量词上回溯。

输入字符串: xfooxxxxxxfoo正则
表达式:。* foo

上面的正则表达式包含两个部分:
(i)'。*'和
(ii)'foo'

下面的每个步骤将分析这两个部分。大括号内说明了与“通过”或“失败”匹配的其他注释。

步骤1:
(i)。* = xfooxxxxxxfoo-通过('。*'是一个贪婪的量词,它将使用整个输入字符串)
(ii)foo =索引13之后没有字符可匹配-失败
匹配失败。

步骤2:
(i)。* = xfooxxxxxxfo-通过(贪婪量词“。*”上的回溯)
(ii)foo = o-失败
匹配失败。

步骤3:
(i)。* = xfooxxxxxxf-通过(在贪婪量词'。*'上回溯)
(ii)foo = oo-失败
匹配失败。

步骤4:
(i)。* = xfooxxxxxx-通过(在贪婪量词'。*'上回溯)
(ii)foo = foo-通过
报告匹配

结果:1个匹配项
我找到了从索引0开始到索引13结束的文本“ xfooxxxxxxfoo”。

勉强的-尽可能少地匹配勉强的量词,并匹配整个正则表达式。如果不匹配,则将字符添加到勉强的量词中。

输入字符串: xfooxxxxxxfoo正则
表达式:。*?foo

上面的正则表达式包含两个部分:
(i)'。*?' 和
(ii)'foo'

步骤1:
。*?=”(空白)-通过(与尽可能少的量词'。*?'匹配。索引0中带有“的匹配”。)
foo = xfo-FAIL(单元格0,1,2,即介于0和3)
匹配失败。

步骤2:
。*?= x-通过(将字符添加到勉强的量词'。*?'。具有'x'的单元格0是一个匹配项。)
foo = foo-通过
报告匹配

步骤3:
。*?=”(空白)-通过(与尽可能少的勉强量词“。*?”匹配。索引4中带有“”是匹配项。)
foo = xxx-失败(单元格4,5,6-即介于4和7)
比赛失败。

步骤4:
。*?= x-通过(将字符添加到勉强的量词'。*?'。单元格4。)
foo = xxx-失败(单元格5,6,7-索引在5到8之间)
匹配失败。

步骤5:
。*?= xx-通过(将字符添加到勉强的量词'。*?'。单元格4到5。)
foo = xxx-失败(单元格6,7,8-索引在6到9之间)
匹配失败。

步骤6:
。*?= xxx-通过(将字符添加到勉强的量词'。*?'。单元格4到6。)
foo = xxx-失败(单元格7,8,9-索引在7到10之间)
匹配失败。

步骤7:
。*?= xxxx-通过(将字符添加到勉强的量词'。*?'。单元格4到7。)
foo = xxf-失败(单元格8,9,10-即介于8和11之间的索引)
匹配失败。

步骤8:
。*?= xxxxx-通过(将字符添加到勉强的量词'。*?'。单元格4到8。)
foo = xfo-失败(单元格9,10,11-索引在9到12之间)
匹配失败。

步骤9:
。*?= xxxxxx-通过(将字符添加到勉强的量词'。*?'。单元格4到9。)
foo = foo-通过(单元格10,11,12-即介于10和13之间的索引)
报告匹配

步骤10:
。*?=''(空白)-通过(与不情愿的量词'。*?'尽可能少地匹配。索引13为空。)
foo =没有字符可匹配-失败(索引13之后没有任何可匹配的字符)
匹配失败了

结果:2个匹配项
我发现文本“ xfoo”从索引0
开始到索引4结束。我发现文本“ xxxxxxfoo”从索引4开始到索引13结束。

拥有-尽可能匹配所有格量词并匹配整个正则表达式。不要回溯。

输入字符串: xfooxxxxxxfoo正则
表达式:。* + foo

上面的正则表达式包含两个部分:“。* +”和“ foo”。

步骤1:
。* + = xfooxxxxxxfoo-通过(尽可能与所有格修饰符'。*'
匹配)foo =没有字符可以匹配-失败(索引13之后没有匹配)
匹配失败。

注意:不允许回溯。

结果: 0场比赛


1

贪婪:“匹配最长的字符序列”

勉强:“匹配尽可能短的字符序列”

拥有:这有点奇怪,因为它没有(与贪婪和勉强相反)尝试为整个正则表达式寻找匹配项。

顺便说一句:没有任何正则表达式模式匹配器实现将使用回溯。所有现实生活中的模式匹配器都非常快-几乎与正则表达式的复杂性无关!


据我所知,现在大多数通用实现都被打包成如此多的功能,因此不使用回溯成为不可能。因此,从理论上讲,它们在某些情况下应非常慢(呈指数级)。但是对于大多数情况,模式匹配器中都内置了特殊的优化。
罗伯特

0

贪婪量化涉及在迭代过程中使用字符串的所有其余未验证字符进行模式匹配。未经验证的字符从活动序列开始。每次不进行匹配时,末尾的字符将被隔离,并再次执行检查。

当活动序列仅满足正则表达式模式的前导条件时,将尝试针对隔离区验证其余条件。如果此验证成功,则隔离区中的匹配字符将得到验证,剩余的不匹配字符将保持未验证状态,并将在下一次迭代中重新开始该过程时使用。

字符流是从活动序列进入隔离区的。结果是,匹配中包含了尽可能多的原始序列。

勉强量化与贪婪限定基本相同,不同之处在于字符流是相反的-也就是说,它们从隔离区开始并流入活动序列。结果是,匹配中包含的原始序列越少越好。

所有权量化没有隔离区,包括固定激活序列中的所有内容

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.