Introduzione alle Espressioni Regolari

Quello di espressione regolare è un concetto che è frequente trovare praticamente ovunque in informatica. E che per un certo periodo di tempo è molto irritante, perchè tutti ne parlano (e di solito ne parlano bene) e tu non hai la minima idea di cosa significhi.

In questo articolo vedremo cosa sono le espressioni regolari, a cosa servono e, brevemente, come si usano.

Espressioni... cosa??

Spesso capita di dover cercare una certa parola o una certa frase in un testo. Ad esempio, supponiamo di dover cercare la sigla MIT all'interno di questo testo (fonte: Wikipedia):

Facile, possiamo farlo in un secondo copiando il testo in un editor di testi qualsiasi (sì, anche il Blocco Note di Windows) e usando la funzione "Trova", come si vede nella seguente immagine:

La funzione 'Trova' fa il suo dovere, anche in Blocco Note

Supponiamo invece di avere un problema un po' più complicato da risolvere, come ad esempio quello di cercare tutti gli anni contenuti nel testo (1928, 1957, etc.)? Se avessimo a disposizione soltanto il Blocco Note a questo punto dovremmo rassegnarci a passare cinque minuti spiacevoli, cercando a mano quello che ci interessa. Infatti utilizzando "Trova" possiamo cercare solo specifiche parole, mentre quello che vogliamo fare ora è trovare tutte le sequenze di quattro numeri (gli anni, appunto) presenti nel documento.

Questo è il tipico problema che possiamo risolvere con le espressioni regolari, come possiamo vedere nella seguente figura:

Un'espressione regolare per trovare tutti gli anni nel testo

Quella strana sequenza di simboli evidenziata in giallo nell'immagine è un'espressione regolare (in inglese regular expression, o più brevemente regex), e a quanto pare è in grado di individuare precisamente quello che volevamo, tutti gli anni presenti nel nostro testo; in questo articolo impareremo a decifrarne significato, e a scriverne di nuove.

Molto grossolanamente, quindi, possiamo dire che un'espressione regolare è una certa sequenza di simboli (una stringa, se vogliamo parlare da informatici) che descrive come è fatto un certo testo (nell'esempio, un anno, formato da quattro numeri ripetuti). Come abbiamo visto nell'esempio, un tipico uso delle espressioni regolari è quello di cercare, in un testo, tutte tutte le parole (ma anche più parole alla volta) che corrispondono alla descrizione.

Un po' di pratica: Rubular

Ok, basta parlare e iniziamo a "sporcarci le mani" con un po' di pratica. Prima di tutto, procuriamoci gli strumenti necessari per sperimentare le espressioni regolari.

Come dicevamo all'inizio, ci sono mille modi in informatica per utilizzare le regex. Esse infatti sono integrate nella maggior parte dei linguaggi di programmazione, e diversi editor di testo supportano la ricerca nel testo con espressioni regolari (Notepad++ su Windows, o Geany, disponibile sia per Linux che per Windows).

Oggi però, per non perdere tempo a installare programmi particolari, utilizzeremo Rubular, un editor di espressioni regolari completo e... online! Lo possiamo raggiungere a questo indirizzo: http://rubular.com/.

L'interfaccia di Rubular si presenta facile da utilizzare: basta scrivere un'espressione regolare nella casella Your regular expression, e un testo di prova, in cui sarà cercata l'espressione, in Your test string. Iniziamo con un esempio banale: vogliamo cercare la parola "pazzo" all'interno di questo testo:

Al di là dell'alto valore poetico del testo in questione, l'esempio è banale: basta ricopiare il "capolavoro" in Your test string e scrivere la parola "pazzo" in Your regular expression:

Un esempio banale

Tutto qui: Rubular evidenzierà automaticamente tutte le parole che corrispondono alla regex che abbiamo inserito. Queste corrispondenze in gergo sono dette match, e il modulo software che esegue il riconoscimento è detto matcher. Vediamo ora come estendere questo esempio e farci qualcosa di più.

Classi di caratteri

