JavaScript 中的负向后等价

Posted

技术标签:

【中文标题】JavaScript 中的负向后等价【英文标题】:Negative lookbehind equivalent in JavaScript 【发布时间】:2021-04-16 06:39:39 【问题描述】:

有没有办法在 javascript 正则表达式中实现 negative lookbehind 的等价物?我需要匹配一个不以特定字符集开头的字符串。

如果在字符串的开头找到匹配的部分,我似乎无法找到执行此操作而不会失败的正则表达式。负向回溯似乎是唯一的答案,但 JavaScript 没有。

这是我想要使用的正则表达式,但它没有:

(?<!([abcdefg]))m

所以它会匹配 'jim' 或 'm' 中的 'm',但不匹配 'jam'

【问题讨论】:

考虑发布正则表达式,因为它看起来带有负面的lookbehind;这可能会更容易做出回应。 想要追踪lookbehind等采用情况的请参考ECMAScript 2016+ compatibility table @WiktorStribiżew :在 2018 年规范中添加了后视功能。 Chrome 支持它们,但 Firefox still hasn't implemented the spec. 这还需要看看吗? (?:[^abcdefg]|^)(m) 呢?如"mango".match(/(?:[^abcdefg]|^)(m)/)[1] 【参考方案1】:

使用

newString = string.replace(/([abcdefg])?m/, function($0,$1) return $1?$0:'m';);

【讨论】:

这没有任何作用:newString 将始终等于 string。为什么这么多赞? @MikeM:因为重点只是为了演示一种匹配技术。 @bug。什么都不做的演示是一种奇怪的演示。答案似乎只是复制和粘贴,而对它的工作原理没有任何了解。因此,缺乏随附的解释,也无法证明任何事情都已匹配。 @MikeM:SO 的规则是,如果它按书面方式回答问题,则它是正确的。 OP 没有指定用例 这个概念是正确的,但是它不是很好地演示。尝试在 JS 控制台中运行它..."Jim Jam Momm m".replace(/([abcdefg])?m/g, function($0, $1) return $1 ? $0 : '[match]'; );。它应该返回Ji[match] Jam Mo[match][match] [match]。但也请注意,正如 Jason 下面提到的,它可能会在某些边缘情况下失败。【参考方案2】:

Mijoja 的策略适用于您的具体情况,但不适用于一般情况:

js>newString = "Fall ball bill balll llama".replace(/(ba)?ll/g,
   function($0,$1) return $1?$0:"[match]";);
Fa[match] ball bi[match] balll [match]ama

这是一个示例,目标是匹配一个双 l,但如果它前面有“ba”则不匹配。请注意“球”这个词——真正的后视应该抑制了前 2 个 l's 但与第二对匹配。但是通过匹配前 2 个 l,然后将该匹配作为误报忽略,正则表达式引擎从该匹配的 end 继续,并忽略误报中的任何字符。

【讨论】:

啊,你是对的。然而,这比我以前更接近了。我可以接受这一点,直到出现更好的东西(比如 javascript 实际实现了lookbehinds)。【参考方案3】:

自 2018 年以来,Lookbehind Assertions 成为 ECMAScript language specification 的一部分。

// positive lookbehind
(?<=...)
// negative lookbehind
(?<!...)

2018 年之前的答案

由于 Javascript 支持negative lookahead,一种方法是:

    反转输入字符串

    与反向正则表达式匹配

    反转并重新格式化匹配项


const reverse = s => s.split('').reverse().join('');

const test = (stringToTests, reversedRegexp) => stringToTests
  .map(reverse)
  .forEach((s,i) => 
    const match = reversedRegexp.test(s);
    console.log(stringToTests[i], match, 'token:', match ? reverse(reversedRegexp.exec(s)[0]) : 'Ø');
  );

示例 1:

关注@andrew-ensley 的问题:

test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)

输出:

jim true token: m
m true token: m
jam false token: Ø

示例 2:

跟随@neaumusic 评论(匹配max-height 但不匹配line-height,令牌为height):

test(['max-height', 'line-height'], /thgieh(?!(-enil))/)

输出:

max-height true token: height
line-height false token: Ø

【讨论】:

