Nombrado grupos de captura en JavaScript regex?

208

Hasta donde yo sé, no hay grupos de captura con nombre en JavaScript. ¿Cuál es la forma alternativa de obtener una funcionalidad similar?

mmierins
fuente
1
Los grupos de captura en javascript son por número ... $ 1 es el primer grupo capturado, $ 2, $ 3 ... hasta $ 99 pero parece que quiere algo más, que no existe
Erik
24
@Erik estás hablando de grupos de captura numerados , los OP están hablando de grupos de captura nombrados . Existen, pero queremos saber si hay soporte para ellos en JS.
Alba Mendez
44
Existe una propuesta para incorporar expresiones regulares con nombre en JavaScript , pero podrían pasar años antes de que lo veamos, si alguna vez lo hacemos.
fregante
Firefox me castigó por tratar de usar grupos de captura con nombre en un sitio web ... mi propia culpa realmente. stackoverflow.com/a/58221254/782034
Nick Grealy

Respuestas:

134

ECMAScript 2018 introduce grupos de captura con nombre en expresiones regulares de JavaScript.

Ejemplo:

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

Si necesita admitir navegadores antiguos, puede hacer todo con grupos de captura normales (numerados) que puede hacer con grupos de captura con nombre, solo necesita realizar un seguimiento de los números, lo que puede ser engorroso si el orden del grupo de captura en su cambios de expresiones regulares.

Solo se me ocurren dos ventajas "estructurales" de los grupos de captura con nombre:

  1. En algunos sabores de expresiones regulares (.NET y JGSoft, hasta donde yo sé), puede usar el mismo nombre para diferentes grupos en su expresión regular ( vea aquí un ejemplo donde esto importa ). Pero la mayoría de los sabores de expresiones regulares no son compatibles con esta funcionalidad de todos modos.

  2. Si necesita referirse a grupos de captura numerados en una situación en la que están rodeados de dígitos, puede tener un problema. Digamos que desea agregar un cero a un dígito y, por lo tanto, desea reemplazar (\d)con $10. En JavaScript, esto funcionará (siempre que tenga menos de 10 grupos de captura en su expresión regular), pero Perl pensará que está buscando un número de referencia en 10lugar de un número 1, seguido de a 0. En Perl, puede usar ${1}0en este caso.

Aparte de eso, los grupos de captura nombrados son simplemente "azúcar sintáctico". Es útil usar grupos de captura solo cuando realmente los necesita y usar grupos sin captura (?:...)en todas las demás circunstancias.

El mayor problema (en mi opinión) con JavaScript es que no admite expresiones regulares detalladas, lo que facilitaría mucho la creación de expresiones regulares complejas y legibles.

La biblioteca XRegExp de Steve Levithan resuelve estos problemas.

Tim Pietzcker
fuente
55
Muchos sabores permiten usar el mismo nombre de grupo de captura varias veces en una expresión regular. Pero solo .NET y Perl 5.10+ hacen que esto sea especialmente útil al mantener el valor capturado por el último grupo de un nombre que participó en el partido.
slevithan 01 de
103
La gran ventaja es: puede cambiar su RegExp, sin mapeo de número a variable. Los grupos que no capturan resuelven este problema, excepto en un caso: ¿qué pasa si cambia el orden de los grupos? Además, es anónimo poner estos caracteres adicionales en los otros grupos ...
Alba Mendez
55
El llamado azúcar sintáctico hace ayuda endulzar la legibilidad del código!
Mrchief
1
Creo que hay otra razón para los grupos de captura con nombre que es realmente valiosa. Por ejemplo, si desea utilizar una expresión regular para analizar una fecha de una cadena, puede escribir una función flexible que tome el valor y la expresión regular. Siempre que la expresión regular haya nombrado capturas para el año, mes y fecha, puede ejecutar una serie de expresiones regulares con un código mínimo.
Dewey Vozel
44
A partir de octubre de 2019, Firefox, IE 11 y Microsoft Edge (pre-Chromium) no admiten capturas de grupo con nombre. La mayoría de los otros navegadores (incluso Opera y Samsung mobile) lo hacen. caniuse.com/…
JDB todavía recuerda a Monica el
63

