Nommé des groupes de capture dans regex JavaScript?

208

Pour autant que je sache, il n'existe pas de groupes de capture nommés en JavaScript. Quelle est la méthode alternative pour obtenir des fonctionnalités similaires?

mmierins
la source
1
Les groupes de capture en javascript sont par numéro. $ 1 est le premier groupe capturé, 2 $, 3 $ ... jusqu'à 99 $ mais il semble que vous vouliez autre chose - qui n'existe pas
Erik
24
@Erik, vous parlez de groupes de capture numérotés , les PO parlent de groupes de capture nommés . Ils existent, mais nous voulons savoir s'il existe un support pour eux dans JS.
Alba Mendez
4
Il y a une proposition pour introduire des expressions rationnelles nommées dans JavaScript , mais cela pourrait prendre des années avant que nous ne le voyions, si jamais nous le faisons.
fregante
Firefox m'a puni pour avoir essayé d'utiliser des groupes de capture nommés sur un site Web ... vraiment ma faute. stackoverflow.com/a/58221254/782034
Nick Grealy

Réponses:

134

ECMAScript 2018 introduit des groupes de capture nommés dans les expressions rationnelles JavaScript.

Exemple:

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

Si vous devez prendre en charge des navigateurs plus anciens, vous pouvez tout faire avec des groupes de capture normaux (numérotés) que vous pouvez faire avec des groupes de capture nommés, il vous suffit de garder une trace des numéros - ce qui peut être lourd si l'ordre de capture du groupe dans votre changements d'expression régulière.

Les groupes de capture nommés ne présentent que deux avantages "structurels":

  1. Dans certaines versions de regex (.NET et JGSoft, pour autant que je sache), vous pouvez utiliser le même nom pour différents groupes dans votre regex ( voir ici pour un exemple où cela est important ). Mais la plupart des versions regex ne prennent pas en charge cette fonctionnalité de toute façon.

  2. Si vous devez faire référence à des groupes de capture numérotés dans une situation où ils sont entourés de chiffres, vous pouvez obtenir un problème. Disons que vous voulez ajouter un zéro à un chiffre et que vous souhaitez donc le remplacer (\d)par $10. En JavaScript, cela fonctionnera (tant que vous avez moins de 10 groupes de capture dans votre expression régulière), mais Perl pensera que vous recherchez un numéro de référence 10au lieu d'un nombre 1, suivi d'un 0. En Perl, vous pouvez utiliser ${1}0dans ce cas.

En dehors de cela, les groupes de capture nommés ne sont que du «sucre syntaxique». Il est utile d'utiliser des groupes de capture uniquement lorsque vous en avez vraiment besoin et d'utiliser des groupes non capturés.(?:...) dans toutes les autres circonstances.

Le plus gros problème (à mon avis) avec JavaScript est qu'il ne prend pas en charge les expressions rationnelles verbeuses, ce qui faciliterait la création d'expressions régulières complexes et lisibles.

La bibliothèque XRegExp de Steve Levithan résout ces problèmes.

Tim Pietzcker
la source
5
De nombreuses versions permettent d'utiliser plusieurs fois le même nom de groupe de capture dans une expression régulière. Mais seuls .NET et Perl 5.10+ rendent cela particulièrement utile en conservant la valeur capturée par le dernier groupe d'un nom ayant participé au match.
slevithan
103
L'énorme avantage est: vous pouvez simplement changer votre RegExp, pas de mappage de nombre à variable. Les groupes non capturants résolvent ce problème, sauf dans un cas: que faire si l'ordre des groupes change? De plus, c'est ennuyeux de mettre ces caractères supplémentaires sur les autres groupes ...
Alba Mendez
55
Le sucre dit syntaxique ne aide sucrer la lisibilité du code!
Mrchief
1
Je pense qu'il y a une autre raison pour laquelle les groupes de capture nommés sont vraiment précieux. Par exemple, si vous souhaitez utiliser une expression régulière pour analyser une date à partir d'une chaîne, vous pouvez écrire une fonction flexible qui prend la valeur et l'expression régulière. Tant que l'expression régulière a nommé des captures pour l'année, le mois et la date, vous pouvez parcourir un tableau d'expressions régulières avec un code minimal.
Dewey Vozel
4
Depuis octobre 2019, Firefox, IE 11 et Microsoft Edge (pré-Chromium) ne prennent pas en charge les captures de groupe nommées. La plupart des autres navigateurs (même Opera et Samsung mobile) le font. caniuse.com/…
JDB se souvient toujours de Monica
63

