Именованные группы захвата в регулярном выражении JavaScript?

208

Насколько я знаю, в JavaScript нет такой вещи, как именованные группы захвата. Какой альтернативный способ получить подобную функциональность?

mmierins
источник
1
Группы захвата в javascript указаны по номеру. $ 1 - первая захваченная группа, $ 2, $ 3 ... до $ 99, но звучит так, как будто вы хотите чего-то другого - чего не существует
Эрик
24
@ Эрик, вы говорите о пронумерованных группах захвата, ОП говорит о названных группах захвата. Они существуют, но мы хотим знать, есть ли их поддержка в JS.
Альба Мендес
4
Есть предложение включить именованное регулярное выражение в JavaScript , но могут пройти годы, прежде чем мы это увидим, если когда-нибудь увидим.
Фреганте
Firefox наказал меня за то, что я пытался использовать именованные группы захвата на веб-сайте ... моя собственная вина действительно. stackoverflow.com/a/58221254/782034
Ник Грейли,

Ответы:

134

ECMAScript 2018 вводит именованные группы захвата в регулярные выражения JavaScript.

Пример:

  const auth = 'Bearer AUTHORIZATION_TOKEN'
  const { groups: { token } } = /Bearer (?<token>[^ $]*)/.exec(auth)
  console.log(token) // "Prints AUTHORIZATION_TOKEN"

Если вам требуется поддержка старых браузеров, вы можете делать все с обычными (пронумерованными) группами захвата, что вы можете делать с именованными группами захвата, вам просто нужно отслеживать номера - что может быть громоздким, если порядок захвата группы в вашем изменения в регулярных выражениях.

Есть только два «структурных» преимущества именованных групп захвата, о которых я могу думать:

  1. В некоторых вариантах регулярных выражений (насколько я знаю, .NET и JGSoft) вы можете использовать одно и то же имя для разных групп в своем регулярном выражении ( см. Пример, где это важно ). Но большинство разновидностей регулярных выражений в любом случае не поддерживают эту функцию.

  2. Если вам нужно обратиться к пронумерованным группам захвата в ситуации, когда они окружены цифрами, вы можете получить проблему. Допустим, вы хотите добавить ноль к цифре и, следовательно, хотите заменить (\d)на $10. В JavaScript это будет работать (до тех пор, пока в вашем регулярном выражении будет менее 10 групп захвата), но Perl будет думать, что вы ищете номер обратной ссылки 10вместо числа 1, за которым следует a 0. В Perl вы можете использовать ${1}0в этом случае.

Кроме того, названные группы захвата являются просто «синтаксическим сахаром». Это помогает использовать группы захвата только тогда, когда они вам действительно нужны, и использовать группы без захвата (?:...)во всех других случаях.

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

Библиотека XRegExp Стива Левитана решает эти проблемы.

Тим Питцкер
источник
5
Многие разновидности позволяют использовать одно и то же имя группы захвата несколько раз в регулярном выражении. Но только .NET и Perl 5.10+ делают это особенно полезным, сохраняя значение, захваченное последней группой имени, которое участвовало в совпадении.
slevithan
103
Огромное преимущество: вы можете просто изменить свой RegExp, не сопоставляя число с переменной. Неполные группы решают эту проблему, за исключением одного случая: что, если порядок групп изменяется? Кроме того, это раздражает, если надеть эти дополнительные символы на другие группы ...
Альба Мендес
55
Так называемый синтаксический сахар делает помощь подсластить читаемость кода!
Mrchief
1
Я думаю, что есть еще одна причина для названных групп захвата, которая действительно ценна. Например, если вы хотите использовать регулярное выражение для анализа даты в строке, вы можете написать гибкую функцию, которая принимает значение и регулярное выражение. Пока регулярное выражение называет записи для года, месяца и даты, вы можете выполнять массив регулярных выражений с минимальным кодом.
Дьюи Возел
4
По состоянию на октябрь 2019 года Firefox, IE 11 и Microsoft Edge (до Chromium) не поддерживают захват именованных групп. Большинство других браузеров (даже Opera и Samsung Mobile) делают. caniuse.com/…
JDB до сих пор помнит Монику
63

Вы можете использовать XRegExp , расширенную, расширяемую, кросс-браузерную реализацию регулярных выражений, включая поддержку дополнительных синтаксиса, флагов и методов:

  • Добавляет новый синтаксис регулярных выражений и замены, включая всестороннюю поддержку именованного захвата .
  • Добавляет два новых флага регулярных выражений:, sчтобы точка соответствовала всем символам (так называемый точечный или однострочный режим), и xдля свободного пробела и комментариев (также расширенный режим).
  • Предоставляет набор функций и методов, которые упрощают обработку сложных регулярных выражений.
  • Автоматически исправляет наиболее часто встречающиеся кросс-браузерные несоответствия в поведении и синтаксисе регулярных выражений.
  • Позволяет легко создавать и использовать плагины, которые добавляют новый синтаксис и флаги в язык регулярных выражений XRegExp.
Юнга Палатино
источник
60

