【问题标题】:Understanding negative look aheads in regular expressions了解正则表达式中的负前瞻
【发布时间】:2013-08-20 11:25:52
【问题描述】:

我想使用 Ruby 正则表达式匹配不包含字符串 'localhost' 的网址

根据答案和 cmets here,我将两个解决方案放在一起,这两个似乎都有效:

解决方案 A:

(?!.*localhost)^.*$ 

示例:http://rubular.com/r/tQtbWacl3g

解决方案 B:

^((?!localhost).)*$ 

例如:http://rubular.com/r/2KKnQZUMwf

问题是我不明白他们在做什么。例如,根据文档,^ 可以以多种方式使用:

[^abc]  Any single character except: a, b, or c  
^ Start of line  

但我不明白它是如何在这里应用的。

谁能帮我分解一下这些表达方式,以及它们之间的区别?

【问题讨论】:

  • 你可以得到它的解释online
  • @CarlNorum wow..多么棒的链接...谢谢:)
  • @CarlNorum- 这个链接太棒了!如果你想把它放在一个答案中,我会给你信任
  • 非常有用的链接...谢谢卡罗尔
  • 太多优秀的回应 - 谢谢大家。喜欢这样..

标签: ruby regex


【解决方案1】:

在这两种情况下,^ 只是行的开头(因为它没有在字符类中使用)。由于^ 和前瞻都是零宽度断言,我们可以在第一种情况下切换它们——我认为这样更容易解释:

^(?!.*localhost).*$ 

^ 将表达式锚定到字符串的开头。然后,前瞻从该位置开始,并尝试在字符串的任何位置找到localhost(“任何位置”由.* 处理在localhost 前面)。如果可以找到 localhost,则前瞻的子表达式匹配,因此 否定 前瞻会导致模式失败。由于前瞻被相邻的^ 绑定在字符串的开头,这意味着模式整体无法匹配。但是,如果.*localhost 不匹配(因此localhost 不会出现在字符串中),则前瞻成功,.*$ 只负责匹配字符串的其余部分。

现在是另一个

^((?!localhost).)*$

这次前瞻只检查当前位置(里面没有.*)。 但是对于每个字符都会重复前瞻。这样,它会再次检查每个位置。大致如下:^ 确保我们再次从字符串的开头开始。前瞻检查是否在该位置找到单词localhost。如果不是,一切都很好,. 消耗一个字符。 * 然后重复这两个步骤。我们现在是字符串中的一个字符,前瞻检查第二个字符是否以单词 localhost 开头 - 再次,如果不是,则一切正常,. 消耗另一个字符。对字符串中的每个字符都执行此操作,直到我们到达末尾。

在这种特殊情况下,两种方法是等效的,您可以根据性能(如果重要)或可读性(如果不重要;可能是第一种)来选择一种。然而,在其他情况下,第二个变体更可取,因为它允许您对字符串的固定部分执行此重复,而第一个变体将始终检查整个字符串。

【讨论】:

  • @m.buettner- 感谢您提供出色的深入回答。但是,不清楚为什么第一个示例的前瞻需要^ - 为什么(?!.*localhost).*$ 不起作用?正则表达式默认不从字符串的开头搜索吗?
  • @Yarin 是的,但是说你的字符串localhost:80,那么正则表达式会在字符串的开头失败。但如果没有锚,可以在以后的位置再试一次(就像/foo/ 可以在"barfoobar" 中找到foo 一样)。因此引擎在下一个位置进行第二次尝试。现在l 在起始位置的左侧,localhost 再也找不到了(只剩下ocalhost),你会得到一个不想要的ocalhost:80 匹配。
  • @m.buettner- 明白了,谢谢- 不过还有一个问题:(?!.*localhost)^.*$^(?!.*localhost).*$ 如何等效?在开头加上^ 现在对我来说很有意义,但是在括号后面加上它仍然让我感到困惑。
  • @Yarin 前瞻不会推进引擎“光标”的位置。因此,在先行完成后,您仍处于与之前相同的位置(这就是他们向前看的方式)。所以无论你首先检查你是否在字符串的开头,然后是否没有localhost,反之亦然,就像说if(a && b) vs. if(b && a)
【解决方案2】:

online 可以很容易地解释它们。 first

NODE                     EXPLANATION
--------------------------------------------------------------------------------
  (?!                      look ahead to see if there is not:
--------------------------------------------------------------------------------
    .*                       any character except \n (0 or more times
                             (matching the most amount possible))
--------------------------------------------------------------------------------
    localhost                'localhost'
--------------------------------------------------------------------------------
  )                        end of look-ahead
--------------------------------------------------------------------------------
  ^                        the beginning of the string
--------------------------------------------------------------------------------
  .*                       any character except \n (0 or more times
                           (matching the most amount possible))
--------------------------------------------------------------------------------
  $                        before an optional \n, and the end of the
                           string
