Michele Beltrame
g. Approfondimenti sulle regular expressions
http://www.perl.it/documenti/articoli/2007/12/approfondimenti.html
© Perl Mongers Italia. Tutti i diritti riservati.

Data di pubblicazione: Questo articolo è stato pubblicato su Dev n°81 e in questo sito per autorizzazione espressa del "Gruppo Editoriale Infomedia" (http://www.infomedia.it).

© 2007 Michele Beltrame.

1. Introduzione 

2. Classi di caratteri 

3. Metacaratteri per il pattern matching 

4. Validità sintattica di un indirizzo e-mail 

5. Modificatori 

6. Sostituzioni con s/// 

7. Traslitterazioni con tr/// 

8. Conclusioni 

Introduzione

La scorsa lezione è stata di carattere più che altro introduttivo: come accennato verso la fine, ben superiore è la potenza delle regular expression, ed essa verrà almeno in parte spiegata in questa puntata. Anzitutto sarà mostrato l'uso delle classi di caratteri e di alcuni metacaratteri utili per effettuare raffinati pattern matching all'interno di una stringa. In secondo luogo saranno mostrate alcune funzioni diverse dalla pura ricerca di sottostringhe, quali la sostituzione di caratteri.

 Classi di caratteri

Poniamo di definire la seguente stringa:

$a = 'Ho comprato 2 rose nere';

e che il numero di rose possa variare indefinitamente. Quello che desideriamo è far sì che il matching abbia esito positivo solo se le rose comprate sono due oppure tre. La soluzione al problema potrebbe essere la seguente:

$a =~ /^Ho comprato (2|3) rose nere$/;

É tuttavia possibile ricorrere alla classi di caratteri, che come vedremo tra poco costituiscono una soluzione ben più completa a problemi di questo tipo:

$a =~ /^Ho comprato [23] rose nere$/;

La parte tra parentesi quadre è la classe, ed è costituita dai vari caratteri che dobbiamo considerare validi ai fini del matching, in questo caso 2 e 3. Una classe di caratteri permette anche di specificare un intervallo. Se ad esempio le rose devono essere un numero variabile da tre a otto possiamo utilizzare una qualsiasi delle seguenti tre soluzioni:

# Senza classe di caratteri
$a =~ /^Ho comprato (3|4|5|6|7|8) rose nere$/;
# Con classe di caratteri senza intervallo
$a =~ /^Ho comprato [345678] rose nere$/;
# Con classe di caratteri ed intervallo
$a =~ /^Ho comprato [3-8] rose nere$/;

Appare evidente la maggiore efficienza delle ultime due soluzioni, e la maggiore praticità dell'ultima in particolare. Gli intervalli specificabili sono di vario tipo:

# Cerca un qualsiasi numero in $a
$a =~ /[0-9]/;
# Cerca una qualsiasi lettera minuscola in $a
$a =~ /[a-z]/;
# Cerca una qualsiasi lettera maiuscola in $a
$a =~ /[A-Z]/;

In una classe si possono tranquillamente combinare più intervalli, mescolandoli anche a caratteri singoli. Se ad esempio poniamo un generico (e fin troppo semplice) codice di registrazione di un programma:

$a = 'REGt';

e vogliamo verificare che l'ultimo carattere sia compreso tra a e c, F e M oppure sia t o ancora un numero fra 3 e 7 possiamo usare:

$a =~ /^REG[a-cF-M3-7t]$/;

Ogni classe di caratteri può avere significato sia negativo che positivo. Se infatti il nostro desiderio è che l'ultimo carattere del codice non sia uno di quelli sopra citati possiamo senz'altro utilizzare:

$a !~ /^REG[a-cF-M3-7t]$/;

L'operatore !~ ribalta il significato logico di ogni pattern matching: esso differisce da =~ allo stesso modo in cui != differisce da ==. Potremmo comunque ometterne l'uso cambiando direttamente il significato della classe di caratteri:

$a =~ /^REG[^a-cF-M3-7t]$/;

L'accento circonflesso (^, chiamato informalmente anche coppo) corrisponde ad un not, e quindi indica alla regular expression di trovare caratteri che non siano quelli che lo seguono all'interno della classe. É molto importante distinguere i due diversi significati dell'accento circonflesso: se è posto all'inizio della regular expression esso fa sì che il matching abbia inizio con il primo carattere della stringa e mai dopo; se invece il suo contesto è rappresentato da una classe di caratteri, esso ne ribalta il significato. I quantificatori possono essere associati alle classi esattamente come lo sono ai caratteri normali:

$a =~ /^Ho comprato [0-9]+ rose nere$/;

Questa regular expression ha esito positivo se il numero di rose è un qualsiasi intero positivo maggiore o uguale a zero: la classe di caratteri è infatti ripetibile, per via del quantificatore +, un numero arbitrario di volte. Potremmo ulteriormente raffinare la nostra espressione così:

$a =~ /^Ho comprato [1-9][0-9]* rose nere$/;

La differenza con l'esempio precedente è che in questo caso la prima cifra (che può anche essere l'unica) deve essere un numero da uno a nove: non è quindi possibile evitare di acquistare almeno una rosa nera!

 Metacaratteri per il pattern matching

