Regex nella pratica: ancoraggi, quantificatori e gruppi di cattura

Pubblicato il 2026-04-13 8 min di lettura

Riepilogo (TL;DR)

Il 2 luglio 2019, una singola regex dentro il WAF di Cloudflare – un pattern quasi innocuo della forma .*(?:.*=.*) – ha tirato giù quasi il 50% del traffico globale per 27 minuti. Il backtracking di PCRE ha inchiodato un core CPU al 100% e ogni richiesta si è fermata dietro. La lezione più onesta che le regex insegnano è che nascondono il proprio costo. La maggior parte dei giorni sono piccole, veloci e seducenti; un singolo input cattivo può fermare un sistema. Ecco perché questo post è leggermente scettico nel tono. La maggior parte del lavoro regex quotidiano poggia su una manciata di pezzi: ancoraggi che legano un pattern all’inizio, alla fine o a un confine di parola; classi di caratteri che descrivono “uno di questi caratteri”; quantificatori che dicono “quanti”; e gruppi che ti permettono di catturare, referenziare o alternare. Metti a posto quelli e i problemi tipici – validare una email approssimativa, estrarre campi da una riga di log, normalizzare un numero di telefono – diventano brevi e leggibili. Sbagliali e finisci con pattern che matchano troppo, troppo poco o fermano il motore come nel caso Cloudflare. Non parsare HTML, JSON o XML con regex – i linguaggi regolari non possono descrivere annidamenti bilanciati. Anche i motori differiscono: regex ECMAScript, PCRE (Perl/PHP), re di Python, Oniguruma (Ruby e il crate onig di Rust) e RE2 di Go hanno ciascuno differenze sottili. L’incidente Cloudflare è successo solo perché il motore era PCRE; sotto RE2 sarebbe stato completato in tempo lineare.

Contesto e concetti

Un’espressione regolare è una grammatica compatta per fare match di stringhe. I pezzi a cui ricorri più spesso meritano di essere nominati esplicitamente.

Gli ancoraggi non consumano caratteri – affermano una posizione. ^ è l’inizio dell’input (o l’inizio di una riga in modalità multiline), $ è la fine, e \b è un confine di parola (la transizione tra \w e non-\w). Senza ancoraggi, abc matcha ovunque dentro una stringa più lunga. Con essi, ^abc$ matcha solo la letterale stringa intera abc, e \babc\b matcha abc come parola a sé.

Le classi di caratteri descrivono un singolo carattere da un insieme. [abc] è a, b o c; [a-z] è qualsiasi lettera minuscola; [^0-9] è “qualsiasi cosa tranne una cifra”. Le classi abbreviate coprono i casi comuni: \d (cifre), \w (caratteri di parola – lettere, cifre, underscore), \s (spazio bianco). Le negazioni \D, \W, \S sono le inverse. . matcha qualsiasi carattere tranne un newline per default; il flag s (dotall) cambia quello.

I quantificatori si attaccano al token precedente e dicono quante volte può ripetersi. * è zero o più, + è uno o più, ? è zero o uno, e {n,m} è “tra n e m” (uno dei bound può essere omesso). Per default i quantificatori sono greedy: matchano quanto più possibile e restituiscono solo se il resto del pattern fallisce. Aggiungi ? per la forma lazy*?, +?, ?? – che matcha quanto meno possibile e si estende solo se forzata. Alcuni motori aggiungono quantificatori possessivi (*+, ++) che matchano greedy ma si rifiutano di restituire in caso di fallimento, il che può prevenire il backtracking catastrofico su input patologici – esattamente lo strumento mancante nell’incidente Cloudflare.

I gruppi avvolgono sottopattern. (pattern) è un gruppo di cattura – numerato da 1 nell’ordine di apertura della parentesi – il cui match può essere referenziato con \1 dentro il pattern o con accessori indicizzati nel linguaggio host. (?<name>pattern) aggiunge una cattura con nome. (?:pattern) è un gruppo non catturante, usato puramente per alternanza ((?:cat|dog)) o raggruppamento di quantificatori senza registrare il match. | dentro un gruppo è alternanza: matcha una delle alternative.

