负向预查正则表达式

67

我想匹配所有以".htm"结尾的字符串,除非它以"foo.htm"结尾。我通常对正则表达式很熟悉,但是负向先行断言让我感到困惑。为什么这不起作用?

/(?!foo)\.htm$/i.test("/foo.htm");  // returns true. I want false.

我应该使用什么替代方案?我认为我需要一个“负回顾后发”表达式(如果JavaScript支持这种表达方式的话,但我知道它不支持)。


3
很遗憾,JavaScript 不支持正则表达式中的“后顾”功能。 - Phil
2
通常情况下,使用一个或两个循环的简单正则表达式比使用超级庞大(好吧,你想要的不是超级庞大,但代码有增长的趋势)且难以维护的正则表达式更好。 - davin
这可能不是及时的,但是解释为什么这不起作用:您的正则表达式不是0宽度,这意味着在JavaScript中它被翻译为“匹配'.htm'但不是以'foo'开头”,因为“.htm”永远不会以“foo”开头,所以这不起作用。否定前瞻的含义是“在此点上,排除此处为真的匹配项”,但它实际上并不消耗字符串。 - Eric
7个回答

97

问题其实很简单,这样做就可以:

/^(?!.*foo\.htm$).*\.htm$/i.test("/foo.htm"); // returns false

1
+1. 不仅不需要回顾,即使有也并非最佳工具。 - Alan Moore
这是一个非常有用的技巧! - Josh Cole
7
你能解释一下正在发生什么吗?我看到你有一个行首标记(^),但有两个行尾标记($)。这是如何让负向先行断言起作用的? - ericbowden
1
如果你还在疑惑:它匹配了字符串的开头,然后不匹配 .*foo\.htm 直到字符串结尾。因为前瞻不被消耗,所以外面的第二个 $ 实际上是被匹配的。 - IceMetalPunk

20
你所描述的(你的意图)是一个负向后顾,而JavaScript不支持后顾。向前查找从放置它们的字符开始往前查找,并且你将其放置在.之前。所以,你实际上得到的是“任何以.htm结尾的东西,只要从该位置开始的前三个字符(.ht)不是foo”,这总是成立的。
通常,替换负向后顾的方法是匹配比你需要的更多内容,并仅提取你实际需要的部分。这很hacky,根据你具体的情况,你可能可以想出其他办法,但是像这样:
// Checks that the last 3 characters before the dot are not foo:
/(?!foo).{3}\.htm$/i.test("/foo.htm"); // returns false 

1
你给了我足够的东西让我自己完成剩下的部分。这对于我所有的测试用例都有效:/(^.{0,2}|(?!foo).{3})\.htm$/i - gilly3
5
+1 优秀的解释。但是/(?!foo).{3}\.htm$/i无法匹配少于三个字符的名称,例如 a.htm。这里有一个可以匹配所有名称的正则表达式:/^(?!.*foo\.htm$).*\.htm$/i - ridgerunner

2

如前所述,JavaScript 不支持负回顾断言。

但是,您可以使用一种解决方法:

/(foo)?\.htm$/i.test("/foo.htm") && RegExp.$1 != "foo";

这将匹配所有以.htm结尾的内容,但是如果它匹配了foo.htm,它将把"foo"存储在RegExp.$1中,因此您可以单独处理它。

MDN 报告说,RegExp.$1 特性是非标准的。 - alxndr

2

就像Renesis提到的那样,“lookbehind”在JavaScript中不受支持,因此可以考虑将两个正则表达式结合使用:

!/foo\.htm$/i.test(teststring) && /\.htm$/i.test(teststring)

JavaScript 支持 Lookahead。 - gilly3
谢谢 :) 刚刚想起来,可能是我记忆不太好,大约是一年前的事情。 - petho

2

可能这个回答来得有点晚,但我还是会把它留在这里,以防现在(在这个问题被问出7年6个月后)有人遇到同样的问题。

现在,ECMA2018标准已经包含了lookbehinds,并且至少在Chrome的最新版本中受支持。然而,您可以使用或不使用它们来解决这个谜题。

一种使用负向前瞻的解决方案:

Original Answer翻译成"最初的回答"

let testString = `html.htm app.htm foo.tm foo.htm bar.js 1to3.htm _.js _.htm`;

testString.match(/\b(?!foo)[\w-.]+\.htm\b/gi);
> (4) ["html.htm", "app.htm", "1to3.htm", "_.htm"]

一种使用负回顾后断言的解决方案:

testString.match(/\b[\w-.]+(?<!foo)\.htm\b/gi);
> (4) ["html.htm", "app.htm", "1to3.htm", "_.htm"]

一种具有(技术上)正向先行断言的解决方案:

testString.match(/\b(?=[^f])[\w-.]+\.htm\b/gi);
> (4) ["html.htm", "app.htm", "1to3.htm", "_.htm"]

所有这些正则表达式以不同的方式告诉JS引擎相同的事情,它们传递给JS引擎的消息类似于以下内容。

请在此字符串中查找满足以下条件的所有字符序列:

  • 与其他文本(如单词)分开;
  • 由一个或多个英文字母、下划线、连字符、点或数字组成;
  • 以".htm"结尾;
  • 除此之外,在".htm"之前的部分可以是任何内容,但不能是"foo"。

等等。


1

String.prototype.endsWith (ES6)

String.prototype.endsWith (ES6)

console.log( /* !(not)endsWith */

    !"foo.html".endsWith("foo.htm"), // true
  !"barfoo.htm".endsWith("foo.htm"), // false (here you go)
     !"foo.htm".endsWith("foo.htm"), // false (here you go)
   !"test.html".endsWith("foo.htm"), // true
    !"test.htm".endsWith("foo.htm")  // true

);


0
你可以使用类似于/(.|..|.*[^f]..|.*f[^o].|.*fo[^o])\.htm$/的方式来模拟负回顾,但编程方法会更好。

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