JavaScript中不支持正则表达式的负回顾。

6

请考虑以下内容:

var re = /(?<=foo)bar/gi;

在 Plunker 中,这是一种无效的正则表达式。为什么呢?

1
因为JS不支持向后查找。使用var re = /foo(bar)/gi;。那么真正的问题是什么? - Wiktor Stribiżew
1
这是正向后行断言,顺便说一下,这是负向后行断言 (?<!foo),正如Wiktor所说,JavaScript不支持后行断言。 - Marcos Casagrande
我想匹配>=和>,但不包括在span标签内的>。样本数据是<span class="css">55</span> 2 >= 1 2 > 1。 - Jackie
这是一个示例:https://regex101.com/r/fH0nF3/1 - Jackie
那段文本来自哪里?它在DOM内部吗? - Marcos Casagrande
用户在文本区域内输入,我需要解析并匹配它们。 - Jackie
2个回答

21

2020年更新: Javascript实现开始本地支持正则表达式的前瞻。ECMAScript 2021的RegExp Lookbehind Assertions草案提议已被接受到ECMA-262 draft specs中,并在Chrome 62+(发布于2017-10-17)中通过V8Irregexp实现,这已经通过Irregexp的shim层Firefox 78+ESR,发布于2020-06-30)中得到了应用。其他JS解释器也将跟进。

在此处查看更详细的支持列表


实现后向断言的遗留解决方案

JavaScript不支持正则表达式后向断言,例如(?<=…)(正向)和(?<!…)(负向),但这并不意味着您不能在JavaScript中实现这种逻辑。

匹配(非全局)

正向后向断言匹配:

// from /(?<=foo)bar/i
var matcher = mystring.match( /foo(bar)/i );
if (matcher) {
  // do stuff with matcher[1] which is the part that matches "bar"
}

固定宽度负回顾匹配:

// from /(?<!foo)bar/i
var matcher = mystring.match( /(?!foo)(?:^.{0,2}|.{3})(bar)/i );
if (matcher) {
  // do stuff with matcher[1] ("bar"), which does not follow "foo"
}

负向后瞻可以在没有全局标志的情况下完成,但只能使用固定宽度,并且您必须计算该宽度(使用alternations可能会变得困难)。使用(?!foo).{3}(bar)将更简单且大致相等,但它不会匹配以“rebar”开头的行,因为.无法匹配换行符,所以我们需要上面的代码中的替代方案来匹配在第四个字符之前具有“bar”的行。
如果您需要使用可变宽度,请使用以下全局解决方案,并在if语句的末尾放置一个break。(这种限制非常普遍。.NETvimJGsoft是唯一支持可变宽度回溯的正则表达式引擎。onlyPCREPHPPerl仅支持固定宽度。Python需要一个替代正则表达式模块来支持此功能。话虽如此,下面的解决方法逻辑应该适用于所有支持正则表达式的语言。)

匹配(全局)

