【问题标题】:How the Look-ahead and Look-behind concept supports such Zero-Width Assertions concept in Regex of Ruby?Look-ahead 和 Look-behind 概念如何在 Ruby 的 Regex 中支持这种零宽度断言概念?
【发布时间】:2013-01-17 20:34:10
【问题描述】:

我刚刚从文档中了解了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"

谁能帮我理解一下?

编辑

在这里,我尝试了两个带有“零宽度断言”概念的 sn-ps,如下所示:

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

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

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

以上都产生相同的输出,现在regexp 在内部如何自行移动以产生输出 - 你能帮我想象一下吗?

谢谢

【问题讨论】:

    标签: ruby regex ruby-1.9.3


    【解决方案1】:

    正则表达式从左到右匹配,并随着字符串移动一种“光标”。如果您的正则表达式包含像a 这样的常规字符,这意味着:“如果光标前面有一个字母a,请将光标前移一个字符,然后继续。否则,有问题;备份并尝试一下别的。”所以你可能会说a 的“宽度”为一个字符。

    “零宽度断言”就是这样:它断言关于字符串的某些内容(即,如果某些条件不成立,则不匹配),但它不会移动光标向前,因为它的“宽度”为零。

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

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

    考虑:

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

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

    1. 从字符串的开头开始:|foo
    2. 正则表达式的第一部分是(?=foo)。这意味着:仅当光标后出现foo 时才匹配。可以?嗯,是的,所以我们可以继续。但是光标不会移动,因为这是零宽度。我们还有|foo
    3. 接下来是f。光标前是否有f?是的,所以继续,将光标移过ff|oo
    4. 接下来是o。光标前是否有o?是的,所以继续,将光标移过ofo|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,然后 also 会检查 same 字符是否为b,这是不可能的。

    • (?&lt;=...) 是“向后看”;它断言... 确实出现在光标之前

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

      “four”中的“our”匹配,因为它前面有一个“f”,但“flour”中的“our”前面有一个“l”,所以它不匹配。

      像上面一样,你必须小心你把它放在之前a(?&lt;=b) 永远不会匹配,因为它会检查下一个字符是 a,移动光标,然后检查前一个字符是 b

    • (?!...) 是“负前瞻”;它断言... 不会出现在光标之后。

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

      “child”匹配,因为接下来是空格,而不是“ren”。 “孩子”没有。

      这可能是我用得最多的一个;精细控制接下来不能发生的事情会派上用场。

    • (?&lt;!...) 是“消极的后视”;它断言... 出现在光标之前

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

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

      作为附加限制,大多数正则表达式引擎要求... 在这种情况下具有固定长度。所以你不能使用?+*{n,m}

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


    事后思考:语法来自Perl regular expressions,它使用(? 后跟各种符号来表示很多扩展语法,因为? 本身是无效的。所以&lt;= 本身并没有任何意义; (?&lt;= 是一个完整的标记,意思是“这是回顾的开始”。就像+=++ 是独立的运算符一样,尽管它们都以+ 开头。

    不过,它们很容易记住:= 表示向前看(或者,实际上是“这里”),&lt; 表示向后看,! 有其传统含义“不”。


    关于你后面的例子:

    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. 它检查ightight 是否出现在光标之后?嗯,是的,确实如此,所以移动光标:foresight|
    4. 我们完成了!

    光标移到子字符串ight 上,所以这是完全匹配,这就是被替换的内容。

    (?!a)b 是没用的,因为你是说:下一个字符不能a,它必须b。但这与匹配b!

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

    这就是你想要的:

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

    这断言s 不会出现在之前 ight

    【讨论】:

    • 您也不能(任意)将嵌套括号与环视匹配。
    • 哦,呃,这是使用捕获递归。我会想一个更好的例子
    • 这是我能想到的尽可能多的解释!如果您还需要什么,请告诉我
    • 你们给了我很多。这是我随时修改这个概念的参考书。都在一个地方! :) 不错。每个人都应该采取的理念从这里开始!令人兴奋............非常感谢你的朋友。只是我的最后一个编辑还有待回答,如果有人回答,这会给我们带来另一种风味:) :)
    • 完美!没有更多问题!亲爱的,想做你的学生:)
    【解决方案2】:

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

    当您看到字符串“foo”时,您自然会读到三个字符。但是,还有四个位置,在这里用竖线标记:“|f|o|o|”。前瞻或后瞻(也称为环视)匹配前面或后面的字符与表达式匹配的位置。

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

    /(app)apple/
    

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

    /(?=app)apple/
    

    会成功,因为前瞻只匹配“app”后面的位置。它实际上不匹配“app”字符,允许下一个表达式使用它们。

    外观描述

    正向预测:(?=s)

    假设您是一名教官,正在执行检查。您从队伍的最前面开始,打算走过每个私人并确保他们达到预期。但是,在这样做之前,您要一一向前看,以确保它们已按财产顺序排列。士兵的名字是“A”、“B”、“C”、“D”和“E”。 /(?=ABCDE)...../.match('ABCDE')。是的,他们都在场并被解释。

    负前瞻:(?!s)

    您执行检查并最终站在列兵 D。现在您将向前看,以确保来自其他公司的“F”没有再次意外滑入错误的队形。 /.....(?!F)/.match('ABCDE')。不,这次他没有溜进来,所以一切都很好。

    正面观察:(?&lt;=s)

    完成检查后,中士在编队的末端。他转身向后扫视,以确保没有人偷偷溜走。 /.....(?&lt;=ABCDE)/.match('ABCDE')。是的,每个人都在场并得到了说明。

    负向回溯:(?&lt;!s)

    最后,军士长最后检查一下,确保二等兵 A 和 B 没有再次调换位置(因为他们喜欢 KP)。 /.....(?&lt;!BACDE)/.match('ABCDE')。不,他们没有,所以一切都很好。

    【讨论】:

    • 天啊!完美——这是我问你们的,比我到现在浏览的在线资料更清晰。你看过我最后的EDIT吗?
    • 在您的帖子中,我了解了 Zero-width 在 Ruby Regex 中的实际含义?和consuming 因素——对于Regexp 学习者来说,什么才是真正的好消息。
    【解决方案3】:

    零宽度断言的含义是在匹配时使用零个字符的表达式。例如,在这个例子中,

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

    匹配的是

    foresight
        ^^^^^
    

    因此结果将是

    foreee
    

    然而,在这个例子中,

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

    匹配的是

    foresight
         ^^^^
    

    因此结果是

    foresee
    

    另一个零宽度断言的例子是 word-boundary 字符\b。例如,要匹配一个完整的单词,您可以尝试用空格将单词括起来,例如

    "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”。

    【讨论】:

    • consumes zero characters - 你能从技术上解释什么消耗吗? Look-aheadLook-behind 在技术上是如何发生的,并有助于以 zero-width assertion 的概念生成这样的输出,请告诉我。我渴望看到这些东西!
    • 我认为@Eevee 解释得最好。我在回答中添加了一些示例以进一步帮助您。
    猜你喜欢
    • 1970-01-01
    • 2011-02-06
    • 1970-01-01
    • 2020-02-07
    • 1970-01-01
    • 2011-09-01
    • 2018-05-30
    • 2020-03-23
    • 1970-01-01
    相关资源
    最近更新 更多