【问题标题】:What do we need Lookahead/Lookbehind Zero Width Assertions for?我们需要 Lookahead/Lookbehind 零宽度断言来做什么?
【发布时间】:2013-06-14 10:01:28
【问题描述】:

我刚刚更详细地了解了这两个概念。我一直很擅长使用 RegEx,而且我似乎从未见过需要这 2 个零宽度断言。

我很确定我错了,但我不明白为什么需要这些构造。考虑这个例子:

Match a 'q' which is not followed by a 'u'.

2 个字符串将作为输入:

Iraq
quit

使用负前瞻,正则表达式如下所示:

q(?!u)

没有它,它看起来像这样:

q[^u]

对于给定的输入,这两个正则表达式给出相同的结果(即匹配 Iraq 但不匹配 quit)(用 perl 测试)。同样的想法也适用于lookbehinds。

我是否缺少使这些断言比经典语法更有价值的关键特性?

【问题讨论】:

  • 如果我想检查foo 之后是否没有bar 怎么办,这将导致foo[^b][^a][^r](并消耗它),这对于foo(?!bar) 和其他方式来说非常容易可读。在 if/else 正则表达式语句中也可能很方便
  • 您是否使用<,或>\b 字边界匹配?它们是前瞻、后瞻的简写:< == (?<!\w)(?=\w); > == (?<=\w)(?!\w)\b == (?:(?<!\w)(?=\w)|(?<=\w)(?!\w))。虽然我猜它们针对特定功能进行了优化。
  • 有点离题:这个问题现在在谷歌中排在“伊拉克退出比赛”的第二位
  • 是的,我以前用过它们,尤其是单词边界。

标签: regex regex-lookarounds


【解决方案1】:

为什么您的测试可能有效(以及为什么不应该)

您能够在测试中匹配Iraq 的原因可能是您的字符串末尾包含\n(例如,如果您从shell 读取它)。如果您有一个以q 结尾的字符串,那么q[^u] 不能像其他人所说的那样匹配它,因为[^u] 匹配一个非u 字符-但关键是必须有一个字符。

我们实际上需要环视什么?

显然,在上述情况下,前瞻并不重要。您可以使用q(?:[^u]|$) 解决此问题。因此,我们仅在 q 后跟非u 字符或字符串结尾时才匹配。不过,前瞻还有更复杂的用途,如果您在没有前瞻的情况下使用它们,这将变得很痛苦。

此答案试图概述一些重要的标准情况,这些情况最好通过环视来解决。

让我们从查看带引号的字符串开始。匹配它们的常用方法是使用"[^"]*" 之类的东西(not".*?")。在开头的" 之后,我们只需重复尽可能多的非引号字符,然后匹配结束引号。再一次,一个否定的字符类是非常好的。但是在某些情况下,否定的字符类不会削减它:

多字符分隔符

现在如果我们没有双引号来分隔我们感兴趣的子字符串,而是一个多字符分隔符。例如,我们正在寻找---sometext---,其中sometext 内允许单- 和双-。现在你不能只使用[^-]*,因为那会禁止单个-。标准技术是在每个位置使用负前瞻,并且只使用下一个字符,如果它不是--- 的开头。像这样:

---(?:(?!---).)*---

如果您以前没有见过,这可能看起来有点复杂,但它肯定比其他替代方案更好(而且通常更有效)。

不同的分隔符

您会遇到类似的情况,您的分隔符只有一个字符,但可能是两个(或更多)不同字符之一。例如,在我们最初的示例中,我们希望允许单引号和双引号字符串。当然,您可以使用'[^']*'|"[^"]*",但最好在没有替代方案的情况下处理这两种情况。使用反向引用可以轻松处理周围的引号:(['"])[^'"]*\1。这可以确保匹配以它开始的相同字符结束。但是现在我们的限制太严格了——我们希望" 在单引号中,' 在双引号中。 [^\1] 之类的东西不起作用,因为反向引用通常包含多个字符。所以我们使用与上面相同的技术:

