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

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

  • 为什么这样的名字Zero-Width Assertions
  • Look-ahead和后look-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(/(? "foresee" irb(main):004:0> "foresight".sub(/(? "foresight" 

谁能帮助我在这里理解?

编辑

在这里,我尝试了两个片段,其中“Zero-Width Assertions”概念如下:

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

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

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

以上两者都产生相同的输出,现在内部两个正则regexp由它们自己移动以产生输出 – 你能帮我形象化吗?

谢谢

正则表达式从左到右匹配,并在字符串移动时移动一种“光标”。 如果你的正则表达式包含像a这样的常规字符,这意味着:“如果光标前面有一个字母a ,将光标向前移动一个字符,继续前进。否则,出错了;备份并尝试别的东西。” 所以你可能会说a有一个字符的“宽度”。

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

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

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. 我们到了正则表达式的末尾,没有任何失败,所以模式匹配。

特别是你的四个断言:

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

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

    “跳跃”中的“ju”匹配,因为接下来是“m”。 但是“六月”中的“ju”下一个没有“m”,所以它一个人留下。

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

  • (?<=...)是“lookbehind”; 它断言... 确实出现光标之前

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

    “我们”在“四个”比赛中因为在它之前有一个“f”,但“面粉”中的“我们”在它之前有一个“l”因此它不匹配。

    如上所述,你必须小心你摆面前的东西。 a(?<=b)永远不会匹配,因为它检查下一个字符是否为a ,移动光标,然后检查前一个字符是否为b

  • (?!...)是“负向前瞻”; 它断言...光标后没有出现。

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

    “孩子”匹配,因为接下来的是一个空间,而不是“仁”。 “孩子”没有。

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

  • (?是“负面的背后”; 它断言... 没有出现光标之前

     1.9.3p125 :004 > 'foot root'.gsub(/(? "feet root" 

    “脚”中的“oot”很好,因为它之前没有“r”。 “根”中的“oot”显然具有“r”。

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

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


事后想法:语法来自Perl正则表达式 ,它使用(?后面跟着各种符号用于很多扩展语法因为?本身是无效的。所以<=并不意味着什么本身; (?<=是一整个令牌,意思是“这是一个外观的开始”。就像+=++是单独的运算符一样,即使它们都以+开头。

它们很容易记住,但是: =表示向前看(或者,真的,“在这里”), <表示向后看,并且! 具有“不”的传统含义。


关于你后来的例子:

 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 。 光标后会出现吗? 嗯,是的,确实如此,所以移动光标: foresight|
  4. 我们完成了!

光标移动到子串ight ,因此这是完全匹配,这就是被替换的内容。

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

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

这就是你想要的:

 1.9.3p125 :001 > "foresight".sub(/(? "foresight" 

这断言s不会在之前出现

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

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

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

 /(app)apple/ 

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

 /(?=app)apple/ 

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

查看说明

积极前瞻: (?=s)

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

否定前瞻:(?! (?!s)

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

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

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

负面观察: (?

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

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

 "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永远不是一个,所以替换失败了。

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

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

 irb(main):004:0> "foresight".sub(/(? "foresight" 

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