Infine, i flag cambiano il comportamento del motore: i per case-insensitive, m per multiline (^ e $ matchano i confini di riga), s per dotall, u per Unicode. In JavaScript, il flag u abilita anche gli escape di proprietà Unicode come \p{L} per “qualsiasi lettera”.

Confronto e dati

QuantificatoreGreedy (default)Lazy (suffisso ?)Possessivo (dove supportato)
* / *? / *+Matcha il più possibile, backtrack in caso di fallimentoMatcha il meno possibile, estende in caso di fallimentoMatcha il più possibile, nessun backtrack
+ / +? / ++Uno o più, greedyUno o più, lazyUno o più, nessun backtrack
? / ?? / ?+Zero o uno, preferisce unoZero o uno, preferisce zeroZero o uno, nessun backtrack
{n,m} / {n,m}? / {n,m}+Intervallo, greedyIntervallo, lazyIntervallo, nessun backtrack

Greedy è il default giusto abbastanza spesso da essere il default. La lazy conta quando “il resto del pattern” è esso stesso permissivo – per esempio, tirare <b>...</b> da un estratto HTML con <b>(.*?)</b> invece di un greedy .* che potrebbe ingoiare più tag (e comunque, non parsare HTML vero con regex). I quantificatori possessivi e i gruppi atomici (?>...) aiutano quando un pattern altrimenti riesplorerebbe esponenzialmente molti percorsi di backtrack su match quasi riusciti. PCRE, Java e Oniguruma li supportano; ECMAScript e il re di Python storicamente no, anche se Python 3.11 ha aggiunto gruppi atomici al modulo re.

Scenari reali

Scenario 1 — Un match email pragmatico. La grammatica email completa da RFC 5322 ammette commenti, local part tra virgolette e letterali IP annidati; una regex che copre tutto questo è notoriamente mostruosa (il tentativo più famoso è lungo 6.425 caratteri) e comunque non è un vero parser. Il pattern che ho effettivamente rilasciato in produzione è quasi sempre ^[^\s@]+@[^\s@]+\.[^\s@]+$ – “non vuoto, non spazio, nessun @ perso, con almeno un punto nel dominio” – che respinge i refusi ovvi senza pretendere di validare completamente. L’unico modo per verificare davvero un indirizzo è inviargli un messaggio. Usa regex per la forma, email per l’esistenza.

Scenario 2 — Numeri di telefono con formati internazionali. +39 02 1234 5678, (02) 1234-5678 e 39-2-1234-5678 descrivono tutti lo stesso numero di Milano. Una regex come ^\+?\d{1,3}[-\s().]*\d{1,4}[-\s().]*\d{3,4}[-\s().]*\d{3,4}$ accetta la punteggiatura comune e poi un passaggio di normalizzazione rimuove la punteggiatura in una forma canonica di sole cifre. Per qualsiasi cosa seria – routing, storage, dial – usa libphonenumber di Google invece di crearlo tu. L’esito di riunione più veloce che ho visto su questo argomento è stato “non lo facciamo con regex”, che ha risparmiato al team una settimana di bug su casi limite. La regex serve per la superficie “assomiglia vagamente a un numero di telefono”.

Scenario 3 — Estrarre campi da una riga di log. Una riga come 2026-04-13T02:11:05Z 192.0.2.42 "GET /search?q=foo HTTP/1.1" 200 1534 può essere divisa con un singolo pattern: ^(?<ts>\S+)\s+(?<ip>\S+)\s+"(?<method>\w+)\s+(?<path>\S+)\s+\S+"\s+(?<status>\d+)\s+(?<bytes>\d+)$. I gruppi nominati pagano qui: l’oggetto match risultante è simile a un dizionario e ogni campo è indirizzabile per nome. Quando il formato di log cambia, il pattern è anche la documentazione di ciò che stai parsando.