Другое возможное решение: создать объект, содержащий имена групп и индексы.

var regex = new RegExp("(.*) (.*)");
var regexGroups = { FirstName: 1, LastName: 2 };

Затем используйте ключи объекта для ссылки на группы:

var m = regex.exec("John Smith");
var f = m[regexGroups.FirstName];

Это улучшает удобочитаемость / качество кода, используя результаты регулярного выражения, но не удобочитаемость самого регулярного выражения.

Мистер ТА
источник
58

В ES6 вы можете использовать деструктуризацию массива, чтобы поймать ваши группы:

let text = '27 months';
let regex = /(\d+)\s*(days?|months?|years?)/;
let [, count, unit] = regex.exec(text) || [];

// count === '27'
// unit === 'months'

Примечание:

  • первая запятая в последнем letпропускает первое значение результирующего массива, который является всей совпадающей строкой
  • || []после .exec()предотвратит ошибки деструктурирующие когда нет матчей (потому что .exec()вернется null)
fregante
источник
1
Первая запятая потому, что первый элемент массива, возвращаемый match, является входным выражением, верно?
Эмилио Грисолиа,
1
String.prototype.matchвозвращает массив с: всей совпавшей строкой в ​​позиции 0, затем любыми группами после этого. Первая запятая гласит «пропустить элемент в позиции 0»
fregante
2
Мой любимый ответ здесь для тех, у кого есть цели или ES6 +. Это не обязательно предотвращает ошибки несоответствия, так как именованные индексы могут, например, если повторно используется регулярное выражение, но я думаю, что краткость здесь легко восполняет это. Я выбрала RegExp.prototype.execза кадром String.prototype.matchв тех местах , где строка может быть nullили undefined.
Майк Хилл,
22

Обновление: оно наконец-то превратилось в JavaScript (ECMAScript 2018)!


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

Группе захвата может быть присвоено имя в угловых скобках с использованием (?<name>...)синтаксиса для любого имени идентификатора. Регулярное выражение для даты тогда может быть записано как /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u. Каждое имя должно быть уникальным и следовать грамматике для ECMAScript IdentifierName .

Именованные группы можно получить из свойств свойства groups результата регулярного выражения. Пронумерованные ссылки на группы также создаются, как и для неназванных групп. Например:

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result = re.exec('2015-01-02');
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

// result[0] === '2015-01-02';
// result[1] === '2015';
// result[2] === '01';
// result[3] === '02';
Forivin
источник
Это предложение четвертого этапа.
0
если вы используете '18, то можете пойти ва-банк с деструктуризацией; let {year, month, day} = ((result) => ((result) ? result.groups : {}))(re.exec('2015-01-02'));
Hashbrown
6

Наименование захваченных групп дает одно: меньше путаницы со сложными регулярными выражениями.

Это действительно зависит от вашего варианта использования, но, возможно, симпатичная печать вашего регулярного выражения может помочь.

Или вы можете попытаться определить константы для ссылок на ваши захваченные группы.

Комментарии могут также помочь показать другим, кто читает ваш код, что вы сделали.

В остальном я должен согласиться с ответом Тимса.

Яшима
источник
5

Существует библиотека node.js с именем named-regexp, которую вы можете использовать в своих проектах node.js (в браузере, упаковав библиотеку browserify или другими пакетными скриптами). Однако эту библиотеку нельзя использовать с регулярными выражениями, которые содержат неназванные группы захвата.

Если вы подсчитываете вводные скобки в своем регулярном выражении, вы можете создать отображение между именованными группами захвата и пронумерованными группами захвата в своем регулярном выражении и можете свободно смешивать и сопоставлять. Вам просто нужно удалить имена групп перед использованием регулярных выражений. Я написал три функции, которые демонстрируют это. Посмотреть эту суть: https://gist.github.com/gbirke/2cc2370135b665eee3ef

chiborg
источник
Это удивительно легкий, я попробую это
fregante
Работает ли он с вложенными именованными группами внутри регулярных групп в сложных регулярных выражениях?
ЭльСайко
Это не идеально. Ошибка когда: getMap ("((a | b (: <foo> c)))"); foo должна быть третьей группой, а не второй. /((a|b(c)))/g.exec("bc "); ["bc", "bc", "bc", "c"]
ЭльСайко
3

Как сказал Тим Пицкер, ECMAScript 2018 вводит именованные группы захвата в регулярные выражения JavaScript. Но что я не нашел в приведенных выше ответах, так это как использовать именованную захваченную группу в самом регулярном выражении.

Вы можете использовать захваченное с именем группы с этим синтаксисом: \k<name>. например

var regexObj = /(?<year>\d{4})-(?<day>\d{2})-(?<month>\d{2}) year is \k<year>/

и, как сказал Форивин, вы можете использовать захваченную группу в результате объекта следующим образом:

let result = regexObj.exec('2019-28-06 year is 2019');
// result.groups.year === '2019';
// result.groups.month === '06';
// result.groups.day === '28';

  var regexObj = /(?<year>\d{4})-(?<day>\d{2})-(?<month>\d{2}) year is \k<year>/mgi;