Uno dei punti chiave delle espressioni regolari è la possibilità di cercare non uno specifico carattere, ma una classe di caratteri, ovvero uno a scelta tra una lista di caratteri forniti. Ad esempio, diciamo di voler cercare sia la parola "pazzo" che la parola "pazza": dobbiamo trovare un modo per dire a Rubular di cercare la stringa "pazz", più uno a scelta tra i caratteri o e a. Possiamo farlo inserendo, al posto dell'ultima lettera, una classe di caratteri, seguendo questa sintassi:

[abc]
Uno a scelta tra i caratteri a, b o c. Attenzione: le espressioni regolari di default distinguono maiuscole e minuscole, quindi questa espressione non includerà le lettere A, B e C (per includerle dovremmo scrivere [abcABC]); questo vale anche per tutte le altre classi.
[^abc]
Qualsiasi carattere che non sia a, b o c.
[a-z]
Qualsiasi carattere che sia compreso tra a e z (quindi in questo caso tutto l'alfabeto, di sole lettere minuscole).
[a-dw-z]
Qualsiasi carattere che sia compreso tra a e d, o tra w e z (A, b, c, d; w, x, y, z). Ad esempio, con [a-zA-Z] possiamo riconoscere tutte le lettere dell'alfabeto, sia maiuscole che minuscole.
.
Un carattere qualsiasi.

Quindi, per risolvere l'esempio in cui vogliamo trovare sia la parola "pazzo" che "pazza", basterà usare un'espressione come questa:

pazz[oa]

Proviamo invece un'espressione come questa:

p.zz.

In questo modo diciamo al matcher di cercare una lettera p, seguita da un carattere qualsiasi (rappresentato dal punto), seguito da due z, seguite da un altro carattere qualsiasi. In pratica becchiamo tutti i vari pazzo, pazza, pizza, pozzo etc.

Un ultimo esempio; diciamo di voler cercare le stesse parole di p.zz., ma solo quelle che non iniziano con "pi" (quindi pazzo, pazza, pozzo, ma non pizza, pizzo, etc.). Possiamo farlo sostituendo il primo punto con la classe di tutti i caratteri che non sono i ([^i]):

p[^i]zz.

Il risultato dovrebbe essere simile a questo:

Esempio sulle classi di caratteri

Classi predefinite

Proviamo a cercare tutte le parole di due caratteri. Ingenuamente potremmo pensare di usare un'espressione che riconosce due caratteri qualsiasi, come questa:

..

Sfortunatamente però questa espressione non risolve il problema. La prima cosa che notiamo è che il punto riconosce le lettere, ma anche virgole, punti e spazi bianchi: quello che succede quindi è che l'espressione riconoscerà l'intero testo, due caratteri alla volta.

Possiamo fare qualcosa dicendo di cercare solo due lettere dell'alfabeto ([a-z], o meglio [a-zA-Z], in modo da prendere sia le maiuscole che le minuscole):

[a-zA-Z][a-zA-Z]

Un po' meglio, ma non è ancora finita: l'espressione riconosce anche tutte le sequenze di due lettere che si trovano all'interno di parole più grandi, mentre noi vogliamo le singole parole di due lettere. Possiamo "aggiustarla" aggiungendo due spazi prima e dopo le tre lettere (le parentesi quadre non sono strettamente necessarie, ma spesso si mettono per rendere l'espressione più leggibile):

[ ][a-zA-Z][a-zA-Z][ ]

L'espressione che abbiamo appena scritto funziona, ma è decisamente "ingombrante" e non molto semplice da leggere; inoltre la prima parola, "Al", non viene riconosciuta perchè non è preceduta da uno spazio bianco. In questi casi è utile fare uso di alcune classi di caratteri predefinite:

\s
Un qualsiasi spazio (incluse tabulazioni e "a capo").
\S
Qualsiasi carattere che non sia uno spazio.
\d
Qualsiasi numero (0, 1, 2, 3...); è come scrivere [0-9].
\w
Qualsiasi carattere che può essere parte di una parola (lettere, numeri e underscore).
\W
Qualsiasi carattere che non può essere parte di una parola (vedi sopra).
\b
Qualsiasi carattere che delimiti una parola dall'altra (spazi bianchi, "a capo", trattini). Anche l'inizio e la fine di una riga sono riconosciuti come delimitatori.

Facendo uso di queste classi, possiamo scrivere un'espressione un po' più elegante ed efficace per trovare le parole di due lettere:

