Libero

Espressioni regolari

Le espressioni regolari (regular expression in anglo-sassone e spesso abbreviate in RE) descrivono in maniera sintetica, tramite una particolare sintassi, un modello testuale ed eseguono dei confronti per scoprire se una stringa di caratteri è conforme o meno. Un esempio banale, ma comune, di RE è la descrizione delle targhe automobilistiche italiane. L'identificativo dei veicoli è conforme al seguente modello:

lettera lettera cifra cifra cifra lettera lettera

Tale descrizione si basa sul fatto che si sappia che cosa si intende per «cifra» e «lettera». Per cifra si intende 1 delle usuali 10 cifre arabe. Per lettera si intende 1 lettera maiuscola dell'alfabeto internazionale, che ne conta 26.

Storicamente le RE esistono da molti anni. Poi col passare del tempo le RE sono state ampliate e potenziate tanto che il nucleo originario viene indicato come BRE, ovvero Basic RE mentre lo stato dell'arte viene designato come ERE, Extended RE. Ma, almeno in questo caso, è stato usato il buon senso: le BRE sono un sottoinsieme delle ERE, salvaguardando così l'esperienza maturata. Di più, dato il diffondersi delle RE, è stato emanato uno standard.

All'atto pratico le RE prevedono delle regole che permettono la realizzazione dei modelli di complessità arbitraria e dei metodi per la validazione (aderenza ad un modello) e la ricerca (presenza di un modello). Nel mondo Unix ci sono vari tool che permettono oppure si basano sull'uso delle RE. Un elenco certamente non esaustivo comprende grep, flex, awk, vi, sed. Ma lo strumento che di gran lunga sfrutta le potenzialità delle RE è il PERL.
Supponiamo di avere una certa quantità di dati testuali e di voler ricercare la presenza di una determinata stringa. Lo strumento scelto esegue il compito affidatogli segnalando le eventuali conformità. Aiutiamoci con un esempio ed operiamo in ambiente Linux. Il file da esplorare si chiama testo.txt e contiene i seguenti dati:
Nord
Sud
Est
Ovest
Ernesto
estate
foresta estesa
Si desidera sapere se la stringa est è presente o meno. Come strumento si utilizza il comando grep. La scelta non è casuale. grep è un acronimo che sta per «general RE printer». Il comando da utilizzare è il seguente:
     grep --color -E 'est' testo.txt
la cui anatomia è la seguente:

grep comando
--color stampa in rosso il risultato
-E abilitazione all'uso delle RE estese
'est' stringa da ricercare *
testo.txt file da esplorare

