8

我刚刚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(/(?<=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自行产生输出 - 你能帮我想象一下吗?

谢谢

4

3 回答 3

18

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

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

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

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

考虑:

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

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

  1. 从字符串的开头开始:|foo.
  2. 正则表达式的第一部分是(?=foo). 这意味着:只有foo出现在光标之后才匹配。可以?嗯,是的,所以我们可以继续。但是光标没有移动,因为这是零宽度。我们还有|foo
  3. 接下来是f. f光标前面有没有?是的,所以继续,将光标移过f: f|oo
  4. 接下来是o. 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”。“孩子”没有。

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

  • (?<!...)是“消极的后视”;它断言... 不会出现光标之前。

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

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

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

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


事后思考:语法来自Perl 正则表达式,它使用(?后跟各种符号来进行很多扩展语法,因为?它本身是无效的。所以<=本身并不意味着什么;(?<=是一个完整的标记,意思是“这是回顾的开始”。这就像 how+=++是单独的运算符,即使它们都以+.

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


关于你后面的例子:

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. 我们完成了!

光标移动到 substringight上,所以这是完全匹配,这就是被替换的内容。

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

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

这就是你想要的:

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

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

于 2013-01-17T21:00:54.727 回答
5

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

当您看到字符串“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'). 不,这次他没有溜进来,所以一切都很好。

正面回顾:(?<=s)

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

负面回顾:(?<!s)

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

于 2013-01-17T21:00:17.307 回答
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。” 对于 an来说总是如此i,因为 ani从来都不是 an s,所以替换成功。

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

上面,你说,“匹配下一个字符一个s,然后是一个i。” 这从来都不是真的,因为 ani从来都不是 an 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”。

于 2013-01-17T20:51:45.030 回答