获取 JavaScript 正则表达式中每个捕获的索引

IT技术 javascript regex capturing-group
2021-03-07 09:33:34

我想匹配一个像/(a).(b)(c.)d/with的正则表达式"aabccde",并得到以下信息:

"a" at index = 0
"b" at index = 2
"cc" at index = 3

我怎样才能做到这一点?String.match 返回匹配列表和完整匹配开始的索引,而不是每个捕获的索引。

编辑:一个不适用于普通 indexOf 的测试用例

regex: /(a).(.)/
string: "aaa"
expected result: "a" at 0, "a" at 2

注意:问题类似于Javascript Regex: How to find index of each subexpression? ,但我无法修改正则表达式使每个子表达式成为捕获组。

6个回答

目前有一个提案(第 3 阶段)在原生 Javascript 中实现这一点:

ECMAScript 的 RegExp 匹配索引

ECMAScript RegExp 匹配索引提供了关于捕获的子字符串相对于输入字符串开头的开始和结束索引的附加信息。

...我们建议indices在 的数组结果(子字符串数组采用附加属性RegExp.prototype.exec()此属性本身将是一个索引数组,其中包含每个捕获的子字符串的一对开始和结束索引。任何不匹配的捕获组都将undefined类似于它们在子字符串数组中的相应元素此外,索引数组本身将有一个组属性,其中包含每个命名捕获组的开始和结束索引。

下面是一个如何工作的例子:

const re1 = /a+(?<Z>z)?/d;

// indices are relative to start of the input string:
const s1 = "xaaaz";
const m1 = re1.exec(s1);
m1.indices[0][0] === 1;
m1.indices[0][1] === 5;
s1.slice(...m1.indices[0]) === "aaaz";

m1.indices[1][0] === 4;
m1.indices[1][1] === 5;
s1.slice(...m1.indices[1]) === "z";

m1.indices.groups["Z"][0] === 4;
m1.indices.groups["Z"][1] === 5;
s1.slice(...m1.indices.groups["Z"]) === "z";

// capture groups that are not matched return `undefined`:
const m2 = re1.exec("xaaay");
m2.indices[1] === undefined;
m2.indices.groups["Z"] === undefined;

因此,对于问题中的代码,我们可以这样做:

const re = /(a).(b)(c.)d/d;
const str = 'aabccde';
const result = re.exec(str);
// indicies[0], like result[0], describes the indicies of the full match
const matchStart = result.indicies[0][0];
result.forEach((matchedStr, i) => {
  const [startIndex, endIndex] = result.indicies[i];
  console.log(`${matchedStr} from index ${startIndex} to ${endIndex} in the original string`);
  console.log(`From index ${startIndex - matchStart} to ${endIndex - matchStart} relative to the match start\n-----`);
});

输出:

aabccd from index 0 to 6 in the original string
From index 0 to 6 relative to the match start
-----
a from index 0 to 1 in the original string
From index 0 to 1 relative to the match start
-----
b from index 2 to 3 in the original string
From index 2 to 3 relative to the match start
-----
cc from index 4 to 6 in the original string
From index 4 to 6 relative to the match start

请记住,该indicies数组包含相对于字符串开头的匹配组的索引,而不是相对于匹配开头的索引


该提案目前处于第 3 阶段,这表明规范文本已经完成,并且 TC39 中需要批准它的每个人都已经这样做了 - 剩下的就是让环境开始交付它,以便可以完成最终测试,然后将纳入官方标准。

一个 polyfill在这里可用

不久前我为此编写了MultiRegExp只要您没有嵌套的捕获组,它就可以解决问题。它的工作原理是在 RegExp 中的捕获组之间插入捕获组,并使用所有中间组来计算请求的组位置。

var exp = new MultiRegExp(/(a).(b)(c.)d/);
exp.exec("aabccde");

应该回来

{0: {index:0, text:'a'}, 1: {index:2, text:'b'}, 2: {index:3, text:'cc'}}

现场版

不错的收获!这是预期的行为,但我需要更新错误消息。我们需要有覆盖整个输出的捕获组,因此不允许在捕获组(只返回匹配项之一)上重复。快速解决方法是添加一个子组并将正则表达式更改为 /((?:ba)+).(a*)/。我已经更新了我的 git repo 上的自述文件来描述这种行为。
2021-04-22 09:33:34
你的对象看起来不错!尽管error当我尝试使用(ba)+.(a*)with text的正则表达式时给出了实时版本babaaaaa
2021-04-23 09:33:34

我创建了一个小的正则表达式解析器,它也能够像魅力一样解析嵌套组。它很小但很大。不完全是。就像唐纳德的手一样。如果有人可以测试它,我会非常高兴,因此它将经过实战测试。可以在以下位置找到:https : //github.com/valorize/MultiRegExp2

用法:

let regex = /a(?: )bc(def(ghi)xyz)/g;
let regex2 = new MultiRegExp2(regex);

let matches = regex2.execForAllGroups('ababa bcdefghixyzXXXX'));

