【问题标题】:Inconsistency between sed and python regular expressionssed和python正则表达式不一致
【发布时间】:2012-08-19 11:56:47
【问题描述】:

如果这是在某个地方发布的,我深表歉意,但我粗略的搜索没有找到任何东西。

在进行一些 Python 编程时,我注意到以下命令:

re.sub("a*((ab)*)b", r"\1", "aabb")

返回空字符串。但是 sed 中的等效命令:

echo "aabb" | sed "s/a*\(\(ab\)*\)b/\1/"

返回ab

对我来说,python 正则表达式开头的“a*”指令将匹配两个a,导致“(ab)*”匹配零次,但我不知道如何 sed提出ab。有谁知道导致这种情况的两个正则表达式引擎之间有什么区别?我相信默认情况下它们都贪婪地匹配星星,但我想到 sed 可能从右边而不是左边匹配。任何见解将不胜感激。

【问题讨论】:

  • 某处我读到“sed/awk 使用 DFA”和“python/perl/java 使用 NFA”。它确实通过交替(回溯?)改变语义......这可能相关吗?
  • @pst:也许我误解了你,但似乎任何基于回溯的方法都会使用 DFA;使用 NFA 的效果是消除回溯的需要,因为同时检查所有分支。所以,我期待 Perl/Python/Java/等。使用 DFA。有没有可能你读到的与你写的相反:也许你读到 sed/awk 使用 NFA 而 Perl/Python/Java 等使用 DFA?
  • @pst:如果 sed/awk 使用 NFA,它解释观察到的行为,然后选择匹配最长的匹配方式。在这种情况下,让\(\(ab\)*\) 匹配ab 会产生更长的整体匹配aabb,然后让它匹配空字符串,因为后者意味着整个正则表达式将只匹配aab。跨度>
  • @pst:对我之前的 cmets 的更正:foo.be/docs/tpj/issues/vol1_2/tpj0102-0006.html 使用术语“NFA”来描述执行回溯的正则表达式引擎,而使用“DFA”来描述不执行回溯的正则表达式引擎。现在我想起来了,我想这是有道理的;一个“NFA”引擎给自己做一个注释“我在这里做了一个不确定的决定,所以如果我回溯到这一点,我应该尝试一个不同的分支”,而一个“DFA”引擎将 NFA 预先转换为非- 通过爆炸状态数回溯 DFA。但是该页面还声称 sed 和 Perl 都使用 NFA 引擎。 :-/
  • 不,sed RE 是贪婪的。它只是遵循 POSIX 并在找到一个匹配项后不会停止。

标签: python regex sed


【解决方案1】:

默认情况下 Python 和 sed 都是贪婪的,但是... Python 正则表达式尝试在所有情况下从左到右进行评估,尽管如果正在尝试的分支无法通过匹配继续,它最终必须回溯到先前的状态。 相反,Sed 正则表达式在评估之前进行优化,以通过将正则表达式重写为更具确定性的形式来防止不必要的回溯。因此,组合的可选模式“aab”可能会在普通的“a”之前进行测试,因为最具体的可能字符串首先被尝试。

Python 模式匹配字符串“aabb”两次“aab”+“b”(标记在“”之间)

>>> re.sub("a*((ab)*)b", r"<\1>", "aabb")
'<><>'

而 sed 通过一个替换匹配整个“aabb”:

$ echo "aabb" | sed "s/a*\(\(ab\)*\)b/<\1>/"
<ab>

Python 正则表达式回溯算法在regex howto - Repeating Things 的两段中通过“一步一步的示例...”的词进行了很好的解释。它完全符合 IMO 的描述 regex docs:“当目标字符串被扫描时,RE 由 '|' 分隔从左到右尝试。”

示范

顺便说一句,“(|a|aa)”的顺序。 "(aa|a|)" 受到 Python 的尊重

>>> re.sub("(?:|a|aa)((ab)*)b", r"<\1>", "aabb")
'<ab>'
>>> re.sub("(?:aa|a|)((ab)*)b", r"<\1>", "aabb")
'<><>'

但是这个顺序被 sed 忽略,因为 sed 优化了正则表达式。匹配“aab”+“b”可以从模式中删除“a”选项来重现。

$ echo "aabb" | sed "s/\(\|a\|aa\)\(\(ab\)*\)b/<\2>/g"
<ab>
$ echo "aabb" | sed "s/\(aa\|a\|\)\(\(ab\)*\)b/<\2>/g"
<ab>
$ echo "aabb" | sed "s/\(aa\|\)\(\(ab\)*\)b/<\2>/g"
<><>

编辑:我删除了有关 DFA/NFA 的所有内容,因为我无法从当前文本中证明这一点。

【讨论】:

    【解决方案2】:

    您构建的有趣谜题。根据我的阅读,python 和 sed 的正则表达式引擎都是基于 Henry Spencer 的正则表达式库(与 perl 一样),它依赖于回溯。 (不幸的是,我找不到我以此为基础的文章)。

    无论如何,这不是应该是实现细节的东西:Python 的行为违反了 POSIX 标准,该标准要求 RE (a) 尽可能早地匹配,并且 (b)匹配从该点开始的可能最长的字符串。 (请参阅man 7 regex(在 Linux 上)了解更多信息。)

    要找到最长的匹配项,回溯(“NFA 类型”)正则表达式引擎必须在找到一个匹配项后继续检查备选方案。因此,实施者偷工减料也就不足为奇了。显然,python 的行为是不一致的,因为它无法找到最长的匹配项。根据 sed 手册页,“出于性能原因”,sed 也不总是符合要求。但很明显,这件事是正确的。

    顺便说一句,您的命令并不完全等效:re.sub 将尽可能多次执行替换,而 sed 的 s/a/b/ 只会执行一次。sed 版本应该是:

    echo "aabb" | sed "s/a*\(\(ab\)*\)b/\1/g"
    

    这解释了为什么我们在python中得到空字符串:RE第一次匹配aab,第二次匹配剩余的b,删除每个部分(因为它都被a*和最后的@987654329匹配@ 的正则表达式)。您可以通过以下变体看到这一点:

    >>> re.sub("a*((ab)*)b", r"X\1Y", "aabb")
    'XYXY'
    

    【讨论】:

    • Perl 在这种情况下与 Python 的作用相同:$ perl -E '$_ = "aabb"; s/a*((ab)*)b/&lt;\1&gt;/g; print $_, "\n";' result &lt;&gt;&lt;&gt; btw。非贪心...s/a*?... 结果&lt;ab&gt;.
    • 那又怎样? Plain * 在所有三个引擎中都是贪婪的。巧合的是,非贪婪版本在这种情况下给出了相同的结果。
    • 酷。 FWIW,告诉python匹配一次,以便两个命令等效,似乎不影响是否使用匹配组(re.sub("a*((ab)*)b","[\1]","aabb") -&gt; "[]b"),但这是一个好点。
    • 完全正确;它不会影响 RE 的应用方式,但两个替换不同于一个。 (但你一定用过re.sub("a*((ab)*)b", r"[\1]", "aabb", count=1);你引用的命令实际上匹配了两次,并且不返回"[]b"
    • Python(以及 Java、JavaScript、.NET、Perl、PHP、Ruby 等)正则表达式引擎不是 POSIX 引擎。最左边最长的规则不适用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-12-30
    • 2012-05-08
    • 2018-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-18
    相关资源
    最近更新 更多