Puede usar XRegExp , una implementación aumentada, extensible y de navegador cruzado de expresiones regulares, que incluye soporte para sintaxis, indicadores y métodos adicionales:

  • Agrega nueva sintaxis de expresiones regulares y texto de reemplazo, incluyendo soporte integral para la captura con nombre .
  • Agrega dos nuevos indicadores regex: spara que el punto coincida con todos los caracteres (también conocido como modo dotall o singleline) y xpara espacios libres y comentarios (también conocido como modo extendido).
  • Proporciona un conjunto de funciones y métodos que facilitan el procesamiento de expresiones regulares complejas.
  • Corrige automáticamente las inconsistencias entre navegadores más comunes en el comportamiento y la sintaxis de expresiones regulares.
  • Le permite crear y usar fácilmente complementos que agregan nuevas sintaxis y marcas al lenguaje de expresión regular de XRegExp.
Yunga Palatino
fuente
60

Otra posible solución: crear un objeto que contenga los nombres e índices del grupo.

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

Luego, use las teclas de objeto para hacer referencia a los grupos:

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

Esto mejora la legibilidad / calidad del código utilizando los resultados de la expresión regular, pero no la legibilidad de la propia expresión regular.

Sr. TA
fuente
58

En ES6 puede usar la desestructuración de matrices para capturar sus grupos:

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

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

Aviso:

  • la primera coma en la última letomite el primer valor de la matriz resultante, que es la cadena completa coincidente
  • el || []after .exec()evitará un error de desestructuración cuando no haya coincidencias (porque .exec()volverá null)
fregante
fuente
1
La primera coma es porque el primer elemento de la matriz devuelto por la coincidencia es la expresión de entrada, ¿verdad?
Emilio Grisolía
1
String.prototype.matchdevuelve una matriz con: toda la cadena coincidente en la posición 0, luego cualquier grupo después de eso. La primera coma dice "omita el elemento en la posición 0"
fregante
2
Mi respuesta favorita aquí para aquellos con objetivos transpiling o ES6 +. Esto no necesariamente evita errores de inconsistencia, así como los índices nombrados podrían, por ejemplo, si una expresión regular reutilizada cambia, pero creo que la concisión aquí lo compensa fácilmente. He optado por RegExp.prototype.execmás String.prototype.matchen lugares donde la cadena puede ser nullo undefined.
Mike Hill
22

Actualización: ¡Finalmente llegó a JavaScript (ECMAScript 2018)!


Los grupos de captura con nombre podrían ingresar a JavaScript muy pronto.
La propuesta está en la etapa 3 ya.

Un grupo de captura puede recibir un nombre dentro de corchetes angulares utilizando la (?<name>...)sintaxis, para cualquier nombre de identificador. La expresión regular para una fecha se puede escribir como /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u. Cada nombre debe ser único y seguir la gramática de ECMAScript IdentifierName .

Se puede acceder a los grupos con nombre desde las propiedades de una propiedad de grupos del resultado de la expresión regular. También se crean referencias numeradas a los grupos, al igual que para los grupos sin nombre. Por ejemplo:

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
fuente
Es una propuesta de etapa 4 en este momento.
GOTO 0
si está usando '18, bien podría ir todo con la desestructuración; let {year, month, day} = ((result) => ((result) ? result.groups : {}))(re.exec('2015-01-02'));
Hashbrown
6

Nombrar grupos capturados proporciona una cosa: menos confusión con expresiones regulares complejas.

Realmente depende de su caso de uso, pero tal vez la impresión bonita de su expresión regular podría ayudar.

