【问题标题】:Perl regular expression engine bug?Perl 正则表达式引擎错误?
【发布时间】:2017-09-21 21:27:19
【问题描述】:

我一直在尝试编写正则表达式来验证文件以确保它遵循特定格式。该文件应该有一个version(); 行,然后是一个或多个element(); 块。

下面是一个有效文件的例子:

version(1.0);

element
(
);

element
(
);

element
(
);

作为测试,我创建了以下 Perl 示例:

use strict;
use warnings;

my $text = <<'END_TEXT';
version(1.0);

element
(
);

garbage <--- THIS SHOULD NOT MATCH!

element
(
);

element
(
);

END_TEXT

my $rx_defs = qr{(?(DEFINE)
    (?<valid_text>
        \A\s*(?&version)\s*
        (?: (?&element) \s* )+
        \s*\Z
    )
    (?<version>
        version\(.+?\);
    )
    (?<element>
        element\s*
        (?&element_body);
    )
    (?<element_body>
        \( (?: [^()]++ | (?&element_body) )* \)
    )
)}xms;

if ($text =~ m/(?&valid_text)$rx_defs/) {
    print "match";
}

如您所见,文本中有一行“垃圾”应该使其无效,但由于某种原因,Perl 似乎仍然认为该文本是有效的!当我运行此代码时,它会产生输出:

match

我花了好几个小时试图找出我的正则表达式出了什么问题,但我就是没发现。我什至使用online regular expression tester 测试了确切的正则表达式,根据测试我的正则表达式应该可以正常工作! (如果您想在格式有效时查看它是否正确匹配,请尝试删除“垃圾”行。)

这让我一整天都被难住了,让我想知道 Perl 正则表达式引擎本身是否存在错误。有人可以告诉我为什么这是不应该匹配的吗?

我正在使用 perl v5.20.1

【问题讨论】:

  • 这一点看起来很可疑:version\(.+?\); - 括号可能包括一个元素和垃圾。考虑限制内容,例如到[^)]+
  • 这似乎可以解决它...但是如何解决?非贪婪匹配应在满足后立即停止!
  • 为什么这会在 perl 中匹配,但在使用“PCRE”作为正则表达式类型的正则表达式测试网站上却不匹配?它们的功能应该相同,不是吗?
  • 一个非常酷的正则表达式,毫无疑问,正则表达式是一个很棒的工具,毫无疑问......但我忍不住说:这很难,但可以通过使用工具来避免处理那些嵌套/平衡的分隔符。 (您上一个问题中this answer 的确切点。)
  • 提示:这绝不是引擎/语言/编译器错误;它始终是用户错误。好吧……除非……但那是一长串可能的行为原因列表中的最后一件事。 (我会重新考虑这样的标题;你确定吗?)

标签: regex perl validation debugging pcre


【解决方案1】:

来自http://www.pcre.org/current/doc/html/pcre2compat.html 的 PCRE 文档:

  1. 在 PCRE2 版本 10.23 之前,子例程调用(无论是否递归)被视为原子组,但从 10.30 版本开始,这一点发生了变化,现在支持回溯到子例程调用中,就像在 Perl 中一样。

regex101 使用 PHP 来运行 PCRE。根据http://php.net/manual/en/pcre.installation.php,PHP 仅支持 PCRE1(8.x 分支)。因此 regex101 不支持回溯到子程序调用中。

...这正是这里发生的事情:

  • 我们进入(?&amp;valid_text&gt;)并尝试匹配\A\s*(?&amp;version)\s*
  • \A(字符串开头)和\s*(可选空格)很简单
  • (?&amp;version) 确实 version\(.+?\);
  • 这匹配输入的以下部分:

    version();
    
    element
    (
    );
    

    version( 是字面匹配的。下一个字符).+? 消耗(至少需要一个字符才能匹配)。然后.+? 慢慢消耗越来越多的字符(它是非贪婪的),直到达到);。第一次发生这种情况是在消费 ; element ( 之后,所以我们现在就停下来。

  • (?&amp;version) 调用返回
  • 我们使用后面的任何空格
  • 下一部分是(?: (?&amp;element) \s* )+,即一个或多个元素,每个元素后跟可选的空格
  • (?&amp;element)element\s*,即它必须以 element 开头
  • 我们在输入中的当前位置是garbage ...,所以这失败了

此时正则表达式引擎尝试回溯。在 PCRE \s*(即“可选空白”位),但匹配较少的空白字符也不会导致匹配成功,因此整个事情很快就会失败。

然而,在 Perl 中,我们可以回溯到子程序调用:我们重新输入 (?&amp;version) 并让 .+? 匹配更多字符(直到找到下一次出现的 );),然后重试 (?&amp;element)。这最终让(?&amp;version) 使用garbage 和下面的element,这反过来又允许整个正则表达式成功。

有人可以告诉我为什么这是不应该匹配的吗?

我不明白你为什么认为它不应该匹配。 :-)

它在 PHP 中不匹配的唯一原因是它使用的旧 PCRE 版本的限制。

【讨论】:

  • 我试图解析的真实文本在 version(&lt;some other text here&gt;) 的括号之间有文本。看来我的例子可能有点过于简化了,但即使有其他文本用于 .+? 吞噬它仍然匹配。我猜这是正确的行为,尽管它非常令人困惑。
  • @tjwrona1992 任何时候你在正则表达式中有.*.+,这都是一个潜在的错误。 .* 可以并且将跳过任何文本,如果这是使整个正则表达式成功所需的(除非它受到 (?&gt; ) 组或 (*PRUNE) 动词或类似的限制)。注意:贪婪不会影响正则表达式是否匹配或匹配的位置;它只会影响比赛的长度。
  • 有道理,我从来没有意识到这一点,所以知道这一点真的很好。我一直认为让它不贪婪会让它停在那里。
【解决方案2】:

非贪婪匹配不会在满足后立即停止。它尝试尽快继续。如果正则表达式的其余部分无法匹配,回溯仍然会发生——但对于非贪婪量词,回溯意味着匹配更多。

避免这种情况的一种可能性在于回溯控制。例如,您可能希望在 version 最初匹配后禁止回溯。我们可以通过(?&gt; ...) 构造来做到这一点。这独立于外部模式匹配包含的模式。如果模式的其余部分失败,回溯将不会继续到包含的模式,而是会跳过整个包含的模式。描述起来有点困难,详情请见perldoc perlre

+ 添加到量词(如++?+*+)与(?&gt; ...) 具有类似的效果。在高效的正则表达式中,强烈建议优先使用这些无回溯量词和(?&gt;...) 组。

具体来说,替换

(?<valid_text>
    \A\s*(?&version)\s*
    (?: (?&element) \s* )+
    \s*\Z
)

(?<valid_text>
    \A\s*(?>(?&version))\s*
    (?: (?&element) \s* )++
    \s*\Z
)

作为另一种选择,您可以使用(*PRUNE) 回溯控制动词。一旦遇到 PRUNE 命令,就不会发生超过该点的回溯。这会将匹配提交给目前选择的替代方案。

(?<valid_text>
    \A\s*(?&version)\s* (*PRUNE)
    (?: (?&element) \s* )+
    \s*\Z
)

【讨论】:

    猜你喜欢
    • 2020-03-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-01
    • 1970-01-01
    相关资源
    最近更新 更多