Vous pouvez utiliser XRegExp , une implémentation augmentée, extensible et multi-navigateur d'expressions régulières, y compris la prise en charge de syntaxe, indicateurs et méthodes supplémentaires:

  • Ajoute une nouvelle expression régulière et une syntaxe de texte de remplacement, y compris une prise en charge complète de la capture nommée .
  • Ajoute deux nouveaux drapeaux d'expression régulière: spour faire correspondre les points à tous les caractères (aka mode dotall ou singleline), et x, pour l'espacement libre et les commentaires (aka mode étendu).
  • Fournit une suite de fonctions et de méthodes qui rendent le traitement des regex complexes un jeu d'enfant.
  • Corrige automatiquement les incohérences entre les navigateurs les plus fréquemment rencontrées dans le comportement et la syntaxe des expressions régulières.
  • Vous permet de créer et d'utiliser facilement des plugins qui ajoutent une nouvelle syntaxe et des indicateurs au langage d'expression régulière de XRegExp.
Yunga Palatino
la source
60

Autre solution possible: créer un objet contenant les noms de groupe et les index.

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

Ensuite, utilisez les clés d'objet pour référencer les groupes:

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

Cela améliore la lisibilité / qualité du code en utilisant les résultats de l'expression régulière, mais pas la lisibilité de l'expression régulière elle-même.

Mr. TA
la source
58

Dans ES6, vous pouvez utiliser la déstructuration de tableau pour intercepter vos groupes:

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

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

Remarquer:

  • la première virgule de la dernière letignore la première valeur du tableau résultant, qui correspond à la chaîne entière correspondante
  • l' || []after .exec()empêchera une erreur de déstructuration quand il n'y a pas de correspondance (car .exec()reviendra null)
fregante
la source
1
La première virgule est parce que le premier élément du tableau renvoyé par match est l'expression d'entrée, non?
Emilio Grisolía
1
String.prototype.matchrenvoie un tableau avec: toute la chaîne correspondante à la position 0, puis tous les groupes après cela. La première virgule dit "sauter l'élément en position 0"
fregante
2
Ma réponse préférée ici pour ceux qui ont des cibles transpilables ou ES6 +. Cela n'empêche pas nécessairement les erreurs d'incohérence ainsi que les indices nommés pourraient, par exemple, si une expression régulière réutilisée change, mais je pense que la concision ici compense facilement cela. J'ai opté pour RegExp.prototype.execplus String.prototype.matchd'endroits où la chaîne peut être nullou undefined.
Mike Hill
22

Mise à jour: il est enfin devenu JavaScript (ECMAScript 2018)!


Les groupes de capture nommés pourraient très bientôt devenir JavaScript.
La proposition en est déjà à l'étape 3.

Un groupe de capture peut recevoir un nom entre crochets angulaires à l'aide de la (?<name>...)syntaxe, pour tout nom d'identifiant. L'expression régulière d'une date peut alors s'écrire /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u. Chaque nom doit être unique et suivre la grammaire d'ECMAScript IdentifierName .

Les groupes nommés sont accessibles à partir des propriétés d'une propriété de groupes du résultat de l'expression régulière. Des références numérotées aux groupes sont également créées, tout comme pour les groupes non nommés. Par exemple:

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
la source
C'est une proposition de l'étape 4 en ce moment.
GOTO 0
si vous utilisez '18, vous pouvez tout aussi bien vous lancer dans la déstructuration; let {year, month, day} = ((result) => ((result) ? result.groups : {}))(re.exec('2015-01-02'));
Hashbrown
6

Nommer les groupes capturés fournit une chose: moins de confusion avec les expressions régulières complexes.

Cela dépend vraiment de votre cas d'utilisation, mais peut-être qu'une jolie impression de votre expression régulière pourrait vous aider.

Ou vous pouvez essayer de définir des constantes pour faire référence à vos groupes capturés.

Les commentaires peuvent également aider à montrer aux autres qui ont lu votre code ce que vous avez fait.