\b\w\w\b

Notiamo subito la parte centrale, formata da due \w, ovvero due caratteri qualsiasi tra lettere, numeri e underscore (che nel nostro caso saranno solo lettere, visto che non abbiamo nè numeri nè underscore nel testo); questi due caratteri sono poi racchiusi tra due delimitatori (\b), che impediscono alle due lettere di trovarsi in mezzo ad una parola, ad esempio:

  • guizzi di bizza corrisponde, perchè il matcher trova uno spazio (\b) prima della parola "di", due lettere (\w\w, la d e la i di "di", appunto) e un altro spazio (\b).

  • nel pozzo non corrisponde perchè, anche se ci sono uno spazio e due lettere (" po", che corrispondono con \b\w\w), non c'è nessuno spazio immediatamente dopo " po", e quindi l'ultimo \b non trova corrispondenza.

  • nel pozzo addirittura non ha spazi, nè prima nè dopo, e quindi non corrisponde.

Questo è il risultato:

Esempio sulle classi di caratteri predefinite

Notiamo che la prima parola nel testo, Al, viene individuata, anche se non ha nessun carattere alla sua sinistra; questo perchè l'inizio (così come la fine) di una riga è contato ugualmente come delimitatore, e quindi corrisponde con \b.

Àncore

Per concludere il paragrafo, diamo un'occhiata anche a questi due simboli:

^
Indica l'inizio della riga.
$
Indica la fine della riga.

Niente di complicato, ma conviene ricordare questi due simboli perchè possono tornare molto utili, specie in informatica, dove si lavora spesso con liste di elementi. Un esempio, diciamo di avere una lista di files e di voler trovare tutti quelli con estensione mp3:

Sarà sufficiente cercare tutte le righe che terminano con mp3, e possiamo farlo usando il carattere di fine riga, $:

mp3$

In questo modo diciamo al matcher di cercare tutte le occorrenze della parola mp3 che sono "attaccate" alla fine di una riga ($).

Ripetere un carattere

Le classi di caratteri ci permettono già di provare un'infinità di combinazioni diverse, ma ancora siamo solo a metà di quello che diremo sulle espressioni regolari.

Un'altra indispensabile caratteristica delle regex è la ripetizione dei caratteri. Infatti per ogni carattere "a" dell'espressione (inteso come un semplice carattere, es. a, o classe di caratteri, es. [abc] o [^c]) possiamo specificare quante volte il carattere deve essere presente nel testo. Possiamo farlo con la seguente sintassi:

a?
Il carattere che precede il punto di domanda può comparire zero o una volta; in pratica, ci può essere o non essere nel testo. Ad esempio, l'espressione pia?zza troverà sia piazza che pizza: il punto di domanda dopo la prima a significa che quella lettera può anche non esserci nella parola che stiamo cercando.
a*
Il carattere che precede l'asterisco può comparire zero o più volte; ad esempio l'espressione ciao!* individua le stringhe ciao, ciao!, ciao!!, ciao!!!!!! etc.
a+
Il carattere che precede il segno più può comparire una o più volte; ad esempio, l'espressione a+w yeah! individuerà le stringhe aw yeah!, aaw yeah!, aaaaaaw yeah! e via dicendo. Notare la differenza con il punto precedente: la a che precede il + deve essere presente almeno una volta, mentre se avessimo usato l'asterisco avrebbe anche potuto non esserci.
a{3}
Il carattere prima delle parentesi graffe deve essere presente esattamente 3 volte. Ad esempio, l'espressione piz{2}a individua esattamente la parola pizza. Un altro esempio, usando le classi di caratteri è l'espressione [asd]{6}; questa espressione indica 6 caratteri a scelta tra a, s e d, come asdasd, dsadsa, aaaaaa, aassdd, dasdas e così via per tutte le altre 729 combinazioni possibili.
a{3,}
Il carattere prima delle parentesi graffe deve essere ripetuto 3 o più volte; ad esempio, l'espressione FFFU{3,} becca tutte le stringhe che iniziano con tre F maiuscole, e finiscono con almeno tre U maiuscole, quindi FFFUUU, FFFUUUUUU etc, ma non FFFUU (ci sono solo due U) o fffuuuu (le lettere sono minuscole).
a{3,6}
Il carattere prima delle parentesi graffe deve essere ripetuto minimo 3 e massimo 6 volte; ad esempio, l'espressione cap{1,2}ello individuerà le parole capello e cappello.