--------------------------------------------------------------------------------
                           ' '

还有second

NODE                     EXPLANATION
--------------------------------------------------------------------------------
  ^                        the beginning of the string
--------------------------------------------------------------------------------
  (                        group and capture to \1 (0 or more times
                           (matching the most amount possible)):
--------------------------------------------------------------------------------
    (?!                      look ahead to see if there is not:
--------------------------------------------------------------------------------
      localhost                'localhost'
--------------------------------------------------------------------------------
    )                        end of look-ahead
--------------------------------------------------------------------------------
    .                        any character except \n
--------------------------------------------------------------------------------
  )*                       end of \1 (NOTE: because you are using a
                           quantifier on this capture, only the LAST
                           repetition of the captured pattern will be
                           stored in \1)
--------------------------------------------------------------------------------
  $                        before an optional \n, and the end of the
                           string
--------------------------------------------------------------------------------

【讨论】:

    【解决方案3】:

    顺便说一句,这两种解决方案都很慢。更好的方法是使用:

    ^(?:[^l]+|l(?!ocalhost))+
    

    换句话说:不是ll的所有字符后面都没有ocalhost

    这将为您提供更好的结果,因为您不必检查每个位置。 (对于像http://localhost:1234/toto 这样的网址,这种模式将在 ~15 步内失败,而其他两种模式在 ~50 步内失败)

    您可以使用原子组和所有格量词来改进这种模式以禁止回溯:

    ^(?>[^l]++|l(?!ocalhost))++
    

    请注意,在您的特定情况下,您可以加快您的模式,因为您只想检查 url 的主机部分。示例:

    ^http:\/\/(?>[^l\s\/]++|l(?!ocalhost))++(?>\/\S*+|$)
    

    【讨论】:

    • 相当大胆的主张;)(虽然我可以看到这应该是真的)。但是,特别是对于第一个答案,如果您以实际的基准测试结果支持该声明,我更愿意。我认为一些引擎优化很可能会倾向于 OPs first 模式(对于失败的字符串)。
    【解决方案4】:

    根据文档,^ 可以以多种方式使用:

    [^abc]  Any single character except: a, b, or c   
    ^ Start of line  
    

    但我不明白它是如何在这里应用的。

    在正则表达式中

    (?!.*localhost)^.*$ 
    

    ^ 不在任何括号内,所以第二个适用。这是一个简单的例子:

    /^x/
    

    该正则表达式表示匹配行首,后跟字母 x。所以它会匹配这样的行:

     xcellent
     x-ray
    

    但是,正则表达式不会匹配行:

     axb
     excellent
    

    ...因为 x 不会直接出现在行首之后。您可能想知道为什么 'axb' 不匹配。毕竟'a'是行的开头,后面是'x'。但是,“行首”就在第一个字符的左侧,如下所示:

       |
       V
        axb
    

    ^ 被称为零宽度匹配,因为它匹配 'a' 左侧的细长条,例如在起始引号和“axb”中的“a”之间。那里实际上没有任何空间,所以 ^ 匹配 0 宽度的东西。

    这是另一个例子:

    /x^/
    

    表示匹配字符 x 后跟行首。好吧,任何行都不能先有一个 x,然后是第二个行的开头,所以这永远不会匹配任何东西。

    现在你的正则表达式:

    (?!.*localhost)^.*$
    

    与“行首”^ 一样,前瞻是零宽度。这意味着前瞻扫描字符串以查找匹配项,但当它找到匹配项时,它会返回字符串的开头,然后查找正则表达式的其余部分:

    ^.*$
    

    一个忠告,当一个正则表达式需要lookarounds(lookaheads 或lookbehinds)时,99% 的时间有更简单的方法来做你想做的事。例如,你可以写:

    url = "....."
    
    if url.index('http') == 0
       #then the line starts with 'http'
    else
       #the line doesn't start with http
    end
    

    这更容易阅读,并且不需要尝试破译复杂的正则表达式。

    【讨论】:

    • @Yarin,嘿,我在最后添加了一些建议。
    • @7stud- 是的,谢谢- 我意识到使用 Ruby else 逻辑通常是首选,但这是为了将正则表达式匹配条件列表传递给第 3 方过滤函数,所以我们没有那个选择
    • @Yarin,另外...当您使用 rubular 时,使用捕获正则表达式部分周围的括号来查看它们匹配的内容通常很有帮助。例如,如果您使用正则表达式:((?!.*localhost))(^.*$),rubular 将显示组 1 和组 2 的匹配项。请注意,组 1 的匹配项是空白的——这是因为前瞻是那些 0 宽度的东西之一真正匹配字符串中的任何字符——它只是寻找它们。 Rubular 可以改进——它可以显示前瞻是否找到了它正在寻找的东西。
    猜你喜欢
    • 2021-10-11
    • 2011-10-14
    • 2010-12-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多