Nominati gruppi di acquisizione in regex JavaScript?


208

Per quanto ne so non esiste nulla come gruppi di acquisizione con nome in JavaScript. Qual è il modo alternativo per ottenere funzionalità simili?


1
I gruppi di acquisizione in JavaScript sono suddivisi per numero .. $ 1 è il primo gruppo acquisito, $ 2, $ 3 ... fino a $ 99 ma sembra che tu voglia qualcos'altro - che non esiste
Erik

24
@Erik stai parlando di gruppi di acquisizione numerati , l'OP sta parlando di gruppi di acquisizione nominati . Esistono, ma vogliamo sapere se esiste un supporto per loro in JS.
Alba Mendez,

4
C'è una proposta per portare regex denominato in JavaScript , ma potrebbero passare anni prima che lo vediamo, se mai lo facciamo.
fregante,

Firefox mi ha punito per aver tentato di utilizzare gruppi di acquisizione denominati su un sito Web ... colpa mia. stackoverflow.com/a/58221254/782034
Nick Grealy,

Risposte:


134

ECMAScript 2018 introduce gruppi di acquisizione denominati nelle regex JavaScript.

Esempio:

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

Se hai bisogno di supportare browser più vecchi, puoi fare tutto con i normali gruppi di acquisizione (numerati) che puoi fare con i gruppi di acquisizione nominati, devi solo tenere traccia dei numeri, il che può essere ingombrante se l'ordine di acquisizione del gruppo nel tuo modifiche regex.

Ci sono solo due vantaggi "strutturali" dei gruppi di acquisizione nominati a cui posso pensare:

  1. In alcune versioni di regex (.NET e JGSoft, per quanto ne so), puoi usare lo stesso nome per diversi gruppi nella tua regex ( vedi qui un esempio in cui è importante ). Ma la maggior parte dei sapori regex non supporta questa funzionalità comunque.

  2. Se è necessario fare riferimento a gruppi di acquisizione numerati in una situazione in cui sono circondati da cifre, è possibile che si verifichi un problema. Supponiamo che tu voglia aggiungere uno zero a una cifra e quindi desideri sostituirlo (\d)con $10. In JavaScript, questo funzionerà (purché tu abbia meno di 10 gruppi di acquisizione nella tua regex), ma Perl penserà che stai cercando un numero di backreference 10invece di numero 1, seguito da a 0. In Perl, puoi usare ${1}0in questo caso.

Oltre a ciò, i gruppi di cattura nominati sono solo "zucchero sintattico". Aiuta a utilizzare i gruppi di acquisizione solo quando ne hai veramente bisogno e a utilizzare i gruppi di acquisizione(?:...) in tutte le altre circostanze.

Il problema più grande (secondo me) con JavaScript è che non supporta regex verbose che renderebbero molto più facile la creazione di espressioni regolari complesse leggibili.

La libreria XRegExp di Steve Levithan risolve questi problemi.


5
Molte versioni consentono di utilizzare lo stesso nome di gruppo di acquisizione più volte in una regex. Ma solo .NET e Perl 5.10+ lo rendono particolarmente utile mantenendo il valore acquisito dall'ultimo gruppo di un nome che ha partecipato alla partita.
slevithan,

103
L'enorme vantaggio è: puoi semplicemente modificare RegExp, nessuna mappatura da numero a variabile. I gruppi non acquisiti risolvono questo problema, tranne in un caso: cosa succede se l'ordine dei gruppi cambia? Inoltre, è ansioso mettere questi caratteri extra sugli altri gruppi ...
Alba Mendez,

55
Il cosiddetto zucchero sintattico fa aiuto addolcire la leggibilità del codice!
Mrchief,

1
Penso che ci sia un altro motivo per nominare i gruppi di acquisizione che è davvero prezioso. Ad esempio, se si desidera utilizzare un regex per analizzare una data da una stringa, è possibile scrivere una funzione flessibile che accetta il valore e il regex. Fintanto che regex ha denominato acquisizioni per l'anno, il mese e la data, è possibile eseguire una serie di espressioni regolari con un codice minimo.
Dewey Vozel,

