理解正则表达式中的负向前瞻

时间:2013-08-18 15:25:58

标签: ruby regex

我希望使用Ruby regex匹配不包含字符串'localhost'的网址

基于答案和评论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  

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

有人可以为我分解这些表达方式,以及它们之间的区别吗?

4 个答案:

答案 0 :(得分:5)

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

^(?!.*localhost).*$ 

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

现在另一个

^((?!localhost).)*$

这一次,前瞻只检查当前位置(里面没有.*)。 每个字符都会重复前瞻。这样它可以再次检查每个位置。以下是大致发生的情况:^确保我们再次从字符串的开头开始。前瞻检查是否在该位置找到了单词localhost。如果没有,一切都很好,.消耗一个字符。 *然后重复这两个步骤。我们现在是字符串中的一个字符,并且前瞻检查第二个字符是否开始单词localhost - 如果不是,则一切都很好,.消耗另一个字符。这是为字符串中的每个字符完成的,直到我们到达结尾。

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

答案 1 :(得分:3)

您可以轻松解释onlinefirst

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
--------------------------------------------------------------------------------

答案 2 :(得分:3)

作为旁听,这两种解决方案都很慢。更好的方法是使用:

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

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

这样可以获得更好的结果,因为您无需检查每个位置。 (对于像http://localhost:1234/toto这样的网址,这种模式会在~15步中失败,对于其他两种模式会失败~50步

您可以使用原子组和占有量词来改进此模式以禁止回溯:

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

请注意,在您的特定情况下,考虑到您只想检查网址的主机部分,您可以加快模式。例如:

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

答案 3 :(得分:2)

  

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

[^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 (向前看或看后方)时,99%的时间有更简单的方法来做你想要的。例如,你可以写:

url = "....."

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

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