正则表达式从左向右匹配,随着匹配的进行,在字符串上一路移动一个“光标”。如果你的正则表达式包含了诸如a
这样的普通字符,那就意味着:“如果光标前面有一个字母a
,就将光标向前移动一个字符,然后继续匹配。否则,就会出现错误;回溯一下,然后尝试其他正则表达式。” 因此,你可以说a
有一个“宽度”为一个字符。
“零宽断言”就是这样:它肯定了字符串中的某些条件(即不匹配某些条件),但它并不会将光标向前移动,因为它的“宽度”为零。
您可能已经熟悉了一些更简单的零宽断言,例如^
和$
,它们匹配字符串的开头和结尾。如果在看到这些符号时光标不在开头或结尾,正则表达式引擎将失败、回溯,并尝试其他正则表达式。但它们实际上并没有将光标向前移动,因为它们不匹配字符;它们只检查光标所在的位置。
前向断言和后向断言的工作方式也相同。当正则表达式引擎尝试匹配它们时,它会检查光标周围的文本以查看正确的模式是否在光标前面或后面,但如果匹配成功,它不会移动光标。
考虑以下示例:
/(?=foo)foo/.match 'foo'
这将匹配!正则表达式引擎的工作流程如下:
- 从字符串开头开始:
|foo
。
- 正则表达式的第一部分是
(?=foo)
,它的意思是:只有在光标后面出现foo
时才匹配。它确实出现了,所以我们可以继续。但是,由于这是零宽度的,光标不会移动。我们仍然有|foo
。
- 接下来是
f
。在光标前面有f
吗?有,因此继续,并将光标移到f|oo
之后。
- 接下来是
o
。在光标前面有o
吗?有,因此继续,并将光标移到fo|o
之后。
- 再次相同的操作,将我们带到
foo|
。
- 我们到达了正则表达式的末尾,没有失败,因此模式匹配。
关于您提出的四个断言:
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"
是的,它们产生相同的输出。这是使用先行断言时的棘手问题:
- 正则表达式引擎尝试了一些东西,但它们没有起作用,现在它在 "fores|ight"。
- 它检查了
(?!s)
。光标 后面 的字符是s
吗?不,是i
!因此该部分匹配并且匹配继续进行,但光标 不会移动,我们仍然有 fores|ight
。
- 它检查了
ight
。光标后面有ight
吗?是的,有,所以移动光标:foresight|
。
- 完成了!
光标经过子字符串 ight
,因此这是完全匹配,也是被替换的内容。
执行 (?!a)b
没有意义,因为你正在说:下一个字符 不能是 a
,而且它 必须 是 b
。 但这与只匹配 b
相同!
这有时可能很有用,但您需要一个更复杂的模式:例如,(?!3)\d
将匹配任何不是3的数字。
这才是您想要的:
1.9.3p125 :001 > "foresight".sub(/(?<!s)ight/, 'ee')
=> "foresight"
这段代码检查了s
是否在ight
之前。