4
A partire da ottobre 2019, Firefox, IE 11 e Microsoft Edge (pre-Chromium) non supportano le acquisizioni di gruppi con nome. La maggior parte degli altri browser (anche Opera e Samsung mobile) lo fanno. caniuse.com/…
JDB ricorda ancora Monica il

63

Puoi usare XRegExp , un'implementazione aumentata, estensibile e cross-browser di espressioni regolari, incluso il supporto per sintassi, flag e metodi aggiuntivi:

  • Aggiunge nuova regex e sintassi di sostituzione del testo, incluso il supporto completo per l' acquisizione denominata .
  • Aggiunge due nuovi flag regex s:, per far corrispondere il punto a tutti i caratteri (aka dotall o singleline mode) e x, per spaziatura libera e commenti (aka modalità estesa).
  • Fornisce una suite di funzioni e metodi che rendono un processo regex complesso un gioco da ragazzi.
  • Corregge automaticamente le incongruenze tra browser più comunemente riscontrate nel comportamento e nella sintassi di regex.
  • Consente di creare e utilizzare facilmente plug-in che aggiungono nuova sintassi e flag al linguaggio delle espressioni regolari di XRegExp.

60

Un'altra possibile soluzione: creare un oggetto contenente i nomi e gli indici dei gruppi.

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

Quindi, utilizzare le chiavi dell'oggetto per fare riferimento ai gruppi:

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

Ciò migliora la leggibilità / qualità del codice usando i risultati della regex, ma non la leggibilità della regex stessa.


58

In ES6 puoi usare l'array destructuring per catturare i tuoi gruppi:

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

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

Avviso:

  • la prima virgola nell'ultimo letsalta il primo valore dell'array risultante, che è l'intera stringa corrispondente
  • il || []dopo .exec()eviterà un errore di destrutturazione quando non ci sono corrispondenze (perché .exec()restituirà null)

1
La prima virgola è perché il primo elemento dell'array restituito dalla corrispondenza è l'espressione di input, giusto?
Emilio Grisolía,

1
String.prototype.matchrestituisce un array con: l'intera stringa corrispondente nella posizione 0, quindi qualsiasi gruppo successivo. La prima virgola dice "salta l'elemento nella posizione 0"
fregante

2
La mia risposta preferita qui per quelli con target transpiling o ES6 +. Ciò non impedisce necessariamente errori di incoerenza, così come gli indici nominati potrebbero, ad esempio, cambiare una regex riutilizzata, ma penso che la concisione qui facilmente compensi questo. Ho optato per la RegExp.prototype.execsostituzione String.prototype.matchin luoghi in cui la stringa potrebbe essere nullo undefined.
Mike Hill,

22

Aggiornamento: finalmente è diventato JavaScript (ECMAScript 2018)!


I gruppi di acquisizione denominati potrebbero trasformarlo in JavaScript molto presto.
La proposta è già nella fase 3.

A un gruppo di acquisizione può essere assegnato un nome tra parentesi angolari utilizzando la (?<name>...)sintassi, per qualsiasi nome identificativo. L'espressione regolare per una data può quindi essere scritta come /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u. Ogni nome deve essere univoco e seguire la grammatica per ECMAScript IdentifierName .

È possibile accedere ai gruppi con nome dalle proprietà di una proprietà di gruppi del risultato dell'espressione regolare. Vengono anche creati riferimenti numerati ai gruppi, proprio come per i gruppi senza nome. Per esempio:

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';

È una proposta della fase 4 in questo momento.
GOTO 0

se stai usando '18, potresti anche andare all in con destrutturazione; let {year, month, day} = ((result) => ((result) ? result.groups : {}))(re.exec('2015-01-02'));
Hashbrown,

6

La denominazione dei gruppi acquisiti fornisce una cosa: meno confusione con espressioni regolari complesse.

Dipende molto dal tuo caso d'uso, ma forse una bella stampa del tuo regex potrebbe aiutare.

Oppure potresti provare a definire le costanti da fare riferimento ai tuoi gruppi acquisiti.

