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à:
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