Получить индекс каждого захвата в регулярном выражении JavaScript

Я хочу соответствовать регулярному выражению, как /(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: как найти индекс каждого подвыражения?, но я не могу изменить регулярное выражение, чтобы сделать каждое подвыражение захватывающей группой.

9 ответов

В настоящее время есть предложение (этап 3) реализовать это в нативном Javascript:

Индексы соответствия RegExp для ECMAScript

ECMAScript RegExp Match Indicies предоставляют дополнительную информацию о начальных и конечных индексах захваченных подстрок относительно начала входной строки.

... Предлагаем принять дополнительный indices свойство результата массива (массив подстрок) RegExp.prototype.exec(), Это свойство само по себе будет массивом индексов, содержащим пару начальных и конечных индексов для каждой захваченной подстроки. Любые непревзойденные группы захвата будут undefined, аналогично их соответствующему элементу в массиве substrings. Кроме того, массив индексов сам по себе будет иметь свойство groups, содержащее начальный и конечный индексы для каждой именованной группы захвата.

Вот пример того, как все будет работать:

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

// 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/;
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, кто должен одобрить его, сделали это - все, что остается, - это чтобы среды начали отправлять его, чтобы можно было провести окончательные испытания, а затем будет введен в официальный стандарт.

Я написал 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'}}

Живая версия

Я создал небольшой анализатор регулярных выражений, который также может анализировать вложенные группы, как талисман. Это маленький, но огромный. Нет, правда. Как руки Дональда. Я был бы очень рад, если бы кто-то смог это проверить, так что это будет испытание в бою. Его можно найти по адресу: 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 } ]

Итак, у вас есть текст и регулярное выражение:

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 ] ]

Обновленный ответ: 2022 г.

См. String.prototype.matchAll

В matchAll()метод сопоставляет строку с регулярным выражением и возвращает iteratorсовпадающих результатов.

Каждое совпадение представляет собой массив с совпадающим текстом в качестве первого элемента, а затем по одному элементу для каждой группы захвата в скобках. Он также включает в себя дополнительные свойства indexа также input.

      let regexp = /t(e)(st(\d?))/g;
let str = 'test1test2';

for (let match of str.matchAll(regexp)) {
  console.log(match)
}

// => ['test1', 'e', 'st1', '1', index: 0, input: 'test1test2', groups: undefined]
// => ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', groups: undefined]

С 2023 года вы можете сделать это с помощьюmatch()и флаг, упомянутый здесь . Итак, чтобы решить исходный пример, вам просто нужно добавитьdдо конца регулярного выражения:

Поиграйте здесь

Обратите внимание, что первый массив — это начало и конец всего совпадения . После этого идут подгруппы.

Я бы назвал группы, а затем получил доступ к их индексам по имени в разделеgroupsатрибут (match.indices.groups).

Основываясь на синтаксисе регулярных выражений ecma, я написал синтаксический анализатор, соответствующий расширению класса RegExp, который решает помимо этой проблемы (полностью индексированный метод exec), а также другие ограничения реализации JavaScript RegExp, например: поиск и замена на основе групп. Вы можете протестировать и загрузить реализацию здесь (также доступно как модуль 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 (\1 ghi) xyz) / g" - при добавлении паратеза впереди затем обратная ссылка \1 должна быть соответственно увеличена (что не так в его реализации).

С помощью RegExp.prototype.exec() и поиска правильных индексов результата:

let regex1 = /([a-z]+):([0-9]+)/g;
let str1 = 'hello:123';
let array1;
let resultArray = []

while ((array1 = regex1.exec(str1)) !== null) {
  const quantityFound = (Object.keys(array1).length - 3); // 3 default keys
  for (var i = 1; i<quantityFound; i++) { // start in 1 to avoid the complete found result 'hello:123'
    const found = array1[i];
    arraySingleResult = [found, str1.indexOf(found)];
    resultArray.push(arraySingleResult);
  }
}
console.log('result:', JSON.stringify(resultArray));

Я не совсем уверен, каковы ваши требования для поиска, но вот как вы могли бы получить желаемый результат в первом примере, используя Regex.exec() и цикл времени.

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 свойство, вы можете вычесть длину соответствующей в настоящее время строки, чтобы получить начальный индекс.

Другие вопросы по тегам