Grupos de captura nomeados em regex JavaScript?

208

Até onde eu sei, não existe o nome de grupos de captura em JavaScript. Qual é a maneira alternativa de obter funcionalidade semelhante?

mmierins
fonte
1
Grupos de captura em javascript são de número .. $ 1 é o primeiro grupo capturado, $ 2, $ 3 ... até US $ 99, mas parece que você quer outra coisa - que não existe
Erik
24
@ Erik, você está falando de grupos de captura numerados , o OP está falando de grupos de captura nomeados . Eles existem, mas queremos saber se há suporte para eles no JS.
Alba Mendez
4
Existe uma proposta para trazer regex nomeado para JavaScript , mas pode levar anos até vermos isso, se é que o fazemos.
Fregante 11/10
O Firefox me puniu por tentar usar grupos de captura nomeados em um site ... minha própria culpa. stackoverflow.com/a/58221254/782034
Nick Grealy

Respostas:

134

O ECMAScript 2018 introduz grupos de captura nomeados em expressões regulares do JavaScript.

Exemplo:

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

Se você precisar oferecer suporte a navegadores antigos, poderá fazer tudo com grupos de captura normais (numerados) que você pode fazer com grupos de captura nomeados, basta acompanhar os números - o que pode ser complicado se a ordem do grupo de captura em seu mudanças de regex.

Existem apenas duas vantagens "estruturais" dos grupos de captura nomeados em que posso pensar:

  1. Em alguns tipos de regex (.NET e JGSoft, até onde eu sei), você pode usar o mesmo nome para diferentes grupos em seu regex ( veja aqui um exemplo em que isso importa ). Mas a maioria dos tipos de expressões regulares não suporta essa funcionalidade.

  2. Se você precisar se referir a grupos de captura numerados em uma situação em que eles estão cercados por dígitos, você pode obter um problema. Digamos que você deseja adicionar um zero a um dígito e, portanto, deseja substituir (\d)por $10. Em JavaScript, isso funcionará (contanto que você tenha menos de 10 grupos de capturas em sua regex), mas Perl pensará que você está procurando por um número de referência anterior em 10vez de um número 1, seguido por a 0. No Perl, você pode usar ${1}0neste caso.

Fora isso, os grupos de captura nomeados são apenas "açúcar sintático". Ajuda a usar grupos de captura somente quando você realmente precisa deles e a usar grupos que não capturam (?:...)em todas as outras circunstâncias.

O maior problema (na minha opinião) do JavaScript é que ele não suporta expressões verbais que facilitariam muito a criação de expressões regulares complexas e legíveis.

A biblioteca XRegExp de Steve Levithan resolve esses problemas.

Tim Pietzcker
fonte
5
Muitos tipos permitem o uso do mesmo nome de grupo de captura várias vezes em uma regex. Mas somente o .NET e o Perl 5.10+ tornam isso especialmente útil, mantendo o valor capturado pelo último grupo de um nome que participou da partida.
slevithan
103
A grande vantagem é: você pode alterar seu RegExp, sem mapeamento de número para variável. Grupos que não capturam resolvem esse problema, exceto em um caso: e se a ordem dos grupos mudar? Além disso, é annonying para colocar este caracteres extras em outros grupos ...
Alba Mendez
55
O chamado açúcar sintático faz ajuda adoçar a legibilidade do código!
Mrchief 31/07
1
Eu acho que há outra razão para os grupos de captura nomeados que é realmente valioso. Por exemplo, se você deseja usar um regex para analisar uma data de uma sequência, você pode escrever uma função flexível que aceita o valor e o regex. Desde que o regex tenha nomeado capturas para o ano, mês e data, você poderá executar uma matriz de expressões regulares com código mínimo.
Dewey Vozel
4
Em outubro de 2019, o Firefox, o IE 11 e o Microsoft Edge (pré-Chromium) não suportam capturas de grupos nomeados. A maioria dos outros navegadores (até Opera e Samsung mobile) o fazem. caniuse.com/…
JDB ainda se lembra de Monica
63

Você pode usar o XRegExp , uma implementação aumentada, extensível e cruzada de expressões regulares, incluindo suporte para sintaxe, sinalizadores e métodos adicionais:

  • Adiciona nova regex e sintaxe de texto de substituição, incluindo suporte abrangente para captura nomeada .
  • Adiciona dois novos sinalizadores regex s:, para fazer o ponto corresponder a todos os caracteres (também conhecido como modo dotall ou linha única) e x, para espaçamento livre e comentários (conhecido como modo estendido).
  • Fornece um conjunto de funções e métodos que facilitam o processamento complexo de expressões regulares.
  • Corrige automaticamente as inconsistências entre navegadores mais comumente encontradas no comportamento e na sintaxe da expressão regular.
  • Permite criar e usar facilmente plug-ins que adicionam nova sintaxe e sinalizadores à linguagem de expressão regular do XRegExp.
Yunga Palatino
fonte
60

Outra solução possível: crie um objeto contendo os nomes e índices do grupo.

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

Em seguida, use as teclas de objeto para fazer referência aos grupos:

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

Isso melhora a legibilidade / qualidade do código usando os resultados da regex, mas não a legibilidade da própria regex.

Mr. TA
fonte
58

No ES6, você pode usar a destruição de matriz para capturar seus grupos:

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

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

Aviso prévio:

  • a primeira vírgula na última letpula o primeiro valor da matriz resultante, que é toda a cadeia correspondente
  • o || []after .exec()evitará um erro de desestruturação quando não houver correspondências (porque .exec()retornará null)