*: la stringa viene incastonata tra apici singoli per evitare interference dovute al quoting.
Il comando grep lavora "a righe". Se trova la stringa ricercata, stampa tutta la riga che la contiene. Quindi molto opportunamente conviene far stampare con un colore diverso i caratteri intercettati. Questo è il risultato:
Ovest
Ernesto
estate
foresta estesa
La riga che contiene EST non viene riprodotta sull'output poiché «EST» ed «est» sono differenti. Dall'esempio si ricava una regola molto importante. Nel campo delle RE ogni carattere rappresenta se stesso. Quindi, ad esempio, 1 rappresenta quel carattere; lo stesso vale anche per %. Ma i seguenti caratteri:
( ) [ ] { } ? * + $ ^ . \ |
hanno un comportamento differente. Sono noti come metacaratteri in quanto utilizzati nelle sintassi delle RE. Se devono essere usati in senso letterale, vanno fatti precedere dal carattere \ (noto come carattere «escape»).
Supponiamo di avere un modello così composto:
^est
Se applicato al file testo.txt produce questo risultato:
estate
Il metacarattere ^ indica l'inizio riga. ^est viene interpretato come ricerca delle righe che iniziano con la stringa indicata.
Supponiamo di avere un modello così composto:
d$
Se applicato al file testo.txt produce questo risultato:
Nord
Sud
Il metacarattere $ indica la fine riga. d$ viene interpretato con ricerca delle righe che finiscono con la stringa indicata.
I metacaratteri ^ e $ possono essere usati congiuntamente. Il modello:
^Est$
serve ad individuare le righe che contengono la sola stringa indicata. Applicato al file testo.txt produce l'ovvio risultato:
Est
Supponiamo di avere un modello così composto:
ar.a
allora le stringhe area, aria, arma ed arca sono tutte conformi al modello. Il significato del metacarattere . (punto) è quello di rappresentare 1 carattere, qualsiasi esso sia. Pertanto la stringa aroma non è conforme al modello. Supponiamo di avere un modello così composto:
ar[ei]a
allora le stringhe aria ed area sono conformi, mentre non lo sono arma ed arca. Il significato dei metacaratteri [] è quello di rappresentare la scelta: un unico carattere presente all'interno delle parentesi. Talvolta all'interno delle parentesi quadre è necessario scrivere diversi caratteri. Se questi sono contigui è possibile usare una notazione più abbreviata. Resta da definire il concetto di contiguità. Per le lettere il concetto è abbastanza ovvio. Le lettere m, n ed o sono contigue (per rendersene conto è sufficiente recitare l'alfabeto). Anche nel campo delle cifre il concetto è facile da applicare. Le cifre 4, 5 e 6 sono contigue. Forti di queste precisazioni, il modello:
[A-Z]
rappresenta una sola lettera maiuscola dell'alfabeto internazionale, mentre il modello
[0-9]
rappresenta una sola cifra. L'abbreviazione si ottiene frapponendo il carattere - (il trattino) tra gli estremi. Si possono indicare anche più intervalli. Il modello:
[a-il-vz]
rappresenta 1 sola lettera minuscola dell'alfabeto italiano. A conti fatti risultano escluse le lettere j, k, w, x ed y. Accanto agli intervalli, talvolta è necessario escludere la presenza di certi caratteri. In tal caso si fa entrare in gioco il metacarattere ^, già visto come "indicatore di inizio riga". Qui però ha il significato di negazione. Il modello:
ar[^i]a
descrive un qualsiasi stringa che contiene i caratteri ar, 1 carattere qualsiasi purché diverso da i, ed un'ulteriore presenza di a. Pertanto le stringhe arca, area ed arma sono conformi mentre non lo è aria. Naturalmente è possibile negare gli intervalli. A scanso di equivoci il carattere di negazione (^), se usato, va posto davanti a ciò che si intende escludere. Quindi:
[^A-J]
serve ad eliminare una sola lettera maiuscola compresa tra A e J, estremi inclusi. Talvolta esiste la necessità di ripetere una parte del modello più volte. Anziché ripeterla, pratica sempre possibile, si usa l'apposita sintassi. Che fa ricorso alle { } (parentesi graffe). Ad esempio:
a{5}
è la notazione per reiterare per 5 volte il carattere a. Così facendo si aumenta la comprensibilità delle RE. a{5} mette in evidenza la ripetizione in maniera migliore di aaaaa. Si badi bene che il meccanismo di ripetizione si applica all'elemento precedente. Se scrivo:
ab{2}
la ripetizione si applica al solo carattere b. Volendola estendere anche al carattere a la notazione è leggermente differente:
(ab){3}
Le () sono i metacaratteri che operano il raggruppamento. Non sempre è possibile conoscere con precisione il numero delle ripetizioni. è possibile indicare un numero minimo ed un massimo, oppure uno solo dei due ed anche nessuno dei due. Scrivendo:
ar{,1}ia
si sottintende che il numero minimo di ripetizioni è pari a 0 (zero). Le stringhe aia ed aria sono conformi, mentre non lo è arria. Se invece a mancare è il massimo, il valore default è ∞, da intendersi non tanto come infinito quanto come qualsivoglia. Quindi:
[a-z]{3,}
descrive una stringa composta da 3 a qualsivoglia numero di lettere minuscole.
Una volta assimilati i concetti di scelta ([]) e ripetizione ({}), si può rimodulare in termini rigorosi la RE che descrive le targhe automobilistiche italiane. Partendo dalla descrizione letterale già fatta:
lettera lettera cifra cifra cifra lettera lettera
si passa ad una descrizione più concisa
lettera{2}cifra{3}lettera{2}
per poi usare la sintassi RE
[A-Z]{2}[0-9]{3}[A-Z]{2}
ove la colorazione è stata fatta a soli fini didattici.
Il meccanismo delle ripetizioni va esplorato nella sua interezza. Alcuni tipi di ripetizione si presentano con particolare frequenza, per cui sono state introdotte delle abbreviazioni. La sottostante tabella le sommarizza:

modello # ripetizioni abbreviazione
{,} da 0 a ∞ volte *
{0,1} 0 volte oppure 1 sola ?
{1,} da 1 a ∞ volte +

L'asterisco è l'abbreviazione più nota ed utilizzata (senza sapere che fa parte delle RE). È dato per noto.
Il ? (punto di domanda) serve per controllare la presenza o meno di un elemento. Supponiamo di voler descrivere, con la sintassi propria delle RE, i numeri da 1 a 99. I primi 9 numeri si descrivono facilmente:
[1-9]
I numeri da 10 a 99 si descrivono tramite:
[1-9][0-9]
Osservando bene le due RE si osserva che [1-9] è presente in entrambe, mentre [0-9] è presente solo nella seconda. Ovvero può apparire 1 sola volta oppure non apparire affatto (= zero volte). Allora le due RE possono essere fuse in un'unica:
[1-0][0-9]?
Anche in questo caso si tenga a mente che il fattore di reiterazione (?) si applica al solo elemento precedente, che è [0-9].

Un ulteriore cenno riguarda l'alternanza. Il modello:
alfa|beta
serve per ricercare la stringa alfa oppure, in alternativa, la stringa beta, senza l'obbligo che le stringhe abbiano la stessa lunghezza. L'alternanza viene realizzata ricorrendo al metacarattere | (barra verticale).
Si abbia, ad esempio, un testo scritto in inglese che fa riferimento agli autocarri. Non è dato di sapere preventivamente se il testo è scritto in inglese oppure in americano. Quindi, per cercare la parola autocarro in anglo-sassone bisogna indicare sia la stringa «van» (GB) che «truck» (USA). In tal caso l'alternanza fa proprio al caso nostro. Quindi:
van|truck

Le RE, per definizione, restituiscono il massimo numero di caratteri conforme al modello proposto. Consideriamo il seguente input:
catafalco
ed il seguente modello:
a.*a
In pratica si ricerca la stringa di caratteri che inizia e finisce col carattere a. Come risultato si ottiene:
catafalco
Per ottenere la stringa minima si opera il seguente ragionamento, da cui poi far discendere il modello. Si deve ricercare la stringa che inizia col carattere voluto (in questo caso a) seguito da un qualsivoglia numero di caratteri diversi da a. La stringa si conclude quindi col carattere a. Il modello è:
a[^a]*a
i caratteri scritti in rosso rappresentano rispettivamente il carattere di inizio e fine stringa e l'entità [^a] rappresenta un qualsiasi carattere «diverso da a» cui viene applicato il moltiplicatore * (un qualsivoglia numero). Se l'input è:
abbandonare
Andromaca
cassapanca
col modello proposto a[^a]*a verrà restituito:
abbandonare
cassapanca

Accanto alle espressioni canoniche, col tempo sono state introdotte delle utili abbreviazioni oltre che delle estensioni. Sono riportate nella tabella sottostante (l'elenco potrebbe non essere esaustivo).

[:lower:] [a-z]
[:upper:] [A-Z]
[:alpha:] [:lower] e [:upper:]
[:digit:] [0-9]
[:xdigit:] [0-9a-fA-F]
[:alnum:] [:alpha:] e [:digit:]
[:space:] «white space character»
[:blank:] lo spazio ed il tabulatore orizzontale
[:print:] un carattere stampabile
[:graph:] un carattere stampabile, spazio escluso
[:cntrl:] opposto di [:print:]
[:punct:] un carattere di punteggiatura
\b inizio/fine parola
\B equivale a [^\b]
\d equivale a [0-9]
\D equivale a [^\d]
\s un "white space character"
\S equivale a [^\s]
\w [:alnum:] e l'underscore
\W equivale a [^\w]

Per «parola» si intende una sequenza di \w.
Per «carattere di punteggiatura» si intendono i seguenti caratteri !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

Ci sono delle occasioni in cui visto finora non è sufficiente. Un esempio potrebbe essere quello di scoprire eventuali ripetizioni di parole, purché sulla stessa riga. Un altro esempio potrebbe essere quello di evidenziare i caratteri doppi, o, generalizzando, multipli.
In entrambi i casi sono necessarie due funzionalità:

  1. la memorizzazione dei caratteri intercettati
  2. la loro gestione

Tutti i tool che ammettono l'uso delle regular expression poggiano su un regex engine interno. Nelle sue forme più evolute, tale motore mette a disposizione 9 buffer, numerati a partire da 1 (uno). Per poter effettuare la memorizzazione, le regex devono essere scritte in modo da permettere all'engine di operare appropriatamente. La regola è la seguente: la regex viene racchiusa con delle parentesi tonde. In tal caso i caratteri intercettati vengono scritti nel primo buffer disponibile. Si procede in questo modo: basta scorrere la regex da sinistra verso destra. All'incontro di una regex tra parentesi viene usato il primo buffer, la regex successiva tra parentesi usa il secondo e così via. Per far riferimento al contenuto del buffer voluto, basta citare il suo numero facendolo precedere dal carattere escape (\).
Vediamo un breve esempio di come si possano intercettare le occorrenze multiple dei caratteri presenti in un testo. Il metacarattere . (punto) individua un carattere qualsiasi. Se mettiamo il punto tra parentesi questo carattere viene memorizzato nel primo buffer (buffer n° 1). Se lo citiamo, secondo la sintassi esposta otteniamo la seguente regex:
(.)\1
che è in grado di intercettare tutti i caratteri doppi. Volendo generalizzarla in modo da intercettare le occorrenze multiple, dobbiamo applicare la ripetizione opportuna al buffer usato. Quindi:
(.)\1+
Per convincersene, supponiamo di operare col comando Unix grep sul file palestra.txt così composto:
alfa alfa
beta
Gamma
deltA
effe
VRuuuuum
parola _w0rd. vocabolo
Il comando:
     grep --color -E '(.)\1+' palestra.txt
restituisce il seguente output (in rosso quanto intercettato)
Gamma
effe
VRuuuuum

Nel caso delle parole doppie si segue uno schema simile. Prima si scrive la regex che individua una parola ((\b\w+\b)) e la si mette nel primo buffer. Poi con un'altra regex (\s+) si intercettano gli spazi/tabulatori ed infine si cita il primo buffer (\1). Riassumendo, la regex è la seguente:
(\b\w+\b)\s+\1
Applicata al file palestra.txt produce il seguente output:
alfa alfa