如何使用正则表达式匹配重叠的字符串?

66

假设我有一个字符串

"12345"

如果我使用.match(/\d{3}/g),我只会得到一个匹配项:"123"。为什么我没有得到[ "123", "234", "345" ]


8
因为 "123" 已经匹配到了,所以你只能得到一个匹配,而剩下的字符 "45" 不匹配。如果你使用 /\d{2}/g ,那么你将会得到 ['12','34']。无论如何,在 Stack Overflow 中有一个答案可以获得重叠的匹配字符串:https://dev59.com/-GUq5IYBdhLWcg3wBLw-#14863268 - EfrainReyes
6个回答

40
string#match 带有全局标志的正则表达式返回一个匹配的子字符串数组。正则表达式 /\d{3}/g 匹配并“消耗”(即读入缓冲区并将其索引提前到当前匹配字符右侧位置)三位数字序列。因此,在“吃掉”123后,索引位于3之后,唯一剩下的可用于解析的子字符串为45 - 这里没有匹配。

我认为在regex101.com使用的技术在这里也值得考虑:使用零宽度断言(具有捕获组的正向先行断言)来测试输入字符串内的所有位置。每次测试后,RegExp.lastIndex(它是一个正则表达式的读/写整数属性,指定下一个匹配的开始索引)手动“推进”,以避免无限循环。

请注意,这是一种在.NET (Regex.Matches)、Python (re.findall)、PHP (preg_match_all)、Ruby (String#scan) 中实现的技术,也可以在Java中使用。这里是使用matchAll的演示:

var re = /(?=(\d{3}))/g;
console.log( Array.from('12345'.matchAll(re), x => x[1]) );

这是一个符合ES5标准的演示:

var re = /(?=(\d{3}))/g;
var str = '12345';
var m, res = [];
 
while (m = re.exec(str)) {
    if (m.index === re.lastIndex) {
        re.lastIndex++;
    }
    res.push(m[1]);
}

console.log(res);

这里有一个regex101.com演示

请注意,可以使用“常规”消耗\d{3}模式并在每次成功匹配后手动设置re.lastIndexm.index+1值来编写相同的内容:

var re = /\d{3}/g;
var str = '12345';
var m, res = [];

while (m = re.exec(str)) {
    res.push(m[0]);
    re.lastIndex = m.index + 1; // <- Important
}
console.log(res);


1
哦,是的,谢谢提醒!已删除相关评论:“我认为最后一个源代码块中只有一个小错误:需要使用res.push(m[1]);而不是res.push(m[0]);,因为匹配结果存储在数组m的索引1而不是索引0中。” - Nighty42

25
你不能只用正则表达式来做到这一点,但你可以接近实现它。

var pat = /(?=(\d{3}))\d/g;
var results = [];
var match;

while ( (match = pat.exec( '1234567' ) ) != null ) {
  results.push( match[1] );
}

console.log(results);

换句话说,你在前瞻中捕获所有三个数字,然后回退并以正常方式匹配一个字符,只是为了推进匹配位置。无论你如何消耗这个字符,都没有关系;.\d都可以。如果你真的感到冒险,你可以只使用前瞻,让JavaScript处理推进。
这段代码改编自这个答案

由于 while 循环的条件从未改变,因此此源代码会产生无限循环... 正如 @Wiktor Stribiżew 在他的答案中提到的那样,为了能够更改匹配结果,必须更改正则表达式对象的索引。 - Nighty42
@Nighty42 你运行了这段代码吗?那里没有无限循环。 - undefined

13

当一个表达式匹配时,通常会“消耗”匹配的字符。因此,在表达式匹配123之后,只剩下不匹配模式的45


7
为了回答这个问题,你可以手动更改最后一个匹配项的索引(需要循环):
var input = '12345', 
    re = /\d{3}/g, 
    r = [], 
    m;
while (m = re.exec(input)) {
    re.lastIndex -= m[0].length - 1;
    r.push(m[0]);
}
r; // ["123", "234", "345"]

这里提供了一个便利函数:

function matchOverlap(input, re) {
    var r = [], m;
    // prevent infinite loops
    if (!re.global) re = new RegExp(
        re.source, (re+'').split('/').pop() + 'g'
    );
    while (m = re.exec(input)) {
        re.lastIndex -= m[0].length - 1;
        r.push(m[0]);
    }
    return r;
}

使用示例:

matchOverlap('12345', /\D{3}/)      // []
matchOverlap('12345', /\d{3}/)      // ["123", "234", "345"]
matchOverlap('12345', /\d{3}/g)     // ["123", "234", "345"]
matchOverlap('1234 5678', /\d{3}/)  // ["123", "234", "567", "678"]
matchOverlap('LOLOL', /lol/)        // []
matchOverlap('LOLOL', /lol/i)       // ["LOL", "LOL"]

0
我认为在这种情况下不使用正则表达式可能更好。如果你想将字符串分成三组,你可以从偏移量开始循环遍历字符串:

let s = "12345"
let m = Array.from(s.slice(2), (_, i) => s.slice(i, i+3))
console.log(m)


-1

使用 (?=(\w{3}))

(3 为序列中字母的数量)


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