这种方法的问题是当你同时有前瞻和后视时它不起作用 你能举个例子吗,说我想匹配max-height而不是line-height,我只想匹配height 如果任务是替换两个连续相同的符号(不超过 2 个),但没有以某个符号开头,这将无济于事。 ''(?!\() 将从另一端替换''(''test'''''''test 中的撇号,从而留下(''test'NNNtest 而不是(''testNNN'test【参考方案4】:

你可以通过否定你的字符集来定义一个非捕获组:

(?:[^a-g])m

...这将匹配每个 m NOT 前面有任何这些字母。

【讨论】:

我认为匹配实际上也会覆盖前面的字符。 ^ 这是真的。一个字符类代表……一个字符!您的所有非捕获组所做的并不是在替换上下文中提供该值。您的表达不是说“每个 m 前面都没有任何这些字母”,而是说“每个 m 前面都有一个字符,它不是任何这些字母” 对于解决原始问题(字符串开头)的答案,它还必须包含一个选项,因此生成的正则表达式将为(?:[^a-g]|^)m。运行示例见regex101.com/r/jL1iW6/2。 使用 void 逻辑并不总能产生预期的效果。【参考方案5】:

/(?![abcdefg])[^abcdefg]m/gi 是的,这是一个技巧。

【讨论】:

检查(?![abcdefg]) 完全是多余的,因为[^abcdefg] 已经完成了防止这些字符匹配的工作。 这将不匹配前面没有字符的“m”。【参考方案6】:

假设您要查找前面没有unsigned 的所有int

支持消极的后视:

(?<!unsigned )int

不支持负后视:

((?!unsigned ).9|^.0,8)int

基本上的想法是抓取 n 个前面的字符并排除匹配负前瞻,但也匹配前面没有 n 个字符的情况。 (其中 n 是后视的长度)。

所以有问题的正则表达式:

(?<![abcdefg])m

将转化为:

([^abcdefg]|^)m
((?![abcdefg]).|^)m

您可能需要使用捕获组来找到您感兴趣的字符串的确切位置,或者您想用其他内容替换特定部分。

【讨论】:

这应该是正确的答案。请参阅:"So it would match the 'm' in 'jim' or 'm', but not 'jam'".replace(/(j(?!([abcdefg])).|^)m/g, "$1[MATCH]") 返回"So it would match the 'm' in 'ji[MATCH]' or 'm', but not 'jam'" 它非常简单而且有效! 太棒了!使用负前瞻作为旧 JavaScript 的解决方法! 你能帮我解决这个问题吗:/\B(? 【参考方案7】:

按照 Mijoja 的思路,借鉴 JasonS 暴露的问题,我有了这个想法;我检查了一下,但不确定自己,所以在 js 正则表达式中由比我更专家的人进行验证会很棒:)

var re = /(?=(..|^.?)(ll))/g
         // matches empty string position
         // whenever this position is followed by
         // a string of length equal or inferior (in case of "^")
         // to "lookbehind" value
         // + actual value we would want to match

,   str = "Fall ball bill balll llama"

,   str_done = str
,   len_difference = 0
,   doer = function (where_in_str, to_replace)
    
        str_done = str_done.slice(0, where_in_str + len_difference)
        +   "[match]"
        +   str_done.slice(where_in_str + len_difference + to_replace.length)

        len_difference = str_done.length - str.length
            /*  if str smaller:
                    len_difference will be positive
                else will be negative
            */

       /*  the actual function that would do whatever we want to do
            with the matches;
            this above is only an example from Jason's */



        /*  function input of .replace(),
            only there to test the value of $behind
            and if negative, call doer() with interesting parameters */
,   checker = function ($match, $behind, $after, $where, $str)
    
        if ($behind !== "ba")
            doer
            (
                $where + $behind.length
            ,   $after
                /*  one will choose the interesting arguments
                    to give to the doer, it's only an example */
            )
        return $match // empty string anyhow, but well
    
str.replace(re, checker)
console.log(str_done)

我的个人输出:

Fa[match] ball bi[match] bal[match] [match]ama

原理是在字符串中任意两个字符之间的每个点调用checker,只要该位置是以下的起点:

--- 任何不想要的大小的子字符串(这里是'ba',因此是..)(如果知道那个大小;否则它可能会更难做)

--- --- 如果它是字符串的开头,则小于该值:^.?

然后,

--- 实际要寻找的东西(这里是'll')。

在每次调用checker时,都会有一个测试来检查ll之前的值是否不是我们不想要的(!== 'ba');如果是这种情况,我们调用另一个函数,它必须是这个 (doer) 才能对 str 进行更改,如果目的是这个,或者更一般地说,它将输入必要的数据手动处理str的扫描结果。

这里我们改变了字符串,所以我们需要跟踪长度的差异,以抵消replace给出的位置,所有这些都是在str上计算的,它本身永远不会改变。

由于原始字符串是不可变的,我们可以使用变量str 来存储整个操作的结果,但我认为这个例子已经被替换变得复杂了,使用另一个变量(str_done)会更清晰.

我想就性能而言,它一定非常苛刻:所有那些将 '' 替换为 ''、this str.length-1 次的毫无意义的替换,加上这里由 doer 手动替换,这意味着很多切片...... 可能在上述特定情况下可以进行分组,方法是将字符串仅切割一次,在我们想要插入[match].join() 的位置周围加上[match] 本身。

另一件事是我不知道它会如何处理更复杂的情况,即虚假后视的复杂值......长度可能是最有问题的数据。

并且,在checker 中,如果$behind 有多种不需要的值的可能性,我们将不得不使用另一个正则表达式对其进行测试(最好在checker 之外缓存(创建),避免在每次调用 checker 时创建相同的正则表达式对象)以了解它是否是我们试图避免的。

希望我已经清楚了;如果不犹豫,我会努力的更好。 :)

【讨论】:

【参考方案8】:

这很有效

"jim".match(/[^a-g]m/)
> ["im"]
"jam".match(/[^a-g]m/)
> null

搜索和替换示例

"jim jam".replace(/([^a-g])m/g, "$1M")
> "jiM jam"

请注意,否定的后视字符串必须为 1 个字符长才能起作用。

【讨论】:

不完全。在“jim”中,我不想要“i”;只是“m”。并且"m".match(/[^a-g]m/) 也会产生null。在那种情况下,我也想要“m”。【参考方案9】:

使用您的情况,如果您想用某些东西替换 m,例如将其转换为大写M,可以在捕获组中取反。

匹配([^a-g])m,替换为$1M

"jim jam".replace(/([^a-g])m/g, "$1M")
\\jiM jam

([^a-g]) 将匹配 a-g 范围内的任何字符 not(^),并将其存储在第一个捕获组中,以便您可以使用 $1 访问它。

所以我们在jim 中找到im 并将其替换为iM,从而得到jiM

【讨论】:

【参考方案10】:

Lookbehind Assertions 于 2018 年将accepted 纳入ECMAScript specification。

正向回溯用法:

console.log(
  "$9.99  €8.47".match(/(?<=\$)\d+\.\d*/) // Matches "9.99"
);

负后向用法:

console.log(
  "$9.99  €8.47".match(/(?<!\$)\d+\.\d*/) // Matches "8.47"
);

平台支持:

✔️V8 ✔️Google Chrome 62.0 ✔️ Microsoft Edge 79.0 ✔️Node.js 6.0 behind a flag and 9.0 without a flag ✔️ Deno(所有版本) ✔️SpiderMonkey ✔️Mozilla Firefox 78.0 ?️ JavaScriptCore:Apple is working on it ?️ Apple Safari ?️ ios WebView(iOS + iPadOS 上的所有浏览器) ❌ Chakra:Microsoft was working on it 但现在已经放弃 Chakra 以支持 V8 ❌ Internet Explorer ❌ 79 之前的 Edge 版本(基于 Edgehtml+Chakra 的版本)

【讨论】:

四年过去了,苹果仍然没有添加它。 :-|【参考方案11】:

这就是我为 Node.js 8 实现 str.split(/(?&lt;!^)@/) 的方式(不支持后向查看):

str.split('').reverse().join('').split(/@(?!$)/).map(s => s.split('').reverse().join('')).reverse()

有效吗?是的(未经测试的unicode)。不愉快?是的。

【讨论】:

【参考方案12】:

如前所述,JavaScript 现在允许后视。在较旧的浏览器中,您仍然需要一种解决方法。

我敢打赌,如果没有后视,就无法找到一个正则表达式,它可以准确地提供结果。您所能做的就是与小组合作。假设您有一个正则表达式(?&lt;!Before)Wanted,其中Wanted 是您要匹配的正则表达式,Before 是计算不应该在匹配之前的正则表达式。您能做的最好的事情就是否定正则表达式Before 并使用正则表达式NotBefore(Wanted)。想要的结果是第一组$1

在您的情况下Before=[abcdefg] 很容易否定NotBefore=[^abcdefg]。所以正则表达式是[^abcdefg](m)。如果你需要Wanted的位置,你也必须对NotBefore进行分组,这样才能得到第二组。

如果Before模式的匹配具有固定长度n,即如果模式不包含重复标记,则可以避免否定Before模式并使用正则表达式(?!Before).n(Wanted),但仍然必须使用第一组或使用正则表达式(?!Before)(.n)(Wanted) 并使用第二组。在这个例子中,模式Before实际上有一个固定的长度,即1,所以使用正则表达式(?![abcdefg]).(m)(?![abcdefg])(.)(m)。如果您对所有匹配项感兴趣,请添加g 标志,请参阅我的代码 sn-p:

function TestSORegEx() 
  var s = "Donald Trump doesn't like jam, but Homer Simpson does.";
  var reg = /(?![abcdefg])(.1)(m)/gm;
  var out = "Matches and groups of the regex " + 
            "/(?![abcdefg])(.1)(m)/gm in \ns = \"" + s + "\"";
  var match = reg.exec(s);
  while(match) 
    var start = match.index + match[1].length;
    out += "\nWhole match: " + match[0] + ", starts at: " + match.index
        +  ". Desired match: " + match[2] + ", starts at: " + start + ".";   
    match = reg.exec(s);
  
  out += "\nResulting string after statement s.replace(reg, \"$1*$2*\")\n"
         + s.replace(reg, "$1*$2*");
  alert(out);

【讨论】:

以上是关于JavaScript 中的负向后等价的主要内容,如果未能解决你的问题,请参考以下文章

笔趣阁“宇宙”背后,用户与平台的负向循环

JS leetcode 存在重复元素 II 题解分析,记一次震惊的负向优化

正则表达式:如何在 PL/SQL 中实现负向后查找

用于替换列表分隔符和字符串结尾的可变长度负向后搜索

JS正则向前查找和向后查找

sql优化