如何在Ruby的正则表达式中使用前瞻和后顾概念支持零宽度断言概念?

8

我刚刚阅读了有关文档中的 零宽度断言 概念。这让我想到了一些问题:

  • 为什么要使用这样的名称 零宽度断言
  • 向前查看向后查看 概念如何支持这样的 零宽度断言 概念?
  • ?<=s<!s=s<=s - 这四个符号在模式内指示了什么?你能帮我理解一下实际发生了什么吗?

我还尝试了一些简短的代码来理解逻辑,但对输出结果不是很自信:

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个回答

18

正则表达式从左向右匹配,随着匹配的进行,在字符串上一路移动一个“光标”。如果你的正则表达式包含了诸如a这样的普通字符,那就意味着:“如果光标前面有一个字母a,就将光标向前移动一个字符,然后继续匹配。否则,就会出现错误;回溯一下,然后尝试其他正则表达式。” 因此,你可以说a有一个“宽度”为一个字符。

“零宽断言”就是这样:它肯定了字符串中的某些条件(即不匹配某些条件),但它并不会将光标向前移动,因为它的“宽度”为零。

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

前向断言和后向断言的工作方式也相同。当正则表达式引擎尝试匹配它们时,它会检查光标周围的文本以查看正确的模式是否在光标前面或后面,但如果匹配成功,它不会移动光标。

考虑以下示例:

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

这将匹配!正则表达式引擎的工作流程如下:

  1. 从字符串开头开始:|foo
  2. 正则表达式的第一部分是(?=foo),它的意思是:只有在光标后面出现foo时才匹配。它确实出现了,所以我们可以继续。但是,由于这是零宽度的,光标不会移动。我们仍然有|foo
  3. 接下来是f。在光标前面有f吗?有,因此继续,并将光标移到f|oo之后。
  4. 接下来是o。在光标前面有o吗?有,因此继续,并将光标移到fo|o之后。
  5. 再次相同的操作,将我们带到foo|
  6. 我们到达了正则表达式的末尾,没有失败,因此模式匹配。

关于您提出的四个断言:

  • (?=...)是“先行断言”,它断言... 光标后出现。

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

"Jump"中的 "ju" 之所以匹配是因为接下来是 "m"。但 "June" 中的 "ju" 没有紧跟着 "m",因此保持原样。