fregante
fonte
1
A primeira vírgula é porque o primeiro elemento do array retornado por match é a expressão de entrada, certo?
Emilio Grisolía 31/07
1
String.prototype.matchretorna uma matriz com: toda a cadeia correspondente na posição 0, depois quaisquer grupos depois disso. A primeira vírgula diz "pular o elemento na posição 0"
fregante 31/07
2
Minha resposta favorita aqui para aqueles com metas de transpiling ou ES6 +. Isso não necessariamente impede que erros de inconsistência, assim como índices nomeados, se, por exemplo, um regex reutilizado for alterado, mas acho que a concisão aqui facilmente compensa isso. Optei por RegExp.prototype.execmais String.prototype.matchem lugares onde a string pode ser nullou undefined.
31717 Mike
22

Atualização: finalmente transformou-se em JavaScript (ECMAScript 2018)!


Grupos de captura nomeados podem entrar no JavaScript muito em breve.
A proposta já está no estágio 3.

Um grupo de captura pode receber um nome entre colchetes angulares usando a (?<name>...)sintaxe, para qualquer nome de identificador. A expressão regular de uma data pode ser escrita como /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u. Cada nome deve ser exclusivo e seguir a gramática para ECMAScript IdentifierName .

Grupos nomeados podem ser acessados ​​a partir de propriedades de uma propriedade de grupos do resultado da expressão regular. Também são criadas referências numeradas para os grupos, assim como para grupos sem nome. Por exemplo:

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
fonte
É uma proposta de estágio 4 no momento.
GOTO 0
se você estiver usando o 18, pode muito bem participar da desestruturação; let {year, month, day} = ((result) => ((result) ? result.groups : {}))(re.exec('2015-01-02'));
Hashbrown 31/01
6

A nomeação de grupos capturados fornece uma coisa: menos confusão com expressões regulares complexas.

Realmente depende do seu caso de uso, mas talvez a impressão bonita do seu regex possa ajudar.

Ou você pode tentar definir constantes para se referir aos seus grupos capturados.

Os comentários também podem ajudar a mostrar aos outros que leem seu código, o que você fez.

Quanto ao resto, devo concordar com a resposta de Tims.

Yashima
fonte
5

Existe uma biblioteca node.js chamada named-regexp que você pode usar em seus projetos node.js. (ativada no navegador empacotando a biblioteca com o browserify ou outros scripts de empacotamento). No entanto, a biblioteca não pode ser usada com expressões regulares que contêm grupos de captura sem nome.

Se você contar os chavetas de captura de abertura em sua expressão regular, poderá criar um mapeamento entre os grupos de captura nomeados e os grupos de captura numerados em sua regex e poderá misturar e combinar livremente. Você apenas precisa remover os nomes dos grupos antes de usar a regex. Eu escrevi três funções que demonstram isso. Veja esta lista: https://gist.github.com/gbirke/2cc2370135b665eee3ef

Chiborg
fonte
Isso é surpreendente leve, eu vou experimentá-lo
fregante
Funciona com grupos nomeados aninhados dentro de grupos regulares em expressões regulares complexas?
Elsajko # 14/16
Não é perfeito. Erro quando: getMap ("((a | b (: <foo> c)))"); foo deve ser o terceiro grupo, não o segundo. /((a|b(c)))/g.exec("bc "); [ "bc", "bc", "bc", "c"]
ElSajko
3

Como Tim Pietzcker disse, o ECMAScript 2018 introduz grupos de captura nomeados nas expressões regulares do JavaScript. Mas o que não encontrei nas respostas acima foi como usar o grupo capturado nomeado no próprio regex.

você pode usar grupo capturado nomeado com a seguinte sintaxe: \k<name>. por exemplo

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

e, como Forivin disse, você pode usar o grupo capturado no resultado do objeto da seguinte maneira:

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>

Hamed Mahdizadeh
fonte
2

Embora você não possa fazer isso com JavaScript vanilla, talvez você possa usar alguma Array.prototypefunção como Array.prototype.reducetransformar correspondências indexadas em nomeadas usando alguma mágica .

Obviamente, a seguinte solução precisará que as correspondências ocorram em ordem:

// @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));

Matías Fidemraizer
fonte
Isso é bem legal. Eu só estou pensando .. não seria possível criar uma função regex que aceita um regex personalizado? Para que você possa ir como:var assocArray = Regex("hello alex, I am dennis", "hello ({hisName}.+), I am ({yourName}.+)");
Forivin
@ Forivin Claramente, você pode ir além e desenvolver esse recurso. Não seria difícil fazê-lo funcionar: D
Matías Fidemraizer
Você pode estender o RegExpobjeto adicionando uma função ao seu protótipo.
Sr. TA
@ Mr.TA AFAIK, não é recomendado para estender objetos internos
Matías Fidemraizer
0

Não possui o ECMAScript 2018?

Meu objetivo era fazê-lo funcionar o mais semelhante possível ao que estamos acostumados com grupos nomeados. Enquanto no ECMAScript 2018 você pode colocar ?<groupname>dentro do grupo para indicar um grupo nomeado, na minha solução para javascript antigo, você pode colocar (?!=<groupname>)dentro do grupo para fazer a mesma coisa. Portanto, é um conjunto extra de parênteses e um extra!= . Bem perto!

Eu envolvi tudo isso em uma função de protótipo de string

Recursos

  • funciona com javascript mais antigo
  • nenhum código extra
  • bastante simples de usar
  • Regex ainda funciona
  • grupos são documentados dentro do próprio regex
  • nomes de grupos podem ter espaços
  • retorna objeto com resultados

Instruções

  • coloque (?!={groupname})dentro de cada grupo que você deseja nomear
  • lembre-se de eliminar grupos que não capturam ()colocando ?:no início desse grupo. Estes não serão nomeados.

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;
  }, {});
};    

uso

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
}

resultado de o

{
  "house number": "123",
  "street name": "Main",
  "street type": "St"
}
toddmo
fonte