Will output:
[ { match: 'defghixyz', start: 8, end: 17 },
  { match: 'ghi', start: 11, end: 14 } ]

基于ecma 正则表达式语法,我编写了一个解析器,分别是 RegExp 类的扩展,它解决了这个问题(完全索引的 exec 方法)以及 JavaScript RegExp 实现的其他限制,例如:基于组的搜索和替换。您可以在此处测试和下载实现(也可以作为 NPM module使用)。

实现工作如下(小例子):

//Retrieve content and position of: opening-, closing tags and body content for: non-nested html-tags.
var pattern = '(<([^ >]+)[^>]*>)([^<]*)(<\\/\\2>)';
var str = '<html><code class="html plain">first</code><div class="content">second</div></html>';
var regex = new Regex(pattern, 'g');
var result = regex.exec(str);

console.log(5 === result.length);
console.log('<code class="html plain">first</code>'=== result[0]);
console.log('<code class="html plain">'=== result[1]);
console.log('first'=== result[3]);
console.log('</code>'=== result[4]);
console.log(5=== result.index.length);
console.log(6=== result.index[0]);
console.log(6=== result.index[1]);
console.log(31=== result.index[3]);
console.log(36=== result.index[4]);

我也尝试了@velop 的实现,但该实现似乎有问题,例如它没有正确处理反向引用,例如“/a(?: )bc(def( \1 ghi)xyz)/g” - 在前面添加括号时反向引用\1需要相应地增加(在他的实现中不是这种情况)。

所以,你有一个文本和一个正则表达式:

txt = "aabccde";
re = /(a).(b)(c.)d/;

第一步是获取与正则表达式匹配的所有子字符串的列表:

subs = re.exec(txt);

然后,您可以对每个子字符串的文本进行简单搜索。您必须将最后一个子字符串的位置保存在一个变量中。我已将这个变量命名为cursor

var cursor = subs.index;
for (var i = 1; i < subs.length; i++){
    sub = subs[i];
    index = txt.indexOf(sub, cursor);
    cursor = index + sub.length;


    console.log(sub + ' at index ' + index);
}

编辑:感谢@nhahtdh,我改进了机制并制作了完整的功能:

String.prototype.matchIndex = function(re){
    var res  = [];
    var subs = this.match(re);

    for (var cursor = subs.index, l = subs.length, i = 1; i < l; i++){
        var index = cursor;

        if (i+1 !== l && subs[i] !== subs[i+1]) {
            nextIndex = this.indexOf(subs[i+1], cursor);
            while (true) {
                currentIndex = this.indexOf(subs[i], index);
                if (currentIndex !== -1 && currentIndex <= nextIndex)
                    index = currentIndex + 1;
                else
                    break;
            }
            index--;
        } else {
            index = this.indexOf(subs[i], cursor);
        }
        cursor = index + subs[i].length;

        res.push([subs[i], index]);
    }
    return res;
}


console.log("aabccde".matchIndex(/(a).(b)(c.)d/));
// [ [ 'a', 1 ], [ 'b', 2 ], [ 'cc', 3 ] ]

console.log("aaa".matchIndex(/(a).(.)/));
// [ [ 'a', 0 ], [ 'a', 1 ] ] <-- problem here

console.log("bababaaaaa".matchIndex(/(ba)+.(a*)/));
// [ [ 'ba', 4 ], [ 'aaa', 6 ] ]
以你的例子,我明白了,ba at index 0 aaa at index 3预期的结果是什么?
2021-04-18 09:33:34
这绝对不是一般情况的解决方案。例如text = "babaaaaa"re = /(ba)+.(a*)/
2021-05-01 09:33:34
re = /((ba))+.(a*)/它在正则表达式捕获ba两次时起作用
2021-05-04 09:33:34
它仍然是错误的。aaa应该在索引 7(对于最后一个测试用例)。(我怀疑在不分析正则表达式的情况下是否有一个简单的通用解决方案)。
2021-05-10 09:33:34
ba应该在索引 2 处,并且aaa应该在索引 5 处。baba将被匹配(ba)+,但由于捕获的部分是重复的,因此只捕获最后一个实例,因此索引 2(在这种情况下并不重要,但它当输入是"bbbaba",正则表达式是/(b+a)+/)时很重要。aaa位于索引 5 处,因为babaa与 匹配,(ba)+.其余aaa由 匹配(a*)
2021-05-11 09:33:34