【问题标题】:Get index of each capture in a JavaScript regex获取 JavaScript 正则表达式中每个捕获的索引
【发布时间】:2013-04-02 19:06:48
【问题描述】:

我想将/(a).(b)(c.)d/ 之类的正则表达式与"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?,但我无法修改正则表达式以使每个子表达式都成为捕获组。

【问题讨论】:

  • 您的所有子表达式都已在捕获组中。
  • @Asad,在哪里? 2 个字母不在捕获组内。
  • 如果使用全局匹配,可以得到捕获组的重复案例。在这种情况下,您需要使用回调函数,如您在问题中的链接所示。
  • @canon 请检查我的编辑以获取一个无法使用的简单测试用例。
  • 似乎没有任何函数可以返回此信息。但是,我很少看到任何用于获取匹配索引的用法,可能除了您想要编写正则表达式测试器的情况。

标签: javascript regex capturing-group


【解决方案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 ] ]

【讨论】:

  • 这绝对不是一般情况下的解决方案。例如text = "babaaaaa"re = /(ba)+.(a*)/
  • 我得到了你的例子,ba at index 0 aaa at index 3。预期的结果是什么?
  • ba应该在索引2,aaa应该在索引5。baba会被(ba)+匹配,但是由于捕获的部分是重复的,所以只有最后一个实例被捕获,因此索引 2(在这种情况下并不重要,但当输入为 "bbbaba" 且正则表达式为 /(b+a)+/ 时很重要)。 aaa 位于索引 5,因为 babaa(ba)+. 匹配,其余 aaa(a*) 匹配。
  • re = /((ba))+.(a*)/ 它在正则表达式捕获ba 两次时起作用。
  • 还是错了。 aaa 应该在索引 7 处(对于最后一个测试用例)。 (我怀疑没有分析正则表达式的简单通用解决方案)。
【解决方案2】:

我不确定您对搜索的确切要求是什么,但这是您在第一个示例中使用 Regex.exec() 和 while 循环获得所需输出的方法。

JavaScript

var myRe = /^a|b|c./g;
var str = "aabccde";
var myArray;
while ((myArray = myRe.exec(str)) !== null)
{
  var msg = '"' + myArray[0] + '" ';
  msg += "at index = " + (myRe.lastIndex - myArray[0].length);
  console.log(msg);
}

输出

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

使用lastIndex属性,可以减去当前匹配字符串的长度,得到起始索引。

【讨论】:

  • 这是一个完全错误的方法。以输入 "baaccde" 为例。它与 OP 的原始正则表达式不匹配,但您的正则表达式会匹配它。
  • 说实话,这个例子完全是人为的。它基本上只要求给出字符串:“aabccde”,第一个“a”、“b”和“cc”的索引是什么?这个答案只是为了展示一种获取匹配索引的方法。您可以在获取索引之前轻松检查以确保字符串匹配,但我会尝试改进我的答案。
  • 看看OP的第二个测试用例。
【解决方案3】:

我不久前为此写了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'}}

Live Version

【讨论】:

  • 您的对象看起来不错!虽然当我尝试使用 (ba)+.(a*) 的正则表达式和文本 babaaaaa 时,现场版本给出了 error
  • 不错的收获!这是预期的行为,但我需要更新错误消息。我们需要捕获组覆盖整个输出,因此不允许重复捕获组(仅返回一个匹配项)。一个快速的解决方法是添加一个子组并将正则表达式更改为 /((?:ba)+).(a*)/。我已经更新了我的 git repo 上的自述文件来描述这种行为。
【解决方案4】:

我创建了一个小的正则表达式解析器,它也能够解析嵌套组,就像一个魅力。它很小但很大。不完全是。像唐纳德的手。如果有人可以测试它,我会非常高兴,因此它将经过实战测试。可以在以下位置找到: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 } ]

【讨论】:

    【解决方案5】:

    基于ecma regular expression syntax,我编写了一个解析器,分别是 RegExp 类的扩展,它解决了这个问题(完全索引的 exec 方法)以及 JavaScript RegExp 实现的其他限制,例如:基于组的搜索和替换.你可以test and download the implementation here(也可以作为 NPM 模块使用)。

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

    //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(\1ghi)xyz)/g" - 在前面添加括号时,后向引用 \1 需要相应增加(在他的实现中并非如此)。

    【讨论】:

      【解决方案6】:

      目前有一个proposal(第 4 阶段)可以在本机 Javascript 中实现这一点:

      ECMAScript 的正则表达式匹配索引

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

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

      这是一个如何运作的例子。以下 sn-ps 至少在 Chrome 中运行没有错误:

      const re1 = /a+(?<Z>z)?/d;
      
      // indices are relative to start of the input string:
      const s1 = "xaaaz";
      const m1 = re1.exec(s1);
      console.log(m1.indices[0][0]); // 1
      console.log(m1.indices[0][1]); // 5
      console.log(s1.slice(...m1.indices[0])); // "aaaz"
      
      console.log(m1.indices[1][0]); // 4
      console.log(m1.indices[1][1]); // 5
      console.log(s1.slice(...m1.indices[1])); // "z"
      
      console.log(m1.indices.groups["Z"][0]); // 4
      console.log(m1.indices.groups["Z"][1]); // 5
      console.log(s1.slice(...m1.indices.groups["Z"])); // "z"
      
      // capture groups that are not matched return `undefined`:
      const m2 = re1.exec("xaaay");
      console.log(m2.indices[1]); // undefined
      console.log(m2.indices.groups.Z); // undefined

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

      const re = /(a).(b)(c.)d/d;
      const str = 'aabccde';
      const result = re.exec(str);
      // indices[0], like result[0], describes the indices of the full match
      const matchStart = result.indices[0][0];
      result.forEach((matchedStr, i) => {
        const [startIndex, endIndex] = result.indices[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 3 to 5 in the original string
      From index 3 to 5 relative to the match start
      

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


      可以使用 polyfill here

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-05-07
        • 1970-01-01
        • 2018-06-22
        • 2013-03-24
        • 2019-01-27
        相关资源
        最近更新 更多