Errori comuni

“Una regex può validare completamente un indirizzo email.” Può solo validare la forma. RFC 5322 è troppo complesso per essere codificato sensatamente in una regex – e anche se lo facessi, “la forma è valida” non significa “la casella esiste”. Il pattern standard industriale è una regex semplice più un’email di verifica.

“Greedy è sempre più lento di lazy.” Non intrinsecamente. I match greedy possono essere più veloci quando il sottopattern del quantificatore è molto restrittivo, perché il motore finisce in un lungo passaggio in avanti. Lazy vince quando “il resto del pattern” ancora il match, come in <b>(.*?)</b>. Fai benchmark con input realistici invece di ricorrere a ? riflessivamente.

“Tutti i motori regex sono uguali.” Non lo sono. La regex ECMAScript manca di quantificatori possessivi e gruppi atomici (il flag v nei motori moderni chiude qualche gap ma non quelli); il re di Python ha il suo proprio set di proprietà Unicode; PCRE supporta back-reference e pattern ricorsivi; Oniguruma – il motore usato da Ruby e dal crate onig di Rust – è un altro dialetto ancora. RE2 di Go rinuncia a back-reference e lookaround in cambio di tempo di esecuzione garantito lineare nell’input – lo stesso motore che Cloudflare ha valutato migrando dopo il loro incidente. Un pattern che copi da un tutorial Perl può non funzionare in JavaScript, e viceversa.

“La regex può parsare HTML (o JSON, o XML).” No, perché i linguaggi regolari non possono descrivere annidamenti bilanciati. La regex può estrarre un sottopattern specifico e ben formato da testo strutturato – un singolo valore di attributo, per esempio – ma non può parsare correttamente l’intero albero. Per formati annidati, usa un parser dedicato (DOMParser, JSON.parse, una libreria XML, un lettore CSV). La saga Stack Overflow “regex vs HTML” (la risposta del 2009) è un monito, non un dibattito.

Lista di controllo

  1. Qual è l’input e quali sono i controesempi? Scrivi entrambi prima di scrivere il pattern.
  2. I dati sono annidati o ricorsivi? Se sì, usa un parser. La regex è lo strumento sbagliato.
  3. Quale motore stai targettando? JavaScript, Python, Go, PCRE differiscono su lookaround, back-reference, Unicode.
  4. Ti serve il match stesso o solo un sì/no? Preferisci (?:...) non catturante per gruppi che esistono solo per alternanza o quantificazione.
  5. Il pattern è fornito dall’utente o applicato a input non fidato? Proteggi dal backtracking catastrofico con un time limit, gruppi atomici o un motore a tempo lineare come RE2. Altrimenti diventi Cloudflare.
  6. Stai facendo normalizzazione testuale dopo? Non codificare tutto in un enorme pattern; abbina un semplice controllo di forma a un piccolo passaggio di post-processing.
  7. La regex è documentata? La modalità multi-riga con x (extended) o un commento sopra il pattern è un’assicurazione a basso costo per la prossima persona che la leggerà.

Strumento correlato

Il regex tester di Patrache Studio esegue pattern contro input di esempio nel browser e mostra le catture di gruppo inline, cosa più veloce che spostarsi tra schede. Quando le stringhe che stai matchando sono esse stesse strutturate – righe di log con payload JSON, risposte API – combina il lavoro regex con Formattazione, validazione e schema di JSON nella pratica così che il pezzo estratto sia validato da un parser appropriato invece che da una seconda regex. Un target comune per regex è un UUID embedded in un URL; UUID v1 vs v4 vs v7: scegliere una chiave primaria DB copre perché gli stessi 36 caratteri possono significare cose diverse a seconda dei bit di versione.

Riferimenti