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被替换000

编辑:只要两边都没有猫,我想替换“狗”。在catdogdogcat中,两只狗都满足这一要求,因此应该更换它们。显然我不明白这些负面的看法......

4

2 回答 2

7

你的方法有两个问题。

  1. 你的第一个前瞻需要是一个后瞻。当您编写(?!cat)时,引擎会检查接下来的三个字符cat,然后重置到它开始的位置(这就是它向前看的方式),然后您尝试匹配dog这三个相同的字符。因此,前瞻不会添加任何内容:如果您可以匹配dog,那么您显然无法cat在同一位置匹配。您想要的是(?<!cat)检查前面的字符是否不是的lookbehind cat。不幸的是,JavaScript 不支持lookbehind。
  2. 您想对这两个环视进行逻辑或运算。在您的情况下,如果任一环顾失败,则会导致模式失败。因此,需要满足两个要求(两端都没有cat。但你实际上想要OR那个。如果支持lookbehinds,那会更喜欢(?<!cat)dog|dog(?!cat)(注意交替将整个模式分开)。但正如我所说,不支持后视。catdogdog您似乎在第一个位中对两个外观进行了 *OR*ed 的原因是前面cat根本没有检查(参见第 1 点)。

那么如何解决lookbehinds呢?Kolink 的回答建议(?!cat)...dog,它将环视放在 acat开始的位置,并使用前瞻。这有两个新问题:它不能匹配dog字符串开头的a(因为前面的三个字符是必需的。它不能匹配两个连续dog的s,因为匹配不能重叠(匹配第一个之后dog,引擎需要三个新字符,这将在再次实际匹配之前...消耗下一个)。dogdog

有时您可以通过反转模式和字符串来解决它,从而将向后看变成向前看 - 但在您的情况下,这会将最后的向前看变成向后看。

仅正则表达式的解决方案

我们必须聪明一点。由于匹配不能重叠,我们可以尝试catdogcat显式匹配,而不替换它(因此在目标字符串中跳过它们),然后只替换dog我们找到的所有 s。我们将这两种情况交替放置,因此它们都在字符串中的每个位置都进行了尝试(catdogcat尽管选项在这里并不重要)。问题是如何获得条件替换字符串。但是让我们看看到目前为止我们得到了什么:

text.replace(/(catdog)(?=cat)|dog/g, "$1[or 000 if $1 didn't match]")

因此,在第一个替代方案中,我们匹配 acatdog并将其捕获到组1中,并检查是否有另一个cat追随者。在替换字符串中,我们简单地写$1回。美妙之处在于,如果第二个备选方案匹配,则第一组将未被使用,因此将是一个空字符串作为替代品。我们只匹配catdog和使用前瞻而不是catdogcat立即匹配的原因再次是重叠匹配。如果我们使用catdogcat,那么在输入catdogcatdogcat中,第一个匹配项将消耗所有内容,直到并包括第二个cat,因此第dog一个替代项无法识别第二个。

现在唯一的问题是,如果我们使用第二种选择,我们如何000进入替换。

不幸的是,我们无法想出不属于输入字符串的条件替换。诀窍是将 a 添加000到输入字符串的末尾,如果我们找到 a dog,则在前瞻中捕获它,然后将其写回:

text.replace(/$/, "000")                            
    .replace(/(catdog)(?=cat)|dog(?=.*(000))/g, "$1$2")
    .replace(/000$/, "")

第一个替换添加000到字符串的末尾。

第二个替换匹配catdog(检查另一个cat跟随)并将其捕获到组12留空)或匹配dog并捕获000到组21留空组)。然后我们写$1$2回,要么是朴素的,要么catdog000.

第三个替换消除000了字符串末尾的多余内容。

回调解决方案

如果您不喜欢准备正则表达式和第二个选项中的前瞻,您可以使用稍微简单的正则表达式和替换回调:

text.replace(/(catdog)(?=cat)|dog/g, function(match, firstGroup) {
    return firstGroup ? firstGroup : "000"
})

每次匹配都会调用所提供函数的版本,replace并将其返回值用作替换字符串。函数的第一个参数是整个匹配,第二个参数是第一个捕获组(undefined如果该组不参与匹配)等等......

所以在替换回调中,我们可以自由地想象我们的000iffirstGroup是未定义的(即dog选项匹配)或只返回firstGroup如果它存在(即catdogcat选项匹配)。这更简洁一些,可能更容易理解。但是,调用该函数的开销会使其显着变慢(尽管这是否重要取决于您想要执行此操作的频率)。选择你最喜欢的!

于 2013-06-27T11:13:48.367 回答
1

您的正则表达式简化为dog(?!cat)(因为第一个lookbehind 不消耗任何内容),因此它正在替换任何dog未跟随的实例cat

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

于 2013-06-27T10:45:19.107 回答