由于它不移动光标,所以在它后面放任何东西时必须小心。(?=a)b 将永远不会匹配任何内容,因为它检查下一个字符是 a,然后还检查相同的字符是 b,这是不可能的。

  • (?<=...) 是“向后查找”,它断言... 在光标之前出现。

  • 1.9.3p125 :002 > 'four flour'.gsub(/(?<=f)our/, 'ive')
     => "five flour" 
    

    “four”中的“our”匹配是因为它之前紧跟着一个“f”,但在“flour”中,“our”之前有一个“l”,所以不匹配。

    就像上面一样,您必须小心在其之前放置什么。 a(?<=b)永远不会匹配,因为它检查下一个字符是否是a,移动光标,然后检查前一个字符是否是b

    (?!...)是“负向先行断言”;它断言在光标后面不会出现...

    1.9.3p125 :003 > 'child children'.gsub(/child(?!ren)/, 'kid')
     => "kid children"
    

    "child"匹配,因为下一个字符是空格,而不是"ren"。"children"不匹配。

    这可能是我最常用的一个;精细控制不能出现的内容非常有用。

  • (?<!...)是"负向后查找";它断言在光标之前没有出现...

  • 1.9.3p125 :004 > 'foot root'.gsub(/(?<!r)oot/, 'eet')
     => "feet root" 
    

    “foot”的“oot”很好,因为它之前没有“r”。 “root”中的“oot”显然有一个“r”。

    作为额外的限制,大多数正则表达式引擎要求在这种情况下...具有固定长度。 因此,您不能使用?+*{n,m}

    您还可以嵌套这些元字符,并且以其他方式执行各种疯狂的操作。 我主要将它们用于我知道自己永远不必维护的单次操作,因此我手头没有任何真实应用程序的伟大示例; 老实说,它们足够奇怪,您应该先尝试以其他方式实现所需内容。 :)


    事后想法:语法来自Perl正则表达式,其中使用(?后跟各种符号表示大量扩展语法,因为单独使用?是无效的。 所以<=本身并没有意义; (?<=是一个完整的标记,表示“这是回顾开始”。 这就像+=++是不同的运算符一样,即使它们都以+开头。

    但是很容易记住: =指向前看(或者实际上是“这里”),<表示向后查找,而具有其“not”的传统含义。


    关于您稍后的示例:

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

    是的,它们产生相同的输出。这是使用先行断言时的棘手问题:

    1. 正则表达式引擎尝试了一些东西,但它们没有起作用,现在它在 "fores|ight"。
    2. 它检查了 (?!s)。光标 后面 的字符是s吗?不,是i!因此该部分匹配并且匹配继续进行,但光标 不会移动,我们仍然有 fores|ight
    3. 它检查了ight。光标后面有ight吗?是的,有,所以移动光标:foresight|
    4. 完成了!

    光标经过子字符串 ight,因此这是完全匹配,也是被替换的内容。

    执行 (?!a)b 没有意义,因为你正在说:下一个字符 不能是 a,而且它 必须b。 但这与只匹配 b 相同!

    这有时可能很有用,但您需要一个更复杂的模式:例如,(?!3)\d 将匹配任何不是3的数字。

    这才是您想要的:

    1.9.3p125 :001 > "foresight".sub(/(?<!s)ight/, 'ee')
     => "foresight" 
    

    这段代码检查了s是否在ight之前。


    你也不能使用预查来匹配(任意)嵌套的括号。 - Tim Pietzcker
    哦,嗯,那是使用捕获递归。我会想一个更好的例子。 - Eevee
    这是我能想到的最详细的解释了!如果您需要其他帮助,请告诉我。 - Eevee
    你们给了我很多。这是我复习任何时候都可以参考的参考书。所有内容都在一个地方! :) 真不错。每个人都应该从这里开始学习概念!令人惊叹...........非常感谢朋友们。只有我的最后一个 EDIT 没有回答,如果有人回答了,我们将得到另一种风味 :) :) - Arup Rakshit
    完美!没有更多的问题了!亲爱的,我想成为你的学生 :) - Arup Rakshit

    5
    零宽断言在你意识到正则表达式匹配位置和字符时会变得难以理解。当你看到字符串“foo”时,你自然会读出三个字符。但是,在这里用管道标记的四个位置也有四个|f|o|o|。前瞻或后顾(也称为lookarounds)匹配一个字符之前或之后与表达式匹配的位置。零宽度表达式和其他表达式的区别在于,零宽度表达式只匹配(或“消耗”)位置。例如:
    /(app)apple/
    

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

    /(?=app)apple/
    

    由于前瞻只匹配紧随其后的“app”位置,因此将成功。它实际上并未匹配“app”字符,允许下一个表达式消耗它们。

    前瞻描述

    正向前瞻: (?=s)

    想象一下你是一个军士长,正在进行检查。您从队列前面开始,并打算走过每个士兵,确保他们符合预期。但是,在这样做之前,您依次向前看,以确保他们已按正确顺序排列。士兵的名字是“A”,“B”,“C”,“D”和“E”。/(?=ABCDE)...../.match('ABCDE')。是的,他们都在场并且没问题。

    负向前瞻: (?!s)

    您沿着队列逐个进行检查,最终站在D士兵处。现在,您将向前看,以确保来自另一个公司的“F”没有再次错误地滑入了错误的形成队列。/.....(?!F)/.match('ABCDE')。不,这次没有滑入,所以一切都好。

    正向后瞻: (?<=s)

    检查完成后,军士长站在队列末尾。他转过身来,向后扫描以确保没有人溜走。/.....(?<=ABCDE)/.match('ABCDE')。是的,每个士兵都在场。

    负向后瞻: (?<!s)

    最后,军士长最后一眼看着A和B士兵是否再次交换了位置(因为他们喜欢在厨房值勤)。/.....(?<!BACDE)/.match('ABCDE')。不,他们没有,所以一切都好。


    1
    哇!太完美了——这正是我向你们询问的,比我到目前为止浏览的在线材料更清晰。你看到我的最后一次编辑了吗? - Arup Rakshit
    1
    在你的帖子中,我理解了Regex中的Zero-width实际上是什么意思,以及consuming因素 - 这对于Regexp学习者来说真的是一个很好的指导。 - Arup Rakshit

    2
    零宽断言的意义在于匹配时消耗零个字符的表达式。例如,在这个例子中,
    "foresight".sub(/sight/, 'ee')
    

    匹配的是什么。
    foresight
        ^^^^^
    

    因此,结果将会是:
    foreee
    

    然而,在这个例子中,
    "foresight".sub(/(?<=s)ight/, 'ee')
    

    匹配的是什么。
    foresight
         ^^^^
    

    因此结果将会是:
    foresee
    

    另一个零宽度断言的例子是“单词边界”字符\b。例如,要匹配完整单词,您可以尝试在单词周围加上空格,如下所示:e.g.
    "flight light plight".sub(/\slight\s/, 'dark')
    

    获取

    flightdarkplight
    

    但是,您可以看到,匹配空格会在替换过程中将其删除。使用单词边界可以解决这个问题:

    "flight light plight".sub(/\blight\b/, 'dark')
    
    \b匹配单词的开头或结尾,但实际上不匹配任何字符:它是零宽度的。
    也许对你问题最简洁的回答是:前瞻和后顾断言是一种零宽度断言。所有前瞻和后顾断言都是零宽度断言。
    以下是你例子的解释:
    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"
    

    以上,现在应该很清楚了。在这种情况下,“firefight”将替换为“firefee”,但不是“foresight”替换为“foresee”。

    消耗零字符 - 什么是消耗,你能从技术上解释一下吗?Look-aheadLook-behind在技术上是如何发生的,并且如何通过零宽度断言的概念来生成此类输出,请向我展示。我很想看到这些东西! - Arup Rakshit
    我认为@Eevee已经解释得最好了。我已经在我的答案中添加了一些例子,以帮助您更进一步。 - Andrew Cheong

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接