(['"])(?:(?!\1).)*\1

即在开头引号之后,在使用每个字符之前,我们确保它与开头字符不同。我们尽可能长时间地这样做,然后再次匹配开始字符。

重叠匹配

这是一个(完全不同的)问题,如果不进行环顾,通常根本无法解决。如果您在全局范围内搜索匹配项(或想要在全局范围内进行正则表达式替换),您可能已经注意到匹配项永远不会重叠。 IE。如果你在abcdefghi 中搜索...,你会得到abcdefghi 而不是bcdcde 等等。如果你想确保你的匹配在其他东西之前(或包围),这可能是个问题。

假设你有一个类似的 CSV 文件

aaa,111,bbb,222,333,ccc

并且您只想提取完全是数字的字段。为简单起见,我假设任何地方都没有前导或尾随空格。如果没有环视,我们可能会进行捕获并尝试:

(?:^|,)(\d+)(?:,|$)

所以我们确保我们有一个字段的开头(字符串开头或,),然后只有数字,然后是字段的结尾(, 或字符串结尾)。在这之间,我们将数字捕获到1 组中。不幸的是,这不会在上面的例子中给我们333,因为它之前的,已经是匹配,222,的一部分——并且匹配不能重叠。环顾四周解决问题:

(?<=^|,)\d+(?=,|$)

或者如果你更喜欢双重否定而不是交替,这相当于

(?<![^,])\d+(?![^,])

除了能够获取所有匹配项之外,我们还摆脱了通常可以提高性能的捕获。 (感谢 Adrian Pronk 提供的示例。)

多个独立条件

另一个使用lookarounds(特别是lookaheads)的经典例子是当我们想要同时检查输入的多个条件时。假设我们要编写一个正则表达式,以确保我们的输入包含一个数字、一个小写字母、一个大写字母、一个不是这些字符的字符,并且没有空格(例如,为了密码安全)。如果没有环视,您必须考虑数字、小写/大写字母和符号的所有排列。喜欢:

\S*\d\S*[a-z]\S*[A-Z]\S*[^0-9a-zA_Z]\S*|\S*\d\S*[A-Z]\S*[a-z]\S*[^0-9a-zA_Z]\S*|...

这些只是 24 种必要排列中的两种。如果您还想确保同一正则表达式中的最小字符串长度,则必须将它们分布在 \S* 的所有可能组合中 - 在单个正则表达式中根本不可能做到。

期待救援!我们可以简单地在字符串的开头使用几个前瞻来检查所有这些条件:

^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^0-9a-zA-Z])(?!.*\s)

因为前瞻实际上不消耗任何东西,所以在检查每个条件后,引擎会重置到字符串的开头,并可以开始查看下一个条件。如果我们想添加一个最小字符串长度(比如8),我们可以简单地追加(?=.{8})。更简单、更易读、更易于维护。

重要提示:不是在任何实际环境中检查这些条件的最佳通用方法。如果您以编程方式进行检查,通常最好为每个条件使用一个正则表达式,并分别检查它们 - 这让您返回更有用的错误消息。但是,如果您有一些固定的框架允许您仅通过提供单个正则表达式来进行验证,则上述内容有时是必要的。此外,如果您有独立的字符串匹配标准,则值得了解一般技术。

我希望这些示例能让您更好地了解人们为什么喜欢使用环视。还有更多的应用程序(另一个经典是inserting commas into numbers),但重要的是您要意识到(?!u)[^u] 之间存在差异,并且在某些情况下否定字符类根本不够强大。

【讨论】:

  • 重叠匹配:提取 CSV 行中的字段。例如,所有数字字段:@numbers = 'aaa,111,bbb,222,ccc' =~ /(?&lt;![^,])([^,]*)(?![^,])/g.
  • @AdrianPronk 谢谢!我对其稍作修改并改用它。
  • @0xCAFEBABE 添加了另一个我完全忘记的非常重要的使用场景
【解决方案2】:

q[^u] 不会匹配“伊拉克”,因为它会寻找另一个符号。

但是,q(?!u) 将匹配“伊拉克”:

regex = /q[^u]/
/q[^u]/
regex.test("Iraq")
false
regex.test("Iraqf")
true
regex = /q(?!u)/
/q(?!u)/
regex.test("Iraq")
true

【讨论】:

    【解决方案3】:

    好吧,除了其他人提到的否定前瞻,你可以匹配连续的字符(例如,你可以否定ui,而使用[^...],你不能否定ui,但是u 或@987654325 @,如果你尝试[^ui]{2},你也会否定uuiiiu

    【讨论】:

      【解决方案4】:

      重点是不要“消耗”下一个字符,以便它可以是例如被随后出现的另一个表达式捕获。

      如果它们是正则表达式中的 last 表达式,那么您显示的内容是等效的。

      但是例如q(?!u)([a-z]) 会让非u 字符成为下一组的一部分。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-04-26
        • 2015-08-13
        • 1970-01-01
        • 2018-01-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多