Pour le reste, je dois être d'accord avec la réponse de Tims.

Yashima
la source
5

Il existe une bibliothèque node.js appelée named-regexp que vous pouvez utiliser dans vos projets node.js (dans le navigateur en empaquetant la bibliothèque avec browserify ou d'autres scripts d'empaquetage). Cependant, la bibliothèque ne peut pas être utilisée avec des expressions régulières qui contiennent des groupes de capture non nommés.

Si vous comptez les accolades de capture d'ouverture dans votre expression régulière, vous pouvez créer un mappage entre les groupes de capture nommés et les groupes de capture numérotés dans votre expression régulière et pouvez mélanger et assortir librement. Il vous suffit de supprimer les noms de groupe avant d'utiliser l'expression régulière. J'ai écrit trois fonctions qui le démontrent. Voir cet essentiel: https://gist.github.com/gbirke/2cc2370135b665eee3ef

chiborg
la source
C'est surprenant de légèreté, je vais l'essayer
fregante
Fonctionne-t-il avec des groupes nommés imbriqués dans des groupes réguliers dans des expressions régulières complexes?
ElSajko
Ce n'est pas parfait. Bug quand: getMap ("((a | b (: <foo> c)))"); foo devrait être le troisième groupe, pas le deuxième. /((a|b(c)))/g.exec("bc "); ["bc", "bc", "bc", "c"]
ElSajko
3

Comme l'a dit Tim Pietzcker , ECMAScript 2018 introduit des groupes de capture nommés dans les expressions rationnelles JavaScript. Mais ce que je n'ai pas trouvé dans les réponses ci-dessus, c'est comment utiliser le groupe capturé nommé dans l'expression régulière elle-même.

vous pouvez utiliser le groupe capturé nommé avec cette syntaxe: \k<name>. par exemple

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

et comme Forivin l'a dit, vous pouvez utiliser le groupe capturé dans le résultat de l'objet comme suit:

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
la source
2

Bien que vous ne puissiez pas le faire avec JavaScript vanilla, vous pouvez peut-être utiliser une Array.prototypefonction telle que Array.prototype.reducetransformer les correspondances indexées en correspondances nommées en utilisant de la magie .

De toute évidence, la solution suivante devra que les correspondances se produisent dans l'ordre:

// @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
la source
C'est plutôt cool. Je pense juste .. ne serait-il pas possible de créer une fonction d'expression régulière qui accepte une expression régulière personnalisée? Pour que vous puissiez y allervar assocArray = Regex("hello alex, I am dennis", "hello ({hisName}.+), I am ({yourName}.+)");
Forivin
@Forivin Vous pouvez clairement aller plus loin et développer cette fonctionnalité. Il ne serait pas difficile de le faire fonctionner: D
Matías Fidemraizer
Vous pouvez étendre l' RegExpobjet en ajoutant une fonction à son prototype.
Monsieur TA
@ Mr.TA AFAIK, il n'est pas recommandé d'étendre les objets
intégrés
0

Vous n'avez pas ECMAScript 2018?

Mon objectif était de faire en sorte que cela fonctionne le plus possible avec ce à quoi nous sommes habitués avec les groupes nommés. Alors que dans ECMAScript 2018, vous pouvez placer ?<groupname>à l'intérieur du groupe pour indiquer un groupe nommé, dans ma solution pour les anciens javascript, vous pouvez placer (?!=<groupname>)à l'intérieur du groupe pour faire la même chose. C'est donc un ensemble supplémentaire de parenthèses et un extra !=. Assez proche!

J'ai tout enveloppé dans une fonction de prototype de chaîne

Caractéristiques

  • fonctionne avec javascript plus ancien
  • pas de code supplémentaire
  • assez simple à utiliser
  • Regex fonctionne toujours
  • les groupes sont documentés dans l'expression régulière elle-même
  • les noms de groupe peuvent avoir des espaces
  • renvoie un objet avec des résultats

Instructions

  • placer (?!={groupname})à l'intérieur de chaque groupe que vous souhaitez nommer
  • n'oubliez pas d'éliminer les groupes non capturants ()en les mettant ?:au début de ce groupe. Ceux-ci ne seront pas nommés.

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

usage

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
}

résultat de o

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