JavaScript 中的负向后等价
Posted
技术标签:
【中文标题】JavaScript 中的负向后等价【英文标题】:Negative lookbehind equivalent in JavaScript 【发布时间】:2010-10-13 02:06:24 【问题描述】:有没有办法在 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】:
自 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
。【参考方案2】:
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 的版本)【讨论】:
【参考方案3】:假设您要查找前面没有unsigned
的所有int
:
支持消极的后视:
(?<!unsigned )int
不支持负后视:
((?!unsigned ).9|^.0,8)int
基本上的想法是抓取 n 个前面的字符并排除匹配负前瞻,但也匹配前面没有 n 个字符的情况。 (其中 n 是后视的长度)。
所以有问题的正则表达式:
(?<!([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(?
【参考方案4】:
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)。【参考方案5】:使用
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 下面提到的,它可能会在某些边缘情况下失败。【参考方案6】:
你可以通过否定你的字符集来定义一个非捕获组:
(?:[^a-g])m
...这将匹配每个 m
NOT 前面有任何这些字母。
【讨论】:
我认为匹配实际上也会覆盖前面的字符。 ^ 这是真的。一个字符类代表……一个字符!您的所有非捕获组所做的并不是在替换上下文中提供该值。您的表达不是说“每个 m 前面都没有任何这些字母”,而是说“每个 m 前面都有一个字符,它不是任何这些字母” 对于解决原始问题(字符串开头)的答案,它还必须包含一个选项,因此生成的正则表达式将为(?:[^a-g]|^)m
。运行示例见regex101.com/r/jL1iW6/2。
使用 void 逻辑并不总能产生预期的效果。【参考方案7】:
这就是我为 Node.js 8 实现 str.split(/(?<!^)@/)
的方式(不支持后向查看):
str.split('').reverse().join('').split(/@(?!$)/).map(s => s.split('').reverse().join('')).reverse()
有效吗?是的(未经测试的unicode)。不愉快?是的。
【讨论】:
【参考方案8】:遵循 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
来存储整个操作的结果,但我认为这个已经被替换复杂的示例使用另一个变量(str_done
)会更清晰.
我想,就性能而言,它一定是相当苛刻的:所有那些将 '' 替换为 '' 的毫无意义的替换,this str.length-1
次,加上这里由 doer 手动替换,这意味着很多切片......
可能在上述特定的情况下可以进行分组,方法是将字符串仅切割一次,在我们要插入 [match]
和 .join()
的位置周围加上 [match]
本身。
另一件事是我不知道它会如何处理更复杂的情况,即假后视的复杂值......长度可能是最有问题的数据。
并且,在checker
中,如果$behind 有多种不需要的值的可能性,我们将不得不使用另一个正则表达式对其进行测试(最好在checker
之外缓存(创建),避免在每次调用 checker
) 时创建相同的正则表达式对象,以了解它是否是我们试图避免的。
希望我已经清楚了;如果不犹豫,我会努力的更好。 :)
【讨论】:
【参考方案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】:如前所述,JavaScript 现在允许后视。在较旧的浏览器中,您仍然需要一种解决方法。
我敢打赌,如果没有后视,就无法找到一个能够准确提供结果的正则表达式。您所能做的就是与小组合作。假设您有一个正则表达式(?<!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);
【讨论】:
【参考方案11】:这很有效
"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”。【参考方案12】:
/(?![abcdefg])[^abcdefg]m/gi
是的,这是一个技巧。
【讨论】:
检查(?![abcdefg])
完全是多余的,因为[^abcdefg]
已经完成了防止这些字符匹配的工作。
这不会匹配前面没有字符的“m”。以上是关于JavaScript 中的负向后等价的主要内容,如果未能解决你的问题,请参考以下文章