Ruby 1.9.3中未实现带有数量限制的通用占有量词{m,n}+?

7
所有权量词是贪婪的并且拒绝回溯。正则表达式/.{1,3}+b/应该表示: 匹配除换行符以外的任何字符,1到3次,尽可能多地匹配,并且不回溯。然后匹配字符b
在这个例子中:
'ab'.sub /.{1,3}+b/, 'c'    #=> "c"

不应该进行替换,与事实相反。

这两个例子的结果不同:

'aab'.sub /.{0,1}+b/, 'c'   #=> "c"
'aab'.sub /.?+b/, 'c'       #=> "ac"

与之相比,Scala 给出了相同的答案:
scala> ".{0,1}+b".r.replaceAllIn("aab", "c")
res1: String = ac
scala> ".?+b".r.replaceAllIn("aab", "c")
res2: String = ac

这是一个Ruby的bug吗?还是有可能激发这种行为?也许,Oniguruma以某种原因使用了所有量词(?*+),除了通用量词{m,n}?如果是这样,为什么呢?


我不确定Oniguruma为什么禁用了范围量词的占有属性 - 或许你应该问相关人员。 - nhahtdh
2个回答

5

真正发生的事情

看起来,加号后跟范围量词不会为范围量词提供“占有”属性。相反,它被视为前面的任何内容重复一次或多次。以.{1,3}+b为例,它将等同于(?:.{1,3})+b

解决方法

您可以使用更通用的构造非回溯组(或原子分组)(?>pattern) 来解决这个问题。让我们以一般情况 pattern{n,m}+ 为例,构造与非回溯组等效的正则表达式(相当于在 Java 中使用 pattern{n,m}+ 进行匹配时的行为):

(?>(?>pattern){n,m})

为什么需要两个级别的非回溯组? 需要2个原因:

  • 当找到pattern的匹配项时(一个重复实例),在pattern内部进行回溯是不允许的。 (请注意,只要未找到实例,就可以在pattern中进行回溯)。这是使用内部非回溯组模拟的。
  • 当不能再找到pattern的其他实例时,禁止回溯以删除任何实例。 这是使用外部非回溯组模拟的。

我不确定是否有任何警告。 如果您发现任何未使用此方法模拟的情况,请用评论提醒我。

测试

测试1

首先,我测试了这个正则表达式:

(.{1,3}+)b

一开始我测试时没有使用捕获组,但结果出乎意料,所以我需要使用捕获组来确认发生了什么。

在这个输入上:

2343333ab

结果是整个字符串匹配,捕获组捕获了2343333a(末尾没有b)。这表明上限已被突破。

rubular演示

测试2

第二个测试显示了范围量词{n}的行为不能被修改为贪婪模式,而且很可能对其他范围量词{n,}{n,m}也适用。相反,下面的+将只表现出重复1次或更多次的行为。

(我的初步结论是+会覆盖上限,但事实证明我是错的)。

测试正则表达式:

(.{3}+)b

输入字符串:

23d4344333ab
234344333ab
23434433ab

捕获组1中捕获的匹配项都是3的倍数。从上到下,正则表达式分别跳过输入字符串的2、1、0个字符。

带注释的输入字符串([]表示整个正则表达式的匹配项,()表示捕获组1捕获的文本):

23[(d4344333a)b]
2[(34344333a)b]
[(23434433a)b]

在 rubular 上尝试 DEMO

解决问题的测试代码

这是 Java 中的测试代码,以显示内部和外部不回溯组均为必要条件。 ideone

class TestPossessive {
  public static void main(String args[]) {
    String inputText = "123456789012";
    System.out.println("Input string: " + inputText);
    System.out.println("Expected: " + inputText.replaceFirst("(?:\\d{3,4}(?![89])){2,}+", ">$0<"));
    System.out.println("Outer possessive group: " + inputText.replaceFirst("(?>(?:\\d{3,4}(?![89])){2,})", ">$0<"));
    System.out.println("Inner possessive group: " + inputText.replaceFirst("(?>\\d{3,4}(?![89])){2,}", ">$0<"));
    System.out.println("Both: " + inputText.replaceFirst("(?>(?>\\d{3,4}(?![89])){2,})", ">$0<"));

    System.out.println();

    inputText = "aab";
    System.out.println("Input string: " + inputText);
    System.out.println("Expected: " + inputText.replaceFirst(".{1,3}+b", ">$0<"));
    System.out.println("Outer possessive group: " + inputText.replaceFirst("(?>.{1,3})b", ">$0<"));
    System.out.println("Inner possessive group: " + inputText.replaceFirst("(?>.){1,3}b", ">$0<"));
    System.out.println("Both: " + inputText.replaceFirst("(?>(?>.){1,3})b", ">$0<"));
  }
}

谢谢。我得出的结论是{m,n}+变成了两个连续的量词。请看一下这两个链接之间的区别:http://rubular.com/r/sMG8aWrnYD 和 http://rubular.com/r/e9O10dNQxA - Staffan Nöteberg
@StaffanNöteberg:是的,我也意识到了一半,然后改变了我的答案。(如果你在前2-3分钟内一直在阅读我的答案)。 - nhahtdh

2
似乎这是在Oniguruma中有意为之的。文档{n,m}+、{n,}+、{n}+ 是ONIG_SYNTAX_JAVA中的占有操作符。我猜这是因为向后兼容性的原因,对吗?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接