前瞻和后视概念如何在Ruby的Regex中支持这种零宽度断言概念?

时间:2013-01-17 20:34:10

标签: ruby regex ruby-1.9.3

我刚刚从文档中了解了Zero-Width Assertions这个概念。一些快速问题进入我的脑海 -

  • 为什么这样的名字Zero-Width Assertions
  • Look-aheadlook-behind概念如何支持 Zero-Width Assertions概念?
  • 这样的?<=s<!s=s<=s - 4个符号在模式中指示了什么?你能帮助我在这里专注于了解实际情况

我还尝试了一些微小的代码来理解逻辑,但对那些输出没有那么自信:

irb(main):001:0> "foresight".sub(/(?!s)ight/, 'ee')
=> "foresee"
irb(main):002:0> "foresight".sub(/(?=s)ight/, 'ee')
=> "foresight"
irb(main):003:0> "foresight".sub(/(?<=s)ight/, 'ee')
=> "foresee"
irb(main):004:0> "foresight".sub(/(?<!s)ight/, 'ee')
=> "foresight"

有人可以帮助我理解吗?

修改

在这里,我尝试了两个片段,其中包含“零宽度断言”概念,如下所示:

irb(main):002:0> "foresight".sub(/(?!s)ight/, 'ee')
=> "foresee"

,另一个没有“零宽度断言”概念如下:

irb(main):003:0> "foresight".sub(/ight/, 'ee')
=> "foresee"

以上两者都产生相同的输出,现在内部如何regexp移动它们自己产生输出 - 你可以帮我形象化吗?

由于

3 个答案:

答案 0 :(得分:18)

正则表达式从左到右匹配,并移动一种&#34;光标&#34;沿着弦走了。如果正则表达式包含常规字符a,则表示:&#34;如果光标前面有一个字母a,请将光标向前移动一个字符,然后继续前进。否则,出了点问题;备份并尝试别的东西。&#34;所以你可能会说a有一个&#34;宽度&#34;一个角色。

A&#34;零宽度断言&#34;就是这样:它断言关于字符串的东西(即,如果某些条件不能保持不匹配),但它不会向前移动光标,因为它的宽度&#34;是零。

您可能已经熟悉了一些更简单的零宽度断言,例如^$。这些匹配字符串的开头和结尾。如果光标在开始或结束时看到这些符号,则正则表达式引擎将失败,备份并尝试其他操作。但他们实际上并没有向前移动光标,因为它们不匹配字符;它们只检查光标所在的位置。

Lookahead和lookbehind工作方式相同。当正则表达式引擎尝试匹配它们时,它会检查周围光标以查看正确的模式是在其前面还是后面,但是如果匹配,它不会移动光标

考虑:

/(?=foo)foo/.match 'foo'