O podría intentar definir constantes para referirse a sus grupos capturados.

Los comentarios también pueden ayudar a mostrar a otros que leen su código, lo que ha hecho.

Por lo demás, debo estar de acuerdo con la respuesta de Tims.

Yashima
fuente
5

Hay una biblioteca node.js llamada named-regexp que puede usar en sus proyectos node.js (en el navegador al empaquetar la biblioteca con browserify u otros scripts de empaquetado). Sin embargo, la biblioteca no se puede usar con expresiones regulares que contienen grupos de captura sin nombre.

Si cuenta las llaves de captura de apertura en su expresión regular, puede crear una asignación entre los grupos de captura con nombre y los grupos de captura numerados en su expresión regular y puede mezclar y combinar libremente. Solo tiene que eliminar los nombres de los grupos antes de usar la expresión regular. He escrito tres funciones que demuestran eso. Vea esta esencia: https://gist.github.com/gbirke/2cc2370135b665eee3ef

chiborg
fuente
Eso es sorprendentemente liviano, lo intentaré
fregante
¿Funciona con grupos con nombre anidados dentro de grupos regulares en expresiones regulares complejas?
ElSajko
No es perfecto Error cuando: getMap ("(((a | b (: <foo> c)))"); foo debería ser el tercer grupo, no el segundo. /((a|b(c)))/g.exec("bc "); ["bc", "bc", "bc", "c"]
ElSajko
3

Como dijo Tim Pietzcker , ECMAScript 2018 introduce grupos de captura con nombre en expresiones regulares de JavaScript. Pero lo que no encontré en las respuestas anteriores fue cómo usar el grupo capturado nombrado en la expresión regular misma.

puede utilizar un grupo de captura llamado con esta sintaxis: \k<name>. por ejemplo

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

y como Forivin dijo, puede usar el grupo capturado en el resultado del objeto de la siguiente manera:

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
fuente
2

Si bien no puede hacer esto con JavaScript de vainilla, tal vez pueda usar alguna Array.prototypefunción como Array.prototype.reduceconvertir las coincidencias indexadas en nombradas usando algo de magia .

Obviamente, la siguiente solución necesitará que las coincidencias ocurran en orden:

// @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
fuente
Eso es muy bonito. Solo estoy pensando ... ¿no sería posible crear una función de expresiones regulares que acepte una expresión regular personalizada? Para que puedas ir comovar assocArray = Regex("hello alex, I am dennis", "hello ({hisName}.+), I am ({yourName}.+)");
Forivin
@Forivin Claramente puedes ir más allá y desarrollar esta función. No sería difícil hacerlo funcionar: D
Matías Fidemraizer
Puede extender el RegExpobjeto agregando una función a su prototipo.
Sr. TA
@ Mr.TA AFAIK, no se recomienda extender los objetos incorporados
Matías Fidemraizer
0

¿No tienes ECMAScript 2018?

Mi objetivo era hacer que funcionara lo más similar posible a lo que estamos acostumbrados con los grupos con nombre. Mientras que en ECMAScript 2018 puede colocar ?<groupname>dentro del grupo para indicar un grupo con nombre, en mi solución para javascript anterior, puede colocar (?!=<groupname>)dentro del grupo para hacer lo mismo. Entonces es un conjunto extra de paréntesis y un extra !=. ¡Muy cerca!

Lo envolví todo en una función de prototipo de cadena

Caracteristicas

  • funciona con javascript antiguo
  • sin código extra
  • bastante simple de usar
  • Regex todavía funciona
  • los grupos están documentados dentro de la expresión regular misma
  • los nombres de grupo pueden tener espacios
  • devuelve objeto con resultados

Instrucciones

  • colocar (?!={groupname})dentro de cada grupo que quieras nombrar
  • recuerde eliminar cualquier grupo que no sea de captura ()colocando ?:al principio de ese grupo. Estos no serán nombrados.

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
fuente