Esempio: numeri di telefono

Vediamo come usare queste informazioni per rsolvere un probema nel "mondo reale". Diciamo che un tizio di nome Bob abbia dimenticato il numero di cellulare di Anna, che lei gli ha scritto via chat. Bob ha salvato la cronologia della chat (la conversazione, pubblicata con il permesso dell'autore, è tratta da http://www.lewisdispensa.com/)...

...ma non ha nessuna voglia di rileggere tutto per cercare il numero di telefono. Bob però sa usare le espressioni regolari, e sa come sono fatti i numeri di cellulare:

  • Le prime 2, 3 o 4 cifre sono il prefisso (es. il prefisso di Milano è 02, quello di un cellulare potrebbe essere 347, mentre quello di Pordenone è 0434). Con il "linguaggio" delle espressioni regolari questo lo possiamo scrivere con [0-9]{2,4}, o più brevemente \d{2,4}; queste espressioni rappresentano, appunto, una cifra qualsiasi (\d, che equivale a scrivere [0-9]) ripetuta da 2 a 4 volte ({2,4})

  • Dopo il prefisso potrebbe esserci uno spazio o un trattino per separare il prefisso dal numero. Attenzione però, non tutti separano prefisso e numero: potrebbe capitare anche che non ci sia nulla in mezzo. L'espressione regolare per qesto punto quindi è [- ]?, che significa un carattere a scelta tra un trattino e uno spazio bianco ([- ]) che compare zero o una volta (?).

  • Le ultime cifre sono il numero vero e proprio. Questo possiamo scriverlo semplicemente come \d+, ovvero un numero (\d) ripetuto una o più volte (+). Per rendere l'espressione un po' più precisa possiamo anche modellare il fatto che un numero di telefono ha almeno 5 cifre; possiamo farlo facilmente scrivendo \d{5,}, dove {5,} significa, appunto, che la nostra cifra andrà ripetuta 5 o più volte.

  • Spesso, per rendere il numero più leggibile, le cifre che lo compongono non sono scritte di fila, ma separate da spazi (eg. "02 123 45 67" invece di "02123456"). Possiamo quindi modificare l'espressione del punto precedente in modo che includa anche gli spazi: [\d ]{5,}; questa espressione troverà un carattere a scelta tra un numero e uno spazio ([\d ]) ripetuto cinque o più volte ({5,}).

Et voilà, ora basta mettere assieme i pezzi per ottenere l'espressione che risolve il problema:

\d{2,4}[- ]?[\d ]{5,}

L'espressione individuerà tutti i numeri telefonici nel testo:

Esempio sui numeri di telefono

Escape di caratteri speciali

Cosa succede se proviamo a cercare questa espressione su Rubular?

2+2=4

Naturalmente noi ci aspetteremmo che l'espressione trovi tutte le addizioni "2+2=4" che ci sono nel testo. Quest però non succede perchè il segno + è un carattere speciale (o meta-carattere) delle espressioni regolari; quindi il matcher interpreterà la prima parte dell'espressione, 2+ con il suo significato nelle regex, e si aspetterà quindi una sequenza di 2 (2, 22, 22222, e via dicendo).

Per trattare il segno + come un normale carattere, anzichè come un parametro dell'espressione, sarà necessario inserire un backslash (\) appena prima del +:

2\+2=4

In questo modo il significato speciale del + viene ignorato, e l'espressione troverà letteralmente tutte le stringhe che corrispondono a 2+2=4.

Questo naturalmente vale per tutti gli altri caratteri speciali che abbiamo visto finora (*, ?, [, ], ^, $, \, e via dicendo).

Funzioni avanzate: modificatori, gruppi e asserzioni

Ci sono in realtà un altro paio di cose più o meno importanti che andrebbero studiate quando si imparano le espressioni regolari. In questo articolo però non le andremo ad approfondire: per ora direi che abbiamo detto abbastanza.

Diamo comunque un'occhiata veloce a queste funzioni "avanzate", giusto per sapere di cosa stiamo parlando.

Modificatori

Se guardiamo meglio Rubular, vediamo che a destra della casella "Your regular expression" ce n'è un'altra più piccola, separata da uno slash (/). Quella casella serve a contenere uno o più modificatori, per aggiungere opzioni all'espressione che abbiamo scritto. I principali modificatori sono:

i
Case insensitive, ovvero l'espressione non distnguerà più tra lettere maiuscole e minuscole.
m
Multi-line match. Anche se in Rubular questo non succede, di solito le espressioni regolari considerano il testo in input come un'unica riga, anche se ci sono degli "a capo" in mezzo. Attivando questa opzione il testo viene scandito riga per riga, rendendo possibile usare le àncore per le singole righe che compongono l'input.
x
Ignora gli spazi nell'espressione regolare (tranne quelli che si trovano tra parentesi quadre).

Gruppi

Le espressioni regolari si usano molto non solo per cercare un certo testo, ma anche per sostituire degli elementi nel testo, o catturare delle informazioni.

I gruppi sono definiti da parentesi tonde, permettono di estrarre tutto il contenuto delle parentesi. Ad esempio, se modifichiamo l'espressione dei numeri di telefono che abbiamo scritto prima, inserendo delle parentesi tonde attorno al numero...

\d{2,4}[- ]?([\d ]{5,})

...l'espressione memorizzerà tutti i numeri di telefono che trova, ma solo la parte dopo il prefisso, perchè le parentesi non lo includono.

Ovviamente questo è inutile finchè usiamo Rubular (l'unico effetto in questo caso sarà che vedremo comparire una lista di elementi "catturati"), ma in molte altre applicazioni, ad esempio nella programmazione catturare elementi di testo è spesso fondamentale per poterli usare successivamente.

Possiamo usare i gruppi anche per specificare la ripetizione, in ordine, di più di un carattere:

(asd)+

Questa espressione individua i tre caratteri asd ripetuti una o più volte (asd, asdasd,asdasdasd). Notare la differenza con [asd]+, che ripete un carattere a, s o d una o più volte, permettendo quindi anche ripetizioni non ordinate (es. aaasd, sdddsa, e via dicendo).

Un ultimo uso dei gruppi riguarda le alternative, che possiamo ottenere con il carattere pipe (|):

sto (dorme|studia)ndo

Questa espressione individa le due stringhe "sto dormendo" e "sto studiando".

Notare che, in ogni caso, il contenuto delle parentesi tonde viene "catturato". Per evitare questo effetto, che in certe situazioni potrebbe essere fastidioso, possiamo inserire i caratteri ?: subito dopo l'apertura della parentesi:

(?:asd)+
sto (?:dorme|studia)ndo

Asserzioni

Le asserzioni sono un'estensione della normale sintassi delle regex che permette di "sbirciare" in altri punti del testo, senza includere i caratteri "sbirciati" nel risultato del matching. Vediamo solo qualche esempio:

c(?=iao)
Trova tutte le lettere "c" che si trovano subito prima di "iao".
c(?!iao)
Trova tutte le lettere "c" che non si trovano subito prima di "iao".

Ci sono altri tipi di asserzione, ma non li esamineremo in questo articolo. Chi fosse curioso può dare un'occhiata ai link in fondo all'articolo.

In breve...

In questo articolo abbiamo dato un'infarinatura generale sulle espressioni regolari, cosa sono e come si usano. In particolare abbiamo visto che:

  • Le espressioni regolari sono un linguaggio usato per descrivere come è fatto un ceto testo.
  • Sono uno strumento molto potente per cercare determinate frasi o parole nel testo.
  • Possiamo usare Rubular per fare un po' di pratica senza impegno con le regex.
  • Possiamo specificare intere classi di caratteri per avere più possibilità di ricerca. Ad esempio, pizz[ao] trova tutte le parole che iniziano per pizz e finiscono con una lettera a scelta fra a e o
  • Possiamo ripetere un carattere o una classe di caratteri più di una volta. Ad esempio, ciao!+ trova la parola ciao seguita da uno o più punti esclamativi.
  • Esistono diverse funzioni avanzate, come modificatori, gruppi e asserzioni che estendono la sintassi di base che abbiamo visto nell'articolo.

Link utili