Espressioni regolari: il modo semplice di descrivere stringhe di caratteri
La ricerca di documenti secondo specifici caratteri o stringhe di caratteri concrete in campo informatico figura da sempre nelle attività standard che vanno sempre sbrigate. Spesso l'obiettivo consiste nel modificare o sostituire le sezioni di testo o le righe di codice cercate, un compito che si rivela tanto più complesso quante sono le volte in cui le specifiche stringhe di caratteri appaiono nel documento. La scoperta di una soluzione che influenza tutt'oggi lo sviluppo di software e che semplifica considerevolmente tali attività ripetitive sotto forma delle cosiddette espressioni regolari (ingl. regular expressions) risale già agli anni '50.
Che cos'è un'espressione regolare?
Le espressioni regolari (ingl. regular expressions) sono unità descrittive di linguaggi regolari che appartengono ai cosiddetti linguaggi informatici. Sono uno strumento centrale dell'informatica teorica che, tra l'altro, costituisce la base per lo sviluppo e l'esecuzione di programmi informatici e per la costruzione dei compilatori necessari a tale scopo. Ecco perché le espressioni regolari, spesso definite anche regex e basate su regole sintattiche ben definite, vengono applicate in particolare nell'attività di sviluppo di software.
Per ogni espressione regolare esiste un automa a stati finiti (noto anche come macchina a stati finiti) che accetta il linguaggio specificato dall'espressione e che può essere sviluppato da un'espressione regolare ricorrendo all'algoritmo di Thompson. D’altro canto per ogni automa a stati finiti esiste anche un'espressione regolare in grado di descrivere il linguaggio accettato dall'automa e che può essere generata a scelta attraverso l'Algoritmo di Kleene o l'eliminazione di Gauss.
Un automa è un modello di comportamento che si compone di stati, passaggi di stato e azioni. Viene definito a stati finiti, se la quantità di stati che può accettare è finita (ossia limitata).
Un esempio noto dell'uso di espressioni regolari in ambito informatico è costituito dalla funzione Trova/Sostituisci degli editor di testo, implementata per la prima volta negli anni '60 da Ken Thompson, pioniere informatico e sviluppatore dei sistemi operativi UNIX, nell'editor di testo orientato alla linea QED e nel suo successore ed. Questa funzione consente di cercare determinate stringhe di caratteri nei testi e, se lo si desidera, di sostituirle mediante un'altra stringa qualsiasi,
Espressioni regolari (ingl. regular expressions): le espressioni regolari sono stringhe di caratteri basate su regole sintattiche che consentono di descrivere le stringhe di caratteri. Come tali, esse sono parte integrante dei linguaggi regolari, un sottogruppo dei linguaggi formali che svolge un ruolo di grande importanza, in particolare in campo informatico e soprattutto nello sviluppo di software.
Come funziona un'espressione regolare?
È possibile costruire un'espressione regolare esclusivamente mediante caratteri normali (ad es. abc) oppure tramite una combinazione di caratteri normali e metacaratteri (ad es. ab*c). In questo contesto, i metacaratteri hanno il compito di descrivere determinate costruzioni o ordini di caratteri, anche se un carattere deve trovarsi ad esempio all'inizio della riga o se un carattere può o deve comparire esattamente una volta, più volte o con minore frequenza. Di seguito riportiamo uno dei due esempi di espressioni regolari precedenti:
abc: questo semplice modello di regex abc richiede una concordanza esatta. Vengono quindi ricercate stringhe di caratteri, in cui non solo sono contenuti tutti i caratteri "abc", ma nelle quali essi compaiono anche nella sequenza esatta, come nella frase: "Conosci l'abc?".
ab*c: al contrario, le espressioni regolari con caratteri speciali funzionano in modo leggermente diverso, poiché non solo vengono ricercate concordanze esatte, ma anche scenari speciali. In questo caso, il simbolo asterisco provvede affinché vengano ricercate stringhe di caratteri che iniziano con la lettera "a" e che terminano con la lettera "c". Tra queste due lettere può essere presente un numero qualsiasi di lettere "b"; in questo modo si trova una concordanza in "abc", ma anche in "abbbbc" e "cbbabbcba".
Inoltre, ciascuna regex può essere associata a un'azione concreta quale, ad esempio, la funzione "Sostituisci" citata in precedenza. Questa azione viene sempre eseguita dove ci si attiene alla rispettiva espressione regolare, ossia dove si ha una concordanza corrispondente come negli esempi descritti.
Quali sfide sono associate all'impiego di espressioni regolari?
Chi desidera lavorare con le istruzioni di regex ha diverse libertà, poiché per ogni impostazione del problema che si desidera risolvere con un'espressione regolare sono sempre presenti più soluzioni. Tuttavia, il fatto che sia possibile ottenere il risultato desiderato in diversi modi non sempre costituisce un vantaggio:
ad esempio, le istruzioni possono essere molto generiche per raggiungere l'obiettivo desiderato in qualsiasi caso. Tuttavia, se desiderate ottenere un risultato il più possibile accurato, non potete evitare di formulare un modello di regex specifico. Inoltre, è consigliabile considerare la lunghezza in generale: più compatta è un'espressione regolare, minore sarà la sua durata di elaborazione. Tuttavia, in questo caso non dovete perdere di vista la leggibilità. Infatti, la possibile modifica futura delle espressioni regolari utilizzate costituirà un ostacolo considerevole se le istruzioni originarie sono troppo complicate e non commentate.
In generale, nella creazione delle espressioni regolari occorre determinare il rapporto ottimale tra compattezza e specificità.
Quali regole sintattiche si applicano alle espressioni regolari?
Come già menzionato, è possibile utilizzare le espressioni regolari in diversi linguaggi quali, ad esempio, Perl, Python, Ruby, JavaScript, XML o HTML, mentre l'utilizzo o la funzione possono essere molto diversi. Ad esempio, nel linguaggio JavaScript vengono impiegati i motivi di regex con i metodi String search(), match() o replace(), mentre le espressioni nei documenti XML servono a limitare i contenuti di elementi. Tuttavia, per quanto riguarda la sintassi non vi sono differenze tra i singoli linguaggi di programmazione e di mark up nel caso delle espressioni regolari.
Un'espressione regolare può quindi comporsi di due o tre parti, indipendentemente dal linguaggio in cui essa viene utilizzata:
Pattern (motivo di ricerca) | L'elemento centrale è il pattern, ossia il motivo di ricerca generale. Come illustrato nella sezione precedente, esso può comporsi esclusivamente di caratteri semplici o di una combinazione di caratteri semplici e di caratteri speciali. |
---|---|
Delimiter (Delimitatore) | I delimiter definiscono l'inizio e la fine del pattern. In linea di massima, vengono presi in considerazione tutti i caratteri non alfanumerici (ad eccezione della barra rovesciata). Ad esempio, PHP prevede come delimitatori hashtag (#pattern#), simboli di percentuale (%pattern%), di addizione (+pattern+) o tilde (~pattern~). Tuttavia, nella maggior parte dei linguaggi vengono attualmente impiegati virgolette ("pattern") o barre (/pattern/). |
Modifier (Modificatore) | I modifier possono essere aggiunti a un motivo di ricerca per modificare l'espressione regolare. Ad esempio, il modificatore i ha la funzione di rimuove la distinzione tra lettere maiuscole e minuscole. Questo garantirà l'importanza delle lettere maiuscole e minuscole e la loro validità per tutte le espressioni regolari. |
Tra i tipici caratteri speciali sintattici, che possono ampliare i pattern con determinate opzioni, troviamo i seguenti:
Caratteri speciali di regex sintattiche | Funzione |
---|---|
[] | Un paio di parentesi quadre indica una classe di caratteri che si trova sempre in un motivo di ricerca per un singolo carattere. |
() | Un paio di parentesi tonde indica una classe di caratteri composta da uno o più caratteri, che possono essere connessi tra loro. |
- | Funge da indicazione del range (da [… ] a […]), se si trova tra due caratteri normali |
^ | Limita la ricerca all'inizio di una riga (ulteriore funzione: simbolo di sottrazione in classi di caratteri) |
$ | Limita la ricerca alla fine di una riga (ulteriore funzione |
. | Viene impiegato per qualsiasi carattere |
* | Il numero di caratteri, della classe o del gruppo che compare davanti a un asterisco può essere un numero qualsiasi (zero incluso). |
+ | Il carattere, la classe o il gruppo che compare davanti a un segno di addizione deve essere presente almeno una volta. |
? | Il carattere, la classe o il gruppo che compare davanti a un punto interrogativo è opzionale e può comparire al massimo una volta. |
{n} | Il carattere, la classe o il gruppo precedente compare esattamente n volte. |
{n,m} | Il carattere, la classe o il gruppo precedente compare esattamente almeno n volte e al massimo m volte. |
{n,} | Il carattere, la classe o il gruppo precedente compare come minimo n volte o con maggiore frequenza. |
\b | Indica di tener conto del limite di parole durante la ricerca. |
\B | Indica di ignorare il limite di parole durante la ricerca. |
\d | Qualsiasi cifra; abbreviazione per la classe di caratteri [0-9] |
\D | Qualsiasi non cifra; abbreviazione per la classe di caratteri [^0-9] |
\w | Qualsiasi carattere alfanumerico; abbreviazione per la classe di caratteri [a-zA-Z_0-9] |
\W | Qualsiasi carattere non alfanumerico; abbreviazione per la classe di caratteri [^\w] |
Tutorial: spiegazione delle possibili espressioni regolari ricorrendo ad esempi
Mentre nelle sezioni precedenti di questo articolo sono stati riassunti i principi delle regex, il prossimo tutorial si ripropone di illustrare il funzionamento delle stringhe di caratteri pratiche. In esso vengono illustrate le diverse possibilità e i trucchi sintattici con esempi concreti di espressioni regolari, nonché di espressioni semplici e complesse.
Espressioni regolari composte da un elemento
La forma più semplice di regex è costituita da un motivo di ricerca che prevede come risultato un unico elemento. Purché non ricerchiate un elemento concreto, è possibile definire senza problemi una simile espressione regolare composta da un elemento servendosi di una classe di caratteri. La seguente espressione accetta, a scelta, le cifre "1", "2", "3", "4", "5", "6" o "7" come possibile risultato:
[1234567]
Dato che in questo caso i numeri si susseguono direttamente, sarebbe possibile anche la seguente forma semplificata:
[1-7]
Al contrario, se occorre modificare l'espressione regolare, rimuovendo la cifra "4" dalla ricerca, potete utilizzare anche la variante semplificata con il segno di sottrazione:
[1-35-7]
I singoli caratteri di un pattern di regex non sono separati da spazi vuoti.
Espressioni regolari composte da più elementi
Potete lavorare con classi di caratteri anche nel caso di un'espressione regolare composta da più elementi per rendere possibile la selezione di diversi risultati. Ad esempio, se l'espressione rileva due elementi per i quali sono concepibili diversi risultati, dovrete semplicemente allineare due classi di caratteri una dopo l'altra:
[1-7][a-c]
Al primo elemento, un numero compreso tra "1" e "7", seguirà anche una delle lettere "a", una "b" o una "c". Come già menzionato, in questo caso è obbligatorio ricorrere alle lettere minuscole. Tuttavia, prima di trattare i modificatori, potete già integrare lettere maiuscole apportando una piccola modifica, come illustrato di seguito:
[1-7][a-cA-C]
Espressioni regolari con elementi opzionali
Indipendentemente dal fatto che siate alla ricerca di più elementi all'interno di una singola espressione regolare o che effettuiate la ricerca servendovi di più gruppi di caratteri, è possibile che determinati elementi debbano o possano essere contenuti solo in determinate condizioni. Questo potrebbe accadere, ad esempio, con un'espressione regolare che deve filtrare tutti i numeri civici. In questo caso, i numeri civici costituiti da un'unica cifra si confrontano talvolta con numeri di due o persino di tre cifre. Inoltre, vi sono indirizzi nei quali una lettera viene applicata al numero civico come elemento aggiuntivo. È possibile includere questa forma aggregata in combinazioni possibili mediante le seguenti istruzioni di regex:
[1-9][0-9]?[0-9]?[a-z]?
L'unico elemento obbligatorio di questo motivo di ricerca è un numero compreso tra "1" e "9". In opzione, possono seguire due cifre comprese tra "0" e "9" e una lettera. I caratteri opzionali vengono segnalati dal punto interrogativo che li segue nel codice.
Mentre la costruzione per i numeri a tre cifre più lettera aggiuntiva è chiara, questa sarebbe considerevolmente diversa con numeri fino a dieci cifre. In questo caso è consigliabile impiegare le parentesi graffe, come nel seguente esempio di espressioni regolari:
[1-9][0-9]{0,9}
Come illustrato nell'esempio precedente, è innanzitutto richiesto un numero tra "1" e "9". Tuttavia, si può decidere se non far seguire alcuna cifra o se far seguire cifre fino a nove, ossia comprese tra "0" e "9", affinché il risultato della ricerca possa essere costituito da un massimo di dieci cifre.
Espressioni regolari con ripetizioni con una frequenza a piacere
Negli esempi finora addotti di espressioni composte da un elemento o e da più elementi era noto sia il numero minimo che il numero massimo di caratteri. Tuttavia, esistono scenari in cui la quantità di caratteri di una regex non deve essere definita esattamente in anticipo. I parametri necessari sono quindi l'asterisco e il segno di addizione, che consentono il ricorso a qualsiasi ripetizione di un carattere o di una classe o gruppo di caratteri. Ad esempio, ciò consente di realizzare tutte le stringhe di caratteri con un numero qualsiasi di cifre (anche lo "zero") con la seguente espressione regolare:
[0-9]*
La stessa regola si applica anche quando viene ricercata una combinazione di caratteri concreta, nella quale possono comparire uno (o più) caratteri tutte le volte che lo si desidera, come nell'esempio seguente:
ab*
In questo caso, tra i possibili risultati rientrano sia la parola "avere" che "abete" e "abbasstanza". Tuttavia, se si dovesse escludere il primo risultato o se il carattere specificato dovesse comparire almeno una volta, si dovrà utilizzare il segno di addizione:
ab+
Negare classi di caratteri
Se volete impiegare espressioni regolari con classi di caratteri che di norma rappresentano uno o più caratteri qualsiasi, ma che in questo caso escludono come risultato uno o più caratteri specifici, avrete bisogno del carattere di negazione "^". Questo carattere si trova sempre all'interno delle parentesi di una classe di caratteri ed è valido solo all'interno di queste parantesi. La seguente istruzione offre un ottimo esempio di una classe di caratteri negata:
C[^a]sa
Per quanto riguarda il secondo carattere, esso può essere costituito da un carattere qualsiasi eccetto "a", per cui la parola "cosa" offre le concordanze richieste. Al contrario, la parola "casa" non offre tali condizioni perché non si attiene neanche all'espressione regolare.
Caratteri jolly
Le espressioni regolari consentono di lavorare anche con caratteri jolly, che a scelta rappresentano uno, più o nessun carattere (a seconda del metacarattere impiegato) all'interno di un motivo di ricerca. Il carattere jolly viene generato mediante un punto, che combinerete con i caratteri speciali elencati precedentemente per le ripetizioni, se desiderate risultati diversi da caratteri singoli. Simili espressioni regolari consentono, ad esempio, di ricercare all'interno di una banca dati una persona della quale conoscete il nome e cognome, ma di cui non sapete se sia registrata con un secondo nome:
Paolo .*Rossi
In questo caso, tra i possibili risultati rientrano sia "Paolo Luca Rossi" (e qualsiasi altra combinazione con doppio nome) o "Paolo L. Rossi" nonché "Paolo Rossi". Se si devono considerare esclusivamente le varianti con un secondo nome, invece dell'asterisco dovrete utilizzare un segno di addizione:
Paolo .+Rossi
Un ottimo esempio dell'impiego efficace di un carattere jolly per un carattere singolo è il seguente motivo di ricerca, concordante sia con "Dire" che con "Dare“:
D.re
Alternative
Potete formulare espressioni regolari, affinché vi siano due o più alternative per una concordanza. In questo caso le alternative vengono separate da un trattino verticale, come nell'esempio seguente:
Fiore|Flora
Pertanto, sia "Fiore" che "Flora" offrono una concordanza.
È possibile formulare alternative anche all'interno di parole o di stringhe di caratteri facendo ricorso a gruppi:
(Lune|Marte|Giove|Vener|)dì|Sabato
In questo esempio, ciascun giorno della settimana costituisce un risultato potenziale, poiché tutti i nomi dei giorni della settimana che terminano in "dì", e che sono proposti come alternativa, vengono correttamente rilevati anche nella forma abbreviata grazie al raggruppamento in parentesi tonde.
Gruppi
I gruppi di caratteri, come quelli illustrati nell'esempio della sezione precedente, rientrano negli elementi strutturali delle espressioni regolari in qualità di classi di caratteri. Possono essere definiti da un paio di parentesi tonde e, di norma, rappresentano un pattern costituito da uno o più caratteri. Ciascuna regex è in sostanza un gruppo, con il quale la definizione tramite parentesi non è applicabile. All'interno delle espressioni, i gruppi concedono la possibilità di impiegare operatori, quali segni di separazione o di ripetizione (più e asterisco), in un'espressione parziale desiderata:
ab(cd)+
In questo caso, la ripetizione desiderata è applicabile per il gruppo di caratteri "cd", mentre sarebbe applicabile solo per la "d" se fosse formulata allo stesso modo ma senza ricorrere alle parentesi. All'interno di una regex non vi sono limiti relativi alla quantità dei gruppi contenuti.
Annidamenti
All'interno di un'espressione regolare non solo può esservi un numero di gruppi qualsiasi, ma possono essere annidati tra loro tutti i gruppi che si desidera per formulare relazioni complesse tra caratteri semplici e speciali anche senza lunghe stringe di caratteri inutili. Un esempio di ciò è costituito dal seguente motivo di regex che ha come possibili risultati quattro modelli di auto "VW Golf", "VW Polo", "Fiat Punto" o "Fiat Panda":
(VW (Golf|Polo)|Fiat (Punto|Panda))
Limiti di parola
Se nell'uso di un'espressione regolare si deve tener conto dei limiti di parola, ossia dell'inizio o della fine di una sequenza alfanumerica, occorre specificarli ricorrendo ai metacaratteri. A tal fine, molti linguaggi utilizzano la combinazione "\b", che può essere premessa, aggiunta o premessa e aggiunta.
La prima variante impone che la sequenza di ricerca si trovi all'inizio della parola:
\bun
Una concordanza per questa espressione regolare è offerta, ad esempio, dalla parola"uno". Al contrario, la parola "Alcuno" è esclusa dai possibili risultati, poiché le lettere "Alc" procedono il carattere oggetto della ricerca. Per ribaltare la situazione, utilizzate la variante numero due e aggiungete il carattere speciale:
un\b
Infine, con la terza opzione fate sì che entrambi i limiti di parola diventino una condizione che, nel caso dell'esempio utilizzato, rende l'articolo indeterminativo "un" l'unico risultato possibile:
\bun\b
Rimuovere il metasignificato di caratteri speciali
Nella sezione precedente, la barra rovesciata ha conferito alla "b" seguente non solo il valore di lettera, ma anche quello di metacarattere. Se combinato con caratteri che di norma appartengono ai caratteri speciali di regex sintattici, esso ottiene l'effetto completamente opposto: il carattere viene trattato come letterale. Grazie a questa opportunità, un'espressione regolare vi permette di cercare senza problemi una data concreta:
11\.10\.2019
In questo caso, la data "11/10/2019" concorda con i criteri di ricerca richiesti come unica stringa di caratteri. Senza applicare la barra rovesciata, entrambi i punti avrebbero il valore di segnalibri per un carattere qualsiasi, rendendo possibili anche i risultati "1101092019“ o "11a10b2019".
"Disinnescare" espressioni regolari voraci
Di norma l'impiego di quantificatori ("?", "+", "*", "{}") fa sì che un'espressione sia "vorace" e cerchi la maggiore concordanza possibile. Tuttavia, dato che questo comportamento non sempre è desiderato, i quantificatori possono essere specificati in un'espressione regolare arginandone la "voracità". Questo processo di modifica viene illustrato nell'esempio seguente:
A.*B
Questa espressione vorace applicata alla stringa di caratteri "ABCDEB" non interromperebbe la ricerca a "AB", ma includerebbe tra i risultati l'intera stringa di caratteri. Al contrario, se la ricerca dovesse interrompersi dopo aver trovato la prima "B", essa avrà bisogno della modifica citata. A tale scopo, in molti linguaggi (tra cui Perl, Tcl, HTML) il quantificatore viene posizionato dopo un punto interrogativo:
A.*?B
In alternativa, l'espressione vorace originaria può essere sostituita anche dalla seguente espressione equivalente "non vorace" al fine di ottenere lo stesso risultato:
A[^B]*B
Il limite delle espressioni voraci regolari complica l'elaborazione di un motivo di ricerca ed è pertanto associato a una maggiore durata di ricerca.