使用负向环视的正则表达式匹配连续出现的两个模式。

4
应该有人可以轻松回答这个问题:
如果我运行这段JavaScript代码:
var regex = new RegExp("(?!cat)dog(?!cat)","g");
var text =  "catdogcat catdogdog catdogdogcat".replace(regex,"000");
console.log(text);

它输出这个:
catdogcat cat000000 cat000dogcat

但是我认为它应该输出这个:
catdogcat cat000000 cat000000cat

为什么在catdogdogcat中第二个“dog”没有被替换为000?我想要在“dog”两侧都没有“cat”时才替换它。在catdogdogcat中,两个“dog”都满足这个要求,因此应该替换它们。显然我不理解这些负向先行断言...

2
因为它后面跟着 cat,而第二个 (?!cat) 则将其排除。你可能需要解释一下你在尝试使用这两个先行断言实现什么功能,因为我怀疑第一个先行断言也无法达到你的预期效果。 - Martin Ender
@Utkanos 真的吗?实际上,在模式内部使用前瞻有相当多的用例。 - Martin Ender
嗯。我已经编辑了我的答案,写明我正在尝试做什么。我想我需要学习一下负向环视…… - user993683
@JoeRocc 哦,是的,这正是我所想的。这使得事情变得相当复杂。请看我的回答。 - Martin Ender
@Utkanos 好的,没问题。 - Martin Ender
显示剩余2条评论
2个回答

7
你的方法有两个问题。
  1. 你的第一个前瞻应该是后顾。 当你写 (?!cat) 时,引擎会检查下一个三个字符是否为 cat,然后重置到它开始的位置(这就是前瞻的原理),然后你再试图在同样的三个字符位置上匹配 dog。因此,前瞻没有添加任何内容:如果你可以匹配到 dog,那么显然你不能在同一位置匹配到 cat。你想要的是后顾 (?<!cat),它检查前面的字符是否不是 cat。不幸的是,JavaScript 不支持后顾。
  2. 你想要将两个前瞻逻辑“或”起来。 在你的情况下,如果任一前瞻失败,它都会使模式失败。因此,两个前瞻的要求(两端都不能有 cat)必须同时满足。但实际上你想要将它们“或”起来。如果支持后顾,则会变成 (?<!cat)dog|dog(?!cat)(请注意,交替分割整个模式)。但正如我所说,JavaScript 不支持后顾。你在第一个 catdogdog 的部分似乎将两个前瞻“或”起来的原因是因为前面的 cat 没有被检查(参见第1点)。

如何绕过后顾?Kolink 的答案建议使用 (?!cat)...dog,它将前瞻放置在可能出现 cat 的位置,并使用前瞻。这有两个新问题:它无法匹配字符串开头的 dog(因为需要前面的三个字符)。而且它不能匹配连续的两个 dog,因为匹配不能重叠(在匹配第一个 dog 后,引擎需要三个新字符,即 ...,这将在实际匹配到下一个 dog 之前匹配掉下一个 dog)。

有时可以通过反转模式和字符串来绕过后顾,从而将后顾变成前瞻 - 但在你的情况下,这会将末尾的前瞻变成后顾。

仅使用正则表达式的解决方案

我们需要更聪明一些。由于匹配不能重叠,我们可以尝试显式地匹配catdogcat,而不是替换它(因此在目标字符串中跳过它们),然后只替换我们找到的所有dog。我们将这两种情况放在交替中,因此它们都会在字符串的每个位置上尝试(虽然在这里并不重要,但catdogcat选项具有优先权)。问题是如何获得条件替换字符串。但让我们先看看我们已经得到了什么:
text.replace(/(catdog)(?=cat)|dog/g, "$1[or 000 if $1 didn't match]")

所以在第一种替代方案中,我们匹配一个catdog并将其捕获到组1中,并检查是否有另一个cat跟随。在替换字符串中,我们只需写回$1即可。美妙的是,如果第二个替代匹配成功,则第一组将未被使用,因此在替换中将成为空字符串。之所以只匹配catdog并使用前瞻而不是立即匹配catdogcat,原因是重叠匹配。如果我们使用catdogcat,那么在输入catdogcatdogcat中,第一个匹配将消耗所有内容,直到包括第二个cat,因此第二个dog无法被第一种替代方案识别。
现在唯一的问题是,如果我们使用第二种替代方案,如何将000放入替换中。
不幸的是,我们无法创造出不属于输入字符串的条件替换。诀窍是在输入字符串的末尾添加000,如果我们找到dog,则在前瞻中捕获它,然后将其写回。
text.replace(/$/, "000")                            
    .replace(/(catdog)(?=cat)|dog(?=.*(000))/g, "$1$2")
    .replace(/000$/, "")

第一个替换将“000”添加到字符串末尾。
第二个替换匹配“catdog”(检查是否有另一个“cat”跟随)并将其捕获到组1中(将2留空),或匹配“dog”并将“000”捕获到组2中(将组1留空)。然后我们写回“$1$2”,它将是未装饰的“catdog”或“000”。
第三个替换消除了我们在字符串末尾的多余“000”。
回调解决方案
如果您不喜欢准备正则表达式和第二个选项中的前瞻,则可以使用稍微简单一些的正则表达式,并使用替换回调:
text.replace(/(catdog)(?=cat)|dog/g, function(match, firstGroup) {
    return firstGroup ? firstGroup : "000"
})

使用replace的这个版本,每次匹配都会调用提供的函数,并将其返回值用作替换字符串。函数的第一个参数是整个匹配,第二个参数是第一个捕获组(如果该组未参与匹配,则其值将为undefined),以此类推...
因此,在替换回调中,如果firstGroup未定义(即匹配了dog选项),我们可以自由地产生000,或者如果存在firstGroup(即匹配了catdogcat选项),则只需返回firstGroup。这种方法更加简洁,可能更容易理解。但是,调用函数的开销使其速度显著变慢(不过,这是否重要取决于您要多频繁地执行此操作)。选择你喜欢的方式吧!

+1 鼓励你花费时间写出如此详细和优秀的解释。 - anubhava
哇!非常感谢 - 我真的很感激你花时间写下这些内容。 - user993683
不错的回答。不过关于性能,需要注意一下。在Firefox 36上,这两种方法的性能差别不是很大,而且我建议使用回调方法而不是正则表达式方法,因为它可以提高代码的清晰度... - nhahtdh

1
你的正则表达式简化为dog(?!cat)(因为第一个回顾不消耗任何内容),所以它将替换任何后面没有跟着catdog实例。

尝试正则表达式(?!cat).{3}dog(?!cat)


那仍然无法产生所需的输出。我认为 OP 想要将这两个先行断言进行“或”操作,而不是“与”操作。此外,这里无法替换连续的 "dog",因为匹配会重叠。 - Martin Ender
s/lookbehind/lookahead/。我猜这就是你的意思? - John Dvorak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接