Le classi di caratteri sono a dire il vero utilizzate solo in alcuni casi, in quanto quelle più comuni possono essere sostituite con dei praticissimi metacaratteri, caratteri dinanzi ai quali è posto un escape (cioè un backslash). Quelli classici sono riportati in Tabella 1, in cui per ciascuno di essi è anche indicata la classe corrispondente.

Simbolo>SignificatoClasse di caratteri
\d
Numero[0-9]
\D
Non-numero[^0-9]
\s
Spaziatura[ \t\n\r\f]
\S
Non-spazio[^ \t\n\r\f]
\w
Carattere alfanumerico[a-zA-Z0-9_]
\W
Carattere non alfanumerico[^a-zA-Z0-9_]

Scrivere ad esempio:

$a =~ /[0-9]{3}/;

equivale ad utilizzare:

$a =~ /\d{3}/;

I metacaratteri possono essere raggruppati a loro volta in una classe più complessa, ed è possibile inserire in quest'ultima anche caratteri normali. Se ad esempio vogliamo che la regular expression trovi solo numeri , spazi e cancelletti possiamo scrivere:

$a =~ /[\d\s#]/;

Chi ha dato un'occhiata a \s o \S, sempre in Tabella 1, avrà notato che la classe di caratteri equivalente ad essi contiene uno spazio ed altri quattro metacaratteri: lo spazio naturalmente trova se stesso, mentre gli altri trovano rispettivamente il tab, il newline, il carriage return ed il form feed. Tutti questi caratteri sono considerabili come spazi, e sono quindi stati inseriti in \s e \S: è opportuno tenere conto di questo aspetto quando si scrivono le proprie regular expression. In Tabella 2 è riportata la lista dei metacaratteri di uso più comune.

SimboloSignificato
\0
Trova il carattere NULL (ASCII 0)
\NNN
Trova il carattere indicato, in notazione ottale, fino a \377
\a
Trova il carattere BEL (quello che fa suonare lo speaker)
\b
Trova il carattere BS (backspace)
\cX
Trova il carattere CTRL-X (es.: \cZ trova CTRL-Z)
\d
Trova un numero
\D
Trova un non-numero
\e
Trova il carattere ESC (escape, da non confondersi col backslash)
\f
Trova il carattere FF (form feed)
\n
Trova il carattere NL (new line, CR sui Macintosh)
\r
Trova il carattere CR (carriage return, NL sui Macintosh)
\s
Trova uno spazio (spazio, HT, NL, CR, FF)
\S
Trova un non-spazio
\t
Trova il carattere di tabulazione (HT)
\w
Trova un carattere alfanumerico più l'underscore (_)
\W
Trova un carattere che non sia un alfanumerico o un underscore
\x
Trova i carattere indicato, in notazione esadecimale

 Validità sintattica di un indirizzo e-mail

A questo punto siamo in grado di capire bene la prima regular expression vista nella scorsa puntata, e cioè quella che verifica la validità sintattica di un indirizzo di posta elettronica. Essa è di seguito riportata:

/^([\w\-\+\.]+)@([\w\-\+\.]+).([\w\-\+\.]+)$/

Si notino anzitutto l'accento circonflesso ed il dollaro, posti rispettivamente all'inizio ed alla fine dell'espressione e che richiedono che l'indirizzo e-mail sia tutto ciò che è contenuto nella stringa analizzata. Il pattern si può dividere in tre parti, ciascuna delle quali è costituita da un set di parentesi tonde contenenti:

([\w\-\+\.]+)

Questo sotto-pattern è formato essenzialmente da una classe di caratteri, che va in cerca di qualcosa che sia un carattere alfanumerico (incluso l'underscore), un segno meno, un segno più oppure un punto, cioè tutti i caratteri ritenuti universalmente validi in un indirizzo e-mail, escludendo la chiocciolina. Il + aggiunto fuori dalle parentesi quadre fa sì che il matching sia positivo solo se vengono trovati uno o più caratteri di quel tipo. Analizzando l'espressione da sinistra verso destra, dopo il primo set di parentesi, viene cercata una (e non più di una) chiocciolina, indispensabile in un indirizzo di posta elettronica. I due set di parentesi a destra, separati da un punto, potrebbero a prima vista sembrare un doppione, in quanto il punto è già incluso nelle classi di caratteri definite. Se tuttavia non operassimo in questo modo otterremmo come validi sia, giustamente, indirizzi quali:

anna@mail.adriacom.com
anna@adriacom.com

che, erroneamente:

anna@adriacom

La regular expression così com'è impostata richiede quindi la presenza a destra della chiocciolina di due caratteri separati da un punto. Detto questo, il pattern in questione va bene nel 99% dei casi, ma lascia tranquillamente passare per validi indirizzi quali:

anna@?
.@..com

e via dicendo. Ricordo che l'effetto collaterale di questa espressione, dovuto alla presenza delle parentesi, è la memorizzazione delle varie parti dell'indirizzo e-mail in $1, $2 e $3.

 Modificatori

Come se classi, metacaratteri, operatori e quantificatori non bastassero a complicare bene le cose, ad aumentare la potenza del pattern matching concorrono i modificatori. Essi possono essere aggiunti dopo lo slash posto alla fine del pattern appunto per "modificare" in qualche modo il comportamento di esso. Nella scorsa puntata abbiamo visto che l'espressione:

/Anna/

ha esito positivo se la variabile analizzata contiene Anna, ma non se essa contiene anna, aNNa oppure AnNa. Per far sì che il matching non sia sensibile alle lettere maiuscole e minuscole possiamo fare uso del modificatore i, e quindi scriviamo:

/Anna/i

Un altro utile modificatore è m, che permette a ^ e $ di trovare anche i newline (\n). Se la nostra espressione è quindi:

$a =~ /^Anna$/m

l'esito è positivo sia in caso $a = "Anna" che $a="\nAnna" che $a="Anna\n" e via dicendo. I modificatori possono anche essere combinati semplicemente scrivendoli l'uno di seguito all'altro in un ordine qualsiasi:

/Anna/mi

Utile è anche s: esso causa che il carattere punto (.), il quale regolarmente trova tutti i caratteri ad eccezione del newline, trovi anche quest'ultimo. L'ultimo modificatore che analizziamo (ve ne sono in realtà altri di uso meno frequente, ed il loro studio è lasciato al lettore che vuole approfondire l'argomento) è g, che fa sì che il matching sia globale: esso non si ferma cioè alla prima occorrenza del pattern ma continua fino a fine stringa, trovandole tutte. Vediamo un esempio del suo utilizzo:

$a = "Ho incontrato Lara, Sara e Mara";
@nomi = $a =~ /[LSM]ara/g;
print @nomi;

Questo programma stampa tutte le occorrenze del pattern trovate (Sara, Lara e Mara), in quanto l'espressione, se usata in contesto di lista, torna in un array tutti i match. L'eventuale uso di parentesi all'interno del pattern fa sì che nella lista venga inserito solo il contenuto di esse e non l'intera stringa trovata. Lo stesso modificatore, in un contesto scalare, consente di effettuare un matching progressivo: utilizzando l'espressione una seconda volta la ricerca partirà dal punto in cui si era interrotta per via del matching precedente. Ad esempio:

$a = "Ho incontrato Lara, Sara e Mara";
while ($a =~ /([LSM]ara)/g) {
  print $1;
}

scriverà sul terminale un output uguale al precedente caso (quando il contesto dell'espressione era una lista), in quanto il while reitera la ricerca progressiva fino a che essa continua ad avere esito positivo.

 Sostituzioni con s///

L'operatore m//, nel quale come abbiamo visto la m può essere omessa e di fatto lo è quasi sempre, non è l'unico messo a disposizione da Perl per quanto riguarda le regular expression. Un altro molto utile è s///, che permette di effettuare sostituzioni all'interno di una stringa, secondo un pattern strutturalmente identico a quelli visti sinora. Vediamo un esempio del suo utilizzo:

$a =~ s/rosa/viola/;

Questa riga di codice farà sì che in $a venga cercata la prima occorrenza di rosa ed essa venga sostituita con viola. Il modificatore g è disponibile anche in questo caso e fa sì che vengano sostituite tutte le occorrenze del pattern nella stringa. Sono possibili sostituzioni molto più compresse quali:

$a =~ "Ho incontrato Lara, Sara e Mara";
$a =~ s/\wara/una ragazza/g;

che modifica $a facendola diventare:

Ho incontrato una ragazza, una ragazza e una ragazza

Si possono anche sfruttare le parentesi e le variabili $n, come nel seguente esempio, che poniamo applicato alla stringa contenuta originariamente in $a:

$a =~ s/(\w)ara/ara$1/g;

La variabile $1 contiene la prima lettera del nome, che viene riutilizzata a destra e posta alla fine di ogni nome anziché all'inizio, trasformando $a in una curiosa:

Ho incontrato araL, araS e araM

Alcuni degli altri modificatori consentiti sono i, m e s: essi hanno la stessa funzione che avevano per l'operatore m//. Un altro molto comodo è e, il quale causa che la parte destra dell'espressione sia considerata linguaggio Perl. Infatti il codice:

$a =~ s/(\d+)/sprintf("%09d", $1)/ge;

sostituisce tutti i numeri trovati in $a con gli stessi formattati a nove cifre, anteponendo degli zeri se il numero dovesse essere più corto. Non abbiamo ancora visto l'utilizzo di sprintf(), che è una funzione che Perl ha ereditato dal C. Per rimuovere dalla stringa in esame dei caratteri senza inserirne altri al loro posto è sufficiente lasciare vuota la parte destra dell'espressione. Ad esempio:

$a =~ s/\r//g;

rimuove tutti i carriage return.

 Traslitterazioni con tr///

Questo operatore analizza una stringa, carattere per carattere, e rimpiazza tutti i caratteri della lista di sinistra con quelli della lista di destra. Esso può anche essere utilizzato come y///, sinonimo creato per gli utilizzatori del programma sed. Chiariamo subito con un esempio:

$a =~ tr/abcd/1234/;

Questo frammento cambia tutte le a in 1, le b in 2 e via dicendo. Naturalmente sarebbe stato in questo caso più comodo scrivere:

$a =~ tr/a-d/1-4/;

É fondamentale notare che né la parte sinistra né la parte destra di tr/// sono regular expression: esse accettano solo i metacaratteri quali \n per l'inserimento di caratteri non scrivibili da tastiera, ma nessun altro operatore o metacarattere proprio delle espressioni regolari. I modificatori possibili sono: c, che fa sì che vengano cercati i caratteri non presenti nella lista a sinistra anziché quelli presenti; d, il quale causa che eventuali caratteri trovati per i quali non c'è alcun rimpiazzo nella lista di destra vengano cancellati; /s, che nel caso vengano trovati più caratteri consecutivi uguali li riduce ad uno solo, rimpiazzandolo con il rispettivo sostituto indicato nella lista di destra.

 Conclusioni

Con questa lezione abbiamo concluso la nostra panoramica sulle regular expression. In realtà esse sono degne di ulteriore approfondimento, in quanto possono far risparmiare molto tempo ad un programmatore, nonché rendere più compatto ed efficiente il codice. Nell'articolo sono state omesse alcune funzionalità la scoperta delle quali è lasciata al lettore, che può affidarsi alla corposa guida in linea fornita con Perl.

  1. Larry Wall, Tom Christiansen, Jon Orwant - "Programming Perl (3rd edition)", O'Reilly & Associates, Luglio 2000