这将匹配!正则表达式引擎是这样的:

  1. 从字符串的开头开始:|foo
  2. 正则表达式的第一部分是(?=foo)。这意味着:仅在光标后出现foo时匹配。可以?嗯,是的,所以我们可以继续。但是光标不会移动,因为这是零宽度。我们仍有|foo
  3. 接下来是f。光标前面有f吗?是的,请继续,将光标移过ff|oo
  4. 接下来是o。光标前面有o吗?是的,请继续,将光标移过ofo|o
  5. 同样的事情,将我们带到foo|
  6. 我们到了正则表达式的末尾,没有任何失败,所以模式匹配。
  7. 特别是你的四个断言:

    • (?=...)&#34; lookahead&#34 ;;它断言... 出现在光标之后。

      1.9.3p125 :002 > 'jump june'.gsub(/ju(?=m)/, 'slu')
       => "slump june" 
      

      &#34; ju&#34; in&#34; jump&#34;匹配,因为&#34; m&#34;接下来。但是&#34; ju&#34;在&#34; 6月&#34;没有&#34; m&#34;接下来,所以它一个人留下。

      由于它没有移动光标,因此在放置光标后必须小心。 (?=a)b永远不会匹配任何内容,因为它检查下一个字符是a,然后检查相同的字符是否为{{1} },这是不可能的。

    • b&#34; lookbehind&#34 ;;它断言(?<=...) 光标之前

      ...

      &#34;我们的&#34;在&#34;四&#34;匹配,因为有&#34; f&#34;在它之前,但是&#34;我们的&#34;在&#34;面粉&#34;有一个&#34; l&#34;在它之前,所以它不匹配。

      如上所述,您必须小心之前的内容。 1.9.3p125 :002 > 'four flour'.gsub(/(?<=f)our/, 'ive') => "five flour" 永远不会匹配,因为它检查下一个字符是a(?<=b),移动光标,然后检查前一个字符是a

    • b是&#34;否定前瞻&#34 ;;它声称(?!...) 不会出现在光标之后。

      ...

      &#34;子&#34;匹配,因为接下来的是一个空间,而不是&#34; ren&#34;。 &#34;的子&#34;没有按&#39;吨

      这可能是我最常用的一个;精确控制接下来会发生什么事情就派上用场了。

    • 1.9.3p125 :003 > 'child children'.gsub(/child(?!ren)/, 'kid') => "kid children" 是&#34;负面的背后&#34 ;;它断言<{1}} 光标之前

      (?<!...)

      &#34; oot&#34; in&#34; foot&#34;很好,因为没有&#34; r&#34;在它之前。 &#34; oot&#34; in&#34; root&#34;显然有一个&#34; r&#34;。

      作为附加限制,大多数正则表达式引擎要求...在这种情况下具有固定长度。因此,您无法使用1.9.3p125 :004 > 'foot root'.gsub(/(?<!r)oot/, 'eet') => "feet root" ...?+

    你也可以嵌套这些,否则做各种疯狂的事情。我将它们主要用于一次性我知道我永远不需要维护,所以我没有任何实用应用程序的好例子;老实说,他们很奇怪,你应该先尝试以其他方式做你想做的事。 :)


    事后考虑:语法来自Perl regular expressions,后者使用*后跟各种符号进行大量扩展语法,因为{n,m}本身无效。所以(?本身并不意味着任何事情; ?是一个完整的标记,意思是&#34;这是一个后视的开始&#34;。它就像<=(?<=是独立的运算符一样,即使它们都以+=开头。

    他们很容易记住,但是:++表示向前看(或者,真的,&#34;此处&#34;),+表示向后看,{{1它的传统含义是&#34;不是&#34;。


    关于你后面的例子:

    =

    是的,这些产生相同的输出。这是使用前瞻的那个棘手的问题:

    1. 正则表达式引擎尝试了一些东西,但它们还没有工作,现在它已经在<
    2. 检查!光标irb(main):002:0> "foresight".sub(/(?!s)ight/, 'ee') => "foresee" irb(main):003:0> "foresight".sub(/ight/, 'ee') => "foresee" 后的字符是吗?不,它是fores|ight!因此该部分匹配并且匹配继续,但光标不移动,我们仍然有(?!s)
    3. 检查si是否在光标之后?嗯,是的,确实如此,所以移动光标:fores|ight
    4. 我们已经完成了!
    5. 光标移动到子串ight上方,以便完全匹配,以及更换的内容。

      执行ight是没用的,因为您说:下一个字符不能foresight|,而必须 ight。但这与仅匹配(?!a)b

      相同

      这有时很有用,但您需要更复杂的模式:例如,a将匹配任何不是3的数字。

      这就是你想要的:

      b

      这声称b (?!3)\d之前没有

答案 1 :(得分:5)

在您发现正则表达式与位置以及字符匹配之前,很难理解零宽度断言。

当你看到字符串“foo”时,你自然会读到三个字符。但是,还有四个位置,这里用管道标记:“| f | o | o |”。前瞻或后瞻(aka lookarounds)匹配角色在表达式之前或之后的位置。

零宽度表达式与其他表达式之间的区别在于零宽度表达式仅匹配(或“消耗”)位置。所以,例如:

/(app)apple/

将无法匹配“apple”,因为它试图匹配“app”两次。但

/(?=app)apple/

将成功,因为前瞻只匹配位置,其中“app”跟随。它实际上与“app”字符不匹配,允许下一个表达式使用它们。

LOOKAROUND DESCRIPTIONS

  

积极前瞻:(?=s)

     
    

想象一下,你是一名训练中士,而且你正在进行检查。你从这条线的前面开始,意图走过每个私人,并确保他们满足期望。但是,在这样做之前,你要一个接一个地向前看,以确保他们已经在房产订单中排队。私人的名字是“A”,“B”,“C”,“D”和“E”。 /(?=ABCDE)...../.match('ABCDE')。是的,他们都在场并且占了一席之地。

  
     

否定前瞻:(?!s)

     
    

你在线下进行检查并最终站在私人D处。现在你要向前看以确保来自另一家公司的“F”没有再次意外地滑入错误的阵型。 /.....(?!F)/.match('ABCDE')。不,他这次没有滑倒,所以一切都很好。

  
     

积极的观察:(?<=s)

     
    

在完成检查后,中士在编队结束时。他转身扫描,确保没有人偷偷溜走。 /.....(?<=ABCDE)/.match('ABCDE')。是的,每个人都在场并且占了上风。

  
     

负面反对:(?<!s)

     
    

最后,训练中士最后一次检查以确保私人A和B没有再次切换位置(因为他们喜欢KP)。 /.....(?<!BACDE)/.match('ABCDE')。不,他们没有,所以一切都很好。

  

答案 2 :(得分:2)

零宽度断言的含义是在匹配时消耗零个字符的表达式。例如,在此示例中,

"foresight".sub(/sight/, 'ee')

匹配的是

foresight
    ^^^^^

因此结果将是

foreee

但是,在这个例子中,

"foresight".sub(/(?<=s)ight/, 'ee')

匹配的是

foresight
     ^^^^

因此结果将是

foresee

零宽度断言的另一个例子是字边界字符\b。例如,要匹配一个完整的单词,您可以尝试用空格围绕单词,例如

"flight light plight".sub(/\slight\s/, 'dark')

获取

flightdarkplight

但你知道在替换过程中空格的匹配是如何消除的吗?使用单词边界解决了这个问题:

"flight light plight".sub(/\blight\b/, 'dark')

\b匹配单词的开头或结尾,但实际上并不匹配字符:它是零宽度

对你的问题最简洁的答案可能就是: Lookahead和lookbehind断言是零宽度断言的一种。所有前瞻和后瞻断言都是零宽度断言。


以下是您的示例的解释:

irb(main):001:0> "foresight".sub(/(?!s)ight/, 'ee')
=> "foresee"

上面,您说“匹配下一个字符 s,然后是i。”对于i始终为真,因为i永远不是s,因此替换成功。

irb(main):002:0> "foresight".sub(/(?=s)ight/, 'ee')
=> "foresight"

上面,您说“匹配下一个字符 s,然后是i。”这是从不为真,因为i永远不是s,因此替换失败。

irb(main):003:0> "foresight".sub(/(?<=s)ight/, 'ee')
=> "foresee"

上面,已经解释过了。 (这是正确的。)

irb(main):004:0> "foresight".sub(/(?<!s)ight/, 'ee')
=> "foresight"

上面,现在应该清楚了。在这种情况下,“交火”将取代“firefee”,而不是“预见”到“预见”。