Una parte del pattern può essere racchiusa tra parentesi (...), diventando così un âgruppo di acquisizioneâ (capturing group).
Ciò comporta due conseguenze:
- Possiamo acquisire una parte della corrispondenza come elemento separato allâinterno di un array di risultati.
- Se poniamo un quantificatore dopo le parentesi, questo si applica allâintero gruppo di acquisizione.
Esempi
Vediamo come operano le parentesi attraverso degli esempi.
Esempio: gogogo
Senza parentesi, il pattern go+ significa: il carattere g seguito da o ripetuto una o più volte. Per esempio goooo o gooooooooo.
Le parentesi raggruppano i caratteri, pertanto (go)+ significa go, gogo, gogogo e così via.
alert( 'Gogogo now!'.match(/(go)+/ig) ); // "Gogogo"
Esempio: dominio
Facciamo un esempio un poâ più complesso, unâespressione regolare per cercare il dominio di un sito.
Per esempio:
mail.com
users.mail.com
smith.users.mail.com
Come possiamo vedere, un dominio consiste in parole ripetute, un punto segue ciascuna parola tranne lâultima.
Tradotto in unâespressione regolare diventa (\w+\.)+\w+:
let regexp = /(\w+\.)+\w+/g;
alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com
La ricerca funziona, ma il pattern non trova riscontro con domini contenenti un trattino, es. my-site.com, perché il trattino non appartiene alla classe \w.
Possiamo correggere il tiro rimpiazzando \w con [\w-] in ogni parola eccetto lâultima: ([\w-]+\.)+\w+.
Esempio: email
Il precedente esempio può essere esteso. A partire da questo possiamo creare unâespressione regolare per le email.
Il formato delle email è: name@domain. Qualsiasi parola può essere ânameâ, sono consentiti trattini e punti. Lâespressione regolare diventa [-.\w]+.
Ecco il pattern:
let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g;
alert("[email protected] @ [email protected]".match(regexp)); // [email protected], [email protected]
Questa regexp non è perfetta, ma per lo più funziona e aiuta a correggere errori di battitura accidentali. Lâunica verifica davvero efficace per unâemail può essere fatta soltanto inviandone una.
I contenuti tra parentesi nella corrispondenza
I gruppi tra parentesi sono numerati da sinistra verso destra. Il motore di ricerca memorizza il contenuto associato a ciascuno di essi e consente di recuperarlo nel risultato.
Il metodo str.match(regexp), se regexp non ha il flag g, cerca la prima corrispondenza e la restituisce in un array:
- Nellâindice
0: lâintera corrispondenza. - Nellâindice
1: il contenuto del primo gruppo tra parentesi. - Nellâindice
2: il contenuto del secondo. - â¦e così viaâ¦
Ad esempio se volessimo trovare i tag HTML <.*?> per elaborarli, sarebbe conveniente averne il contenuto (ciò che è allâinterno delle parentesi uncinate) in una variabile separata.
Racchiudiamo il contenuto tra parentesi, in questo modo: <(.*?)>.
Adesso otterremo sia lâintero tag <h1> sia il suo contenuto h1 nellâarray di risultati:
let str = '<h1>Hello, world!</h1>';
let tag = str.match(/<(.*?)>/);
alert( tag[0] ); // <h1>
alert( tag[1] ); // h1
Gruppi annidati
Le parentesi possono essere annidate. Anche in questo caso la numerazione procede da sinistra verso destra.
Per esempio durante la ricerca del tag in <span class="my"> potrebbe interessarci:
- Lâintero contenuto del tag:
span class="my". - Il nome del tag:
span. - Gli attributi del tag:
class="my".
Aggiungiamo le parentesi a questo scopo: <(([a-z]+)\s*([^>]*))>.
Ecco come sono numerate (da sinistra verso destra, a partire dalla parentesi di apertura):
In azione:
let str = '<span class="my">';
let regexp = /<(([a-z]+)\s*([^>]*))>/;
let result = str.match(regexp);
alert(result[0]); // <span class="my">
alert(result[1]); // span class="my"
alert(result[2]); // span
alert(result[3]); // class="my"
Lâindice zero di result contiene sempre lâintera corrispondenza.
Seguono i gruppi, numerati da sinistra verso destra, a partire dalla parentesi di apertura. Il primo gruppo è result[1], esso racchiude lâintero contenuto del tag.
Troviamo il gruppo della seconda parentesi ([a-z]+) in result[2] ed a seguire il nome del tag ([^>]*) in result[3].
Ed ecco la rappresentazione del contenuto di ciascun gruppo nella stringa:
Gruppi opzionali
Anche se un gruppo è opzionale e non ha alcun riscontro (ad esempio ha il quantificatore (...)?), lâelemento corrispondente è comunque presente nellâarray result ed equivale a undefined.
Consideriamo per esempio la regexp a(z)?(c)? che cerca la "a" facoltativamente seguita da "z" e da "c".
Se la eseguiamo sulla stringa con la singola lettera a, questo è il risultato:
let match = 'a'.match(/a(z)?(c)?/);
alert( match.length ); // 3
alert( match[0] ); // a (l'intera corrispondenza)
alert( match[1] ); // undefined
alert( match[2] ); // undefined
Lâarray è costituito da 3 elementi, ma tutti i gruppi sono vuoti.
Ed ora ecco un riscontro più articolato per la stringa ac:
let match = 'ac'.match(/a(z)?(c)?/)
alert( match.length ); // 3
alert( match[0] ); // ac (l'intera corrispondenza)
alert( match[1] ); // undefined, perché non c'è riscontro per (z)?
alert( match[2] ); // c
La lunghezza dellâarray resta in ogni caso: 3, ma non câè riscontro per il gruppo (z)?, quindi il risultato è ["ac", undefined, "c"].
Ricerca di tutte le corrispondenze con gruppi: matchAll
matchAll è un nuovo metodo, potrebbe essere necessario un polyfillIl metodo matchAll non è supportato nei browser più datati.
Potrebbe essere richiesto un polyfill come https://github.com/ljharb/String.prototype.matchAll.
Quando cerchiamo tutte le corrispondenze (flag g), il metodo match non restituisce il contenuto dei gruppi.
Cerchiamo ad esempio tutti i tag in una stringa:
let str = '<h1> <h2>';
let tags = str.match(/<(.*?)>/g);
alert( tags ); // <h1>,<h2>
Il risultato è un array di riscontri, ma senza i dettagli di ciascuno di essi. Nella pratica comune, tuttavia, nel risultato ci occorre il contenuto dei gruppi di acquisizione.
Per ottenerlo, dovremmo utilizzare la ricerca con il metodo str.matchAll(regexp).
à stato aggiunto al linguaggio JavaScript molto tempo dopo match, come sua âversione nuova e migliorataâ.
Proprio come match cerca le corrispondenze, ma ci sono 3 differenze:
- Non restituisce un array, ma un oggetto iterabile.
- Quando è presente il flag
g, restituisce ogni riscontro come un array i cui elementi corrispondono ai gruppi. - Se non câè alcun riscontro, non restituisce
null, bensì un oggetto iterabile vuoto.
Per esempio:
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
// results, non è un array ma un oggetto iterabile
alert(results); // [object RegExp String Iterator]
alert(results[0]); // undefined (*)
results = Array.from(results); // convertiamolo in un array
alert(results[0]); // <h1>,h1 (primo tag)
alert(results[1]); // <h2>,h2 (secondo tag)
Come possiamo notare la prima differenza è davvero rilevante, lo dimostra la linea (*). Non possiamo ricavare la corrispondenza come results[0] perché quellâoggetto non è uno pseudo array. Possiamo convertirlo in un Array a tutti gli effetti tramite Array.from. Trovate ulteriori dettagli sugli pseudo array e sugli iterabili nellâarticolo Iteratori.
Non occorre la conversione con Array.from se adoperiamo un ciclo iterativo sui risultati:
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
for(let result of results) {
alert(result);
// primo alert: <h1>,h1
// secondo: <h2>,h2
}
â¦Oppure se ci avvaliamo della sintassi destrutturata:
let [tag1, tag2] = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
Ogni elemento dellâoggetto di risultati restituito da matchAll ha lo stesso formato del risultato di match senza il flag g: si tratta di un array con le proprietà aggiuntive index (la posizione del riscontro nella stringa) e input (la stringa sorgente):
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
let [tag1, tag2] = results;
alert( tag1[0] ); // <h1>
alert( tag1[1] ); // h1
alert( tag1.index ); // 0
alert( tag1.input ); // <h1> <h2>
matchAll è un oggetto iterabile e non un array?Perché questo metodo è progettato in questo modo? La ragione è semplice, per lâottimizzazione.
La chiamata a matchAll non esegue la ricerca. Al contrario, restituisce un oggetto iterabile inizialmente privo di risultati. La ricerca è eseguita ogni volta che richiediamo un elemento, ad esempio allâinterno di un ciclo iterativo.
Verranno pertanto trovati tutti i risultati necessari, non di più.
Considerate che potrebbero esserci anche 100 riscontri nel testo, ma potremmo decidere che sono sufficienti le prime cinque iterazioni di un ciclo for..of e interrompere con break. Lâinterprete a quel punto non sprecherà tempo a recuperare gli altri 95 risultati.
I gruppi nominati
Ricordare i gruppi con i rispettivi numeri è difficoltoso. à fattibile per i pattern semplici, ma per quelli più complessi contare le parentesi è scomodo. Abbiamo a disposizione unâopzione decisamente migliore: dare un nome alle parentesi.
Per farlo inseriamo ?<name> subito dopo la parentesi di apertura.
Cerchiamo una data, ad esempio, nel formato âyear-month-dayâ:
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
let str = "2019-04-30";
let groups = str.match(dateRegexp).groups;
alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30
Come potete osservare, troviamo i gruppi dentro la proprietà .groups.
Per cercare tutte le date, possiamo aggiungere il flag g.
Abbiamo inoltre bisogno di matchAll per ottenere sia le corrispondenze sia il dettaglio dei gruppi:
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30 2020-01-01";
let results = str.matchAll(dateRegexp);
for(let result of results) {
let {year, month, day} = result.groups;
alert(`${day}.${month}.${year}`);
// primo alert: 30.10.2019
// secondo: 01.01.2020
}
Sostituire testo con i gruppi di acquisizione
Il metodo str.replace(regexp, replacement), che sostituisce tutti i riscontri con regexp in str, consente di usare il contenuto tra parentesi nella stringa replacement. Per farlo si usa $n, dove n indica il numero del gruppo.
Ad esempio,
let str = "John Bull";
let regexp = /(\w+) (\w+)/;
alert( str.replace(regexp, '$2, $1') ); // Bull, John
Per i gruppi nominati il riferimento sarà $<name>.
Rimoduliamo, ad esempio, le date da âyear-month-dayâ a âday.month.yearâ:
let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30, 2020-01-01";
alert( str.replace(regexp, '$<day>.$<month>.$<year>') );
// 30.10.2019, 01.01.2020
I gruppi non acquisiti e lâuso di ?:
Talvolta abbiamo bisogno delle parentesi per applicare correttamente un quantificatore, ma non vogliamo il loro contenuto nel risultato.
Un gruppo può essere escluso aggiungendo ?: dopo la parentesi di apertura.
Se desideriamo, ad esempio, cercare (go)+, ma non vogliamo il contenuto tra le parentesi (go) in un elemento dellâarray, scriveremo: (?:go)+.
Nellâesempio qui di seguito otterremo solo il nome John come elemento distinto nel risultato:
let str = "Gogogo John!";
// ?: esclude 'go' dall'acquisizione
let regexp = /(?:go)+ (\w+)/i;
let result = str.match(regexp);
alert( result[0] ); // Gogogo John (l'intera corrispondenza)
alert( result[1] ); // John
alert( result.length ); // 2 (non ci sono ulteriori elementi nell'array)
Riepilogo
Le parentesi raggruppano insieme una parte dellâespressione regolare, in modo che il quantificatore si applichi al gruppo nel suo insieme.
I gruppi tra parentesi sono numerati da sinistra verso destra, e, facoltativamente, si può attribuire loro un nome (?<name>...).
Il contenuto di un gruppo può essere ottenuto nei risultati:
- Il metodo
str.matchrestituisce i gruppi di acquisizione solo se non è presente il flagg. - Il metodo
str.matchAllrestituisce in ogni caso i gruppi di acquisizione.
Se le parentesi non hanno alcun nome, il loro contenuto è disponibile nellâarray dei risultati col rispettivo numero. I gruppi nominati sono disponibili anche nella proprietà groups.
Possiamo usare, inoltre, il contenuto tra parentesi nella sostituzione di stringhe con str.replace: in base al numero $n o in base al nome $<name>.
Un gruppo può essere escluso dalla numerazione aggiungendo ?: dopo la parentesi di apertura. Di solito si fa se abbiamo bisogno di applicare un quantificatore ad un intero gruppo, ma non vogliamo che quel gruppo compaia come elemento distinto nellâarray dei risultati. In quel caso non possiamo nemmeno usare un riferimento a tali gruppi nella sostituzione di stringhe.
Commenti
<code>, per molte righe â includile nel tag<pre>, per più di 10 righe â utilizza una sandbox (plnkr, jsbin, codepenâ¦)