I commenti potrebbero anche aiutare a mostrare agli altri che hanno letto il tuo codice, cosa hai fatto.

Per il resto devo essere d'accordo con la risposta di Tims.


5

Esiste una libreria node.js chiamata named-regexp che potresti usare nei tuoi progetti node.js (attiva nel browser comprimendo la libreria con browserify o altri script di packaging). Tuttavia, la libreria non può essere utilizzata con espressioni regolari che contengono gruppi di acquisizione senza nome.

Se conti le parentesi graffe di apertura nella tua espressione regolare puoi creare una mappatura tra i gruppi di cattura nominati e i gruppi di cattura numerati nella tua regex e puoi mescolarli e abbinarli liberamente. Devi solo rimuovere i nomi dei gruppi prima di usare regex. Ho scritto tre funzioni che lo dimostrano. Guarda questo riassunto: https://gist.github.com/gbirke/2cc2370135b665eee3ef


È sorprendentemente leggero, lo proverò
fregante

Funziona con gruppi nominati nidificati all'interno di gruppi regolari in espressioni regolari complesse?
ElSajko,

Non è perfetto Bug quando: getMap ("((a | b (: <foo> c)))"); foo dovrebbe essere il terzo gruppo, non il secondo. /((a|b(c)))/g.exec("bc "); ["bc", "bc", "bc", "c"]
ElSajko

3

Come ha affermato Tim Pietzcker, ECMAScript 2018 introduce regex JavaScript nei gruppi di acquisizione denominati. Ma ciò che non ho trovato nelle risposte sopra è stato come utilizzare il gruppo acquisito con nome nella regex stessa.

è possibile utilizzare il nome del gruppo catturate con la seguente sintassi: \k<name>. per esempio

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

e come ha detto Forivin puoi usare il gruppo acquisito nel risultato dell'oggetto come segue:

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

Anche se non puoi farlo con JavaScript vaniglia, forse puoi usare alcune Array.prototypefunzioni come Array.prototype.reducetrasformare le partite indicizzate in nominali usando un po 'di magia .

Ovviamente, la seguente soluzione richiederà che si verifichino delle corrispondenze nell'ordine:

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


È abbastanza bello. Sto solo pensando ... non sarebbe possibile creare una funzione regex che accetta una regex personalizzata? In modo da poter andare comevar assocArray = Regex("hello alex, I am dennis", "hello ({hisName}.+), I am ({yourName}.+)");
Forivin

@Forivin Chiaramente puoi andare oltre e sviluppare questa funzione. Non sarebbe difficile farlo funzionare: D
Matías Fidemraizer il

È possibile estendere l' RegExpoggetto aggiungendo una funzione al suo prototipo.
Sig. TA,

@ Mr.TA AFAIK, non è consigliabile estendere gli oggetti incorporati
Matías Fidemraizer,

0

Non hai ECMAScript 2018?

Il mio obiettivo era di farlo funzionare il più simile possibile a quello a cui siamo abituati con i gruppi denominati. Mentre in ECMAScript 2018 puoi inserire ?<groupname>all'interno del gruppo per indicare un gruppo nominato, nella mia soluzione per javascript precedente, puoi inserire (?!=<groupname>)all'interno del gruppo per fare la stessa cosa. Quindi è un set extra di parentesi e un extra !=. Molto vicino!

Ho avvolto tutto in una funzione prototipo di stringa

Caratteristiche

  • funziona con javascript precedente
  • nessun codice aggiuntivo
  • piuttosto semplice da usare
  • Regex funziona ancora
  • i gruppi sono documentati all'interno della regex stessa
  • i nomi di gruppo possono avere spazi
  • restituisce l'oggetto con risultati

Istruzioni

  • posto (?!={groupname})all'interno di ogni gruppo a cui vuoi dare un nome
  • ricordati di eliminare tutti i gruppi non catturanti ()mettendo ?:all'inizio di quel gruppo. Questi non saranno nominati.

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
}

risultato di o

{
  "house number": "123",
  "street name": "Main",
  "street type": "St"
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.