正则表达式从左到右匹配,并沿字符串移动某种“光标”。如果您的正则表达式包含常规字符,例如a
,这意味着:“如果有一封信a
在光标前面,将光标向前移动一个字符,然后继续。否则,就有问题了;备份并尝试其他事情。”所以你可能会这么说a
其“宽度”为一个字符。
“零宽度断言”就是这样:它asserts有关字符串的某些内容(即,如果某些条件不成立,则不匹配),但它不会向前移动光标,因为它的“宽度”为零。
您可能已经熟悉一些更简单的零宽度断言,例如^
and $
。它们匹配字符串的开头和结尾。如果光标在看到这些符号时不在开头或结尾,则正则表达式引擎将失败、备份并尝试其他操作。但它们实际上并没有向前移动光标,因为它们不匹配字符;他们只检查光标在哪里。
Lookahead 和 Lookbehind 的工作方式相同。当正则表达式引擎尝试匹配它们时,它会检查around光标以查看正确的模式是否位于其前面或后面,但如果匹配,则不会移动光标。
考虑:
/(?=foo)foo/.match 'foo'
这会匹配!正则表达式引擎是这样的:
- 从字符串的开头开始:
|foo
.
- 正则表达式的第一部分是
(?=foo)
。这意味着:仅匹配如果foo
出现在光标后面。可以?嗯,是的,所以我们可以继续。但光标不动,因为这是零宽度。我们还有|foo
.
- Next is
f
。有没有一个f
在光标前面?是的,所以继续,并将光标移过f
: f|oo
.
- Next is
o
。有没有一个o
在光标前面?是的,所以继续,并将光标移过o
: fo|o
.
- 同样的事情又把我们带到了
foo|
.
- 我们到达了正则表达式的末尾,没有任何失败,因此模式匹配。
特别是关于您的四个主张:
-
(?=...)
是“前瞻”;它断言...
does出现在光标后面。
1.9.3p125 :002 > 'jump june'.gsub(/ju(?=m)/, 'slu')
=> "slump june"
“jump”中的“ju”匹配,因为接下来是“m”。但“june”中的“ju”后面没有“m”,所以就不用管它了。
由于它不会移动光标,因此在其后面放置任何内容时必须小心。(?=a)b
永远不会匹配任何内容,因为它检查下一个字符是a
, then also检查是否same性格是b
,这是不可能的。
-
(?<=...)
是“后视”;它断言...
does appear before光标。
1.9.3p125 :002 > 'four flour'.gsub(/(?<=f)our/, 'ive')
=> "five flour"
“four”中的“our”匹配,因为它前面有一个“f”,但“flour”中的“our”前面有一个“l”,所以它不匹配。
就像上面一样,你必须小心你所放的东西before it. a(?<=b)
永远不会匹配,因为它检查下一个字符是a
,移动光标,然后检查前一个字符是否为b
.
-
(?!...)
是“负前瞻”;它断言...
does not出现在光标后面。
1.9.3p125 :003 > 'child children'.gsub(/child(?!ren)/, 'kid')
=> "kid children"
“child”匹配,因为接下来是一个空格,而不是“ren”。 “孩子”则不然。
这可能是我最常用的一个;精细地控制接下来不能发生的事情会派上用场。
-
(?<!...)
是“负向后看”;它断言...
does not appear before光标。
1.9.3p125 :004 > 'foot root'.gsub(/(?<!r)oot/, 'eet')
=> "feet root"
“foot”中的“oot”很好,因为它前面没有“r”。 “root”中的“oot”显然有一个“r”。
作为附加限制,大多数正则表达式引擎要求...
在这种情况下具有固定长度。所以你不能使用?
, +
, *
, or {n,m}
.
你也可以嵌套它们,或者做各种疯狂的事情。我使用它们主要是为了一次性使用,我知道我永远不需要维护,所以我手头没有任何现实世界应用程序的好例子;老实说,它们很奇怪,您应该首先尝试以其他方式做您想做的事情。 :)
事后思考:语法来自Perl正则表达式,其中使用了(?
后面跟着许多扩展语法的各种符号,因为?
其本身无效。所以<=
本身并没有任何意义;(?<=
是一个完整的标记,意思是“这是回顾的开始”。就像怎样+=
and ++
是单独的运算符,即使它们都以+
.
不过,它们很容易记住:=
表示向前看(或者实际上是“这里”),<
表示向后看,并且!
有其传统含义“不”。
关于你后面的例子:
irb(main):002:0> "foresight".sub(/(?!s)ight/, 'ee')
=> "foresee"
irb(main):003:0> "foresight".sub(/ight/, 'ee')
=> "foresee"
是的,它们产生相同的输出。这是使用前瞻的棘手之处:
- 正则表达式引擎尝试了一些方法,但没有成功,现在处于
fores|ight
.
- 它检查
(?!s)
。是人物after光标s
?不,它是i
!所以该部分匹配并且匹配继续,但是光标不动,我们还有fores|ight
.
- 它检查
ight
. Does ight
来到光标之后?嗯,是的,所以移动光标:foresight|
.
- 我们完成了!
光标移到子字符串上ight
,这就是完整的匹配,这就是被替换的内容。
Doing (?!a)b
没用,因为你说:下一个字符must not be a
,和它must be b
。但这与仅匹配相同b
!
这有时很有用,但您需要更复杂的模式:例如,(?!3)\d
将匹配任何不是 3 的数字。
这就是你想要的:
1.9.3p125 :001 > "foresight".sub(/(?<!s)ight/, 'ee')
=> "foresight"
这断言s
不来before ight
.