当您需要在给定字符串中循环每个匹配项(全局匹配,使用g修饰符)时,您必须在每个循环迭代中重新定义matcher变量,并且必须使用{{link1:RegExp.exec()}}(在循环之前创建{{link2:RegExp}}),因为{{link3:String.match()}}会以不同的方式解释全局修饰符{{link4:并导致无限循环!

全局正向回溯:

var re = /foo(bar)/gi;  // from /(?<=foo)bar/gi
while ( matcher = re.exec(mystring) ) {
  // do stuff with matcher[1] which is the part that matches "bar"
}

“Stuff”当然可以包括填充数组以供进一步使用。

全局负回顾:

var re = /(foo)?bar/gi;  // from /(?<!foo)bar/gi
while ( matcher = re.exec(mystring) ) {
  if (!matcher[1]) {
    // do stuff with matcher[0] ("bar"), which does not follow "foo"
  }
}

请注意,有情况不会完全表示负回溯。考虑 /(?<!ba)ll/g Fall ball bill balll llama 匹配的情况。它只会找到三个所需的四个匹配项,因为当它解析 balll 时,它找到了 ball ,然后在 l llama 处延迟了一个字符。只有当结尾的部分匹配可能会干扰不同结尾的部分匹配时才会发生这种情况( balll 破坏了(ba)?ll foobarbar (foo)?bar 没有问题)。唯一的解决方法是使用上述固定宽度方法。

替换

模仿JavaScript中的后顾断言是一篇很棒的文章,描述了如何实现这个功能。
它甚至有一个后续,指向JS中实现这个功能的短函数集合

String.replace()中实现后顾断言要容易得多,因为你可以创建一个匿名函数作为替换,并在该函数中处理后顾断言逻辑。

这些适用于第一个匹配项,但只需添加g修饰符即可使其全局匹配。

正向后顾替换:

// assuming you wanted mystring.replace(/(?<=foo)bar/i, "baz"):
mystring = mystring.replace( /(foo)?bar/i,
  function ($0, $1) { return ($1 ? $1 + "baz" : $0) }
);

这段代码将目标字符串中的bar替换为baz,但前提是它们跟在foo后面。如果是这样,就会匹配到$1,然后三元运算符(?:)返回匹配到的文本和替换文本(但不包括bar部分)。否则,三元运算符返回原始文本。

负回顾后发替换:

// assuming you wanted mystring.replace(/(?<!foo)bar/i, "baz"):
mystring = mystring.replace( /(foo)?bar/i,
  function ($0, $1) { return ($1 ? $0 : "baz") }
);

这基本上是相同的,但由于它是负回顾,所以当$1缺失时起作用(我们不需要在这里说$1 + "baz",因为我们知道$1是空的)。
这与其他动态宽度负回顾的解决方法具有相同的注意事项,并且可以通过使用固定宽度方法来修复。

原始问题在其他地方(问题的评论和另一个答案)列出了细化内容。我也回答了那个经过细化的版本,可以在这里找到(向下滚动到“您的特定用例”),但后来将其删除,以使答案更简洁并更适用于实际问题。 - Adam Katz
关于 全局匹配前瞻 中的注释:为了避免 干扰 问题,将 (?<!ba)ll 转换为 (ba)?ll:一个简单的解决方法是只消耗一个位置并使用前瞻:(ba)?(?=ll). - Casimir et Hippolyte
关于固定宽度负回顾匹配:模式(?!foo)(?:^.{0,2}|.{3})(bar)最好写成这样:(?:^.{0,2}|(?!foo).{3})(bar)(此外,想象一下,如果你不是在处理foo/bar而是在以fooar开头的字符串中进行处理)。 - Casimir et Hippolyte
PCRE、PHP 和 Perl 只能使用固定宽度的正则表达式。PHP 使用 PCRE 正则表达式引擎。另外请注意,固定宽度子模式的交替是可能的:(?<=ab|abc|abcd) - Casimir et Hippolyte
@CasimiretHippolyte - 是的,将鼠标悬停在PHP链接上,您会看到我已经提到它使用libpcre。只要所有交替都具有相同的宽度,您确实可以在固定宽度的后顾中使用交替,但是在所有引擎中,不同宽度的交替都无法工作。我没有审核您的其他评论,但是在全局匹配中进行前瞻可能会存在迭代问题。 - Adam Katz
非常棒的文章,超级有帮助,谢谢。 - Geoffrey Hale

1

以下是使用JS中的DOM解析HTML字符串并仅在标签外执行替换的方法:

var s = '<span class="css">55</span> 2 >= 1 2 > 1';
var doc = document.createDocumentFragment();
var wrapper = document.createElement('myelt');
wrapper.innerHTML = s;
doc.appendChild( wrapper );

function textNodesUnder(el){
  var n, walk=document.createTreeWalker(el,NodeFilter.SHOW_TEXT,null,false);
  while(n=walk.nextNode())
  {
       if (n.parentNode.nodeName.toLowerCase() === 'myelt')
        n.nodeValue =  n.nodeValue.replace(/>=?/g, "EQUAL"); 
  }
  return el.firstChild.innerHTML;
} 
var res = textNodesUnder(doc);
console.log(res);
alert(res);


能否让演示在regex101上运行?https://regex101.com/r/fH0nF3/1。谢谢。 - Jackie
>=? 正则表达式的演示?在这里 - Wiktor Stribiżew
抱歉,我需要更复杂的测试用例。请查看此链接 - Jackie
3
众所周知,在JS中处理HTML应该使用DOM解析器,正则表达式应只针对文本节点运行。请参阅RegEx匹配未闭合的标签,但不包括XHTML自包含标签 - Wiktor Stribiżew

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