function check(){
    var inp = document.getElementById("tinput").value;
    let result = regexObj.exec(inp);
    document.getElementById("year").innerHTML = result.groups.year;
    document.getElementById("month").innerHTML = result.groups.month;
    document.getElementById("day").innerHTML = result.groups.day;
}
td, th{
  border: solid 2px #ccc;
}
<input id="tinput" type="text" value="2019-28-06 year is 2019"/>
<br/>
<br/>
<span>Pattern: "(?<year>\d{4})-(?<day>\d{2})-(?<month>\d{2}) year is \k<year>";
<br/>
<br/>
<button onclick="check()">Check!</button>
<br/>
<br/>
<table>
  <thead>
    <tr>
      <th>
        <span>Year</span>
      </th>
      <th>
        <span>Month</span>
      </th>
      <th>
        <span>Day</span>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <span id="year"></span>
      </td>
      <td>
        <span id="month"></span>
      </td>
      <td>
        <span id="day"></span>
      </td>
    </tr>
  </tbody>
</table>

Хамед Махдизаде
источник
2

Хотя вы не можете сделать это с помощью обычного JavaScript, возможно, вы можете использовать какую-то Array.prototypeфункцию, например, Array.prototype.reduceчтобы превратить индексированные совпадения в именованные, используя некоторую магию .

Очевидно, что следующему решению потребуется, чтобы совпадения происходили по порядку:

// @text Contains the text to match
// @regex A regular expression object (f.e. /.+/)
// @matchNames An array of literal strings where each item
//             is the name of each group
function namedRegexMatch(text, regex, matchNames) {
  var matches = regex.exec(text);

  return matches.reduce(function(result, match, index) {
    if (index > 0)
      // This substraction is required because we count 
      // match indexes from 1, because 0 is the entire matched string
      result[matchNames[index - 1]] = match;

    return result;
  }, {});
}

var myString = "Hello Alex, I am John";

var namedMatches = namedRegexMatch(
  myString,
  /Hello ([a-z]+), I am ([a-z]+)/i, 
  ["firstPersonName", "secondPersonName"]
);

alert(JSON.stringify(namedMatches));

Матиас Фидемрайзер
источник
Это круто. Я просто думаю ... не было бы возможно создать функцию регулярного выражения, которая принимает пользовательское регулярное выражение? Так что вы могли бы пойти какvar assocArray = Regex("hello alex, I am dennis", "hello ({hisName}.+), I am ({yourName}.+)");
Forivin
@Forivin Очевидно, что вы можете пойти дальше и развить эту функцию. Нетрудно заставить его работать: D
Matías Fidemraizer
Вы можете расширить RegExpобъект, добавив функцию к его прототипу.
г-н Т.А.
@ Mr.TA AFAIK, расширять встроенные объекты не рекомендуется
Matías Fidemraizer
0

У вас нет ECMAScript 2018?

Моя цель состояла в том, чтобы сделать его максимально похожим на то, к чему мы привыкли с именованными группами. В то время как в ECMAScript 2018 вы можете поместить ?<groupname>внутри группы, чтобы указать именованную группу, в моем решении для старого javascript вы можете поместить (?!=<groupname>)в группу, чтобы сделать то же самое. Так что это дополнительный набор скобок и дополнительный !=. Довольно близко!

Я обернул все это в функцию прототипа строки

Характеристики

  • работает со старым JavaScript
  • без дополнительного кода
  • довольно прост в использовании
  • Regex все еще работает
  • группы задокументированы в самом регулярном выражении
  • имена групп могут иметь пробелы
  • возвращает объект с результатами

инструкции

  • поместите (?!={groupname})внутри каждой группы, которую вы хотите назвать
  • не забудьте исключить любые группы, которые не захватывают (), поместив ?:в начале этой группы. Они не будут названы.

arrays.js

// @@pattern - includes injections of (?!={groupname}) for each group
// @@returns - an object with a property for each group having the group's match as the value 
String.prototype.matchWithGroups = function (pattern) {
  var matches = this.match(pattern);
  return pattern
  // get the pattern as a string
  .toString()
  // suss out the groups
  .match(/<(.+?)>/g)
  // remove the braces
  .map(function(group) {
    return group.match(/<(.+)>/)[1];
  })
  // create an object with a property for each group having the group's match as the value 
  .reduce(function(acc, curr, index, arr) {
    acc[curr] = matches[index + 1];
    return acc;
  }, {});
};    

использование

function testRegGroups() {
  var s = '123 Main St';
  var pattern = /((?!=<house number>)\d+)\s((?!=<street name>)\w+)\s((?!=<street type>)\w+)/;
  var o = s.matchWithGroups(pattern); // {'house number':"123", 'street name':"Main", 'street type':"St"}
  var j = JSON.stringify(o);
  var housenum = o['house number']; // 123
}

результат о

{
  "house number": "123",
  "street name": "Main",
  "street type": "St"
}
toddmo
источник