Introduzione alle Espressioni Regolari

Le espressioni regolari sono una funzionalità che permette di ottenere risultati stupefacenti attraverso la ricerca di corrispondenze di testo (``pattern matching''). Il modulo re implementa la gestione efficiente delle espressioni regolari in stile Perl all'interno di Python.

La prima cosa da fare è importare il modulo che le implementa:

>>> import re
Esso permette di "compilare" le nostre espressioni regolari in oggetti istanza, i cui metodi principali sono search e match.

Tra l'altro a partire dalla versione 1.6 c'è un nuovo motore per le espressioni regolari: si tratta di SRE di F.Lundh, che comunque è compatibile all'indietro col vecchio motore (di cui si parla nel Regular Expression HOWTO).

Il nuovo motore permette le ricerche su stringhe Unicode oltre che su quelle tradizionali a 8 bit, questo crea delle complicazioni aggiuntive che comunque eviterò, visto anche che attualmente sto usando Python 1.6.

Si raccomanda in ogni caso di consultare la Library Reference, che fornisce l'interfaccia ufficiale.

Sintassi delle Espressioni Regolari

Come si costruisce un'espressione regolare?

Le espressioni regolari operano sulle stringhe, e le stringhe più semplici sono costituite da singoli caratteri. La maggior parte dei caratteri corrisponde semplicemente a se stessa, quindi a corrisponderà alla stringa "a" (circa la discriminazione tra maiuscolo e minuscolo si veda la sezione opzioni più avanti), stessa cosa per una stringa costituita da caratteri ordinari, come a esempio spam.

Alcuni caratteri assumono invece significati particolari e vengono chiamati metacaratteri. Laddove si voglia vengano interpretati come caratteri ordinari dovranno essere protetti. Eccone una lista completa:

[ \ { | ( )  ^ $ ? + * .

I metacaratteri

I metacaratteri costituiscono uno dei fondamenti della potenza delle espressioni regolari, da qui in avanti indicate come RE.

Insiemi di caratteri specificati tramite [ e ]

Un esempio:
[abc]
corrisponderà a uno qualsiasi dei caratteri tra [ e ], l'intervallo completo tra due caratteri può essere abbreviato usando -, quindi l'esempio sopra è equivalente a:
[a-c]
È anche possibile indicare un carattere esterno a un intervallo ("complementazione"), usando il metacarattere ^ (`caret'), per esempio a
[^c]
corrisponderà qualunque carattere eccetto "c", ma a
[^]
corrisponder` semplicemente "^". Infatti eventuali metacaratteri compresi tra [ e ] verranno presi alla lettera, quindi
[]]
corrisponderà a "]".

Le RE adiacenti vengono concatenate, per cui se A e B sono due espressioni regolari, AB è un'espressione regolare. Se una stringa p corrisponde ad A e un'altra stringa q a B, la stringa pq corrisponderà ad AB.

Possiamo costruire insiemi di caratteri concatenando intervalli, a esempio

[a-zA-Z]
corrisponderà a un qualunque carattere alfabetico (per problemi di localizzazione si veda la sezione opzioni).

Il metacarattere \ e relativi problemi

Intervalli come quello sopra possono essere specificati tramite particolari sequenze che corrispondono a classi di caratteri, eccone alcune:

\d
corrisponde a una qualsiasi cifra decimale; è equivalente a [0-9].
\D
corrisponde a un qualsiasi carattere che non sia una cifra; è equivalente a [0-9].
\s
corrisponde a uno spazio bianco; è equivalente a [ \t\n\r\f\v]. (Lo spazio bianco in testa è necessario! \t corrisponde a uno stop di tabulazione, \n a un `newline' ecc.).
\S
corrisponde a un qualsiasi carattere che non sia uno spazio bianco; è equivalente a [^ \t\n\r\f\v].
\w
corrisponde a un qualsiasi carattere alfanumerico; equivale a: [a-zA-Z0-9_].
\W
corrisponde a un qualsiasi carattere non alfanumerico; equivale a: [â-zA-Z0-9_].
(Questa trattazione è volutamente semplificata visto il carattere introduttivo. Si veda, qui e altrove, la Library Reference e il Regular Expression HOWTO, tenendo conto che quest'ultimo si riferisce alla versione pre del motore).

Oltre che a segnalare tali sequenze, `\' svolge la funzione di proteggere (il noto `escape') caratteri speciali quali a es.`*', facendo sì che vengano interpretati come caratteri normali.

Purtroppo questo può essere fonte di problemi; non si dimentichi infatti che `\' è usato in Python quale carattere di escape nelle stringhe letterali!

Vogliamo ad esempio una RE che corrisponda alla stringa "\index" che s'incontra in file sorgenti .tex . È necessario proteggere la barra obliqua inversa con una seconda barra, quindi si ottiene "\\index", che è la stringa che dev'essere passata a compile(). Per esprimerla come stringa letterale tocca però proteggere nuovamente le barre oblique inverse, per cui si ottiene un folle "\\\\index". Quindi per esprimere un `\' letterale ne occorrono ben quattro.

("Letterale" in Python è un elemento di programma, una notazione per valori costanti di alcuni tipi built-in. Può essere un numero o una stringa, visto che i caratteri sono stringhe di lunghezza unitaria).

Per evitare tale moltiplicazione delle barre oblique inverse, nei pattern complessi si può usare la notazione raw per le stringhe. Prefissando a una stringa letterale una r si otterrà che i caratteri speciali non vengano gestiti in modi particolari. A esempio la nostra \index sarà scritta come stringa raw r"\\index"

Altri metacaratteri

.
corrisponde a un qualsiasi carattere eccetto un newline. Affinché corrisponda anche a un newline bisogna abilitare l'opzione DOTALL, si veda la sezione sulle opzioni.

^
(`caret') corrisponde all'inizio della stringa, nel modo MULTILINE significa anche "subito dopo un newline". Inoltre serve per specificare un intervallo di caratteri tramite "complemento", come riportato sopra.

$
corrisponde alla fine della stringa, nel modo MULTILINE significa anche "subito prima di un newline"; banana corrisponde sia a "banana" che a "bananasplit", "bananarama" ecc. mentre banana$ corrisponde solo a "banana".

Ripetizioni

Devono naturalmente essere gestite anche le particolari corrispondenze in cui tutta o parte della RE si ripetono.

Il metacarattere `*' specifica che la RE che lo precede (può essere un singolo carattere, ma a esempio anche un intervallo [a-z]) può ripetersi zero o più volte, quindi a esempio pip*o trova corrispondenza in "pio" (zero "p"), "pipo", "pippo", "pipppo" e così via.

Simile è il comportamento di `+', che specifica che la RE che lo precede può ripetersi una o più volte (attenzione a tale differenza!). Quindi pip+o troverà corrispondenza in "pipo", "pippo", "pipppo" e così via, ma non in "pio" (al minimo una "p").

Più ristretto è il comportamento di `?', che specifica che la RE che lo precede può ripetersi zero o una volte. Quindi la nostra pip?o troverà corrispondenza solo in "pio" o "pipo".

Resta il modificatore più complesso tra quelli dedicati alle ripetizioni: {m,n}, con m e n interi decimali. Esso specifica che della RE che lo precede vanno ricercate un numero di corrispondenze compreso tra m e n, il massimo possibile. n può essere omesso, in tal caso viene preso un limite superiore talmente alto da poter essere considerato infinito agli effetti pratici. A esempio ar{1,3}gh può corrispondere ad "argh", "arrgh" e "arrrgh", ma non ad "agh" o "arrrrgh".

Si faccia attenzione a un aspetto: i modificatori `*', `+' e `{m,n}' cercheranno di far corrispondere il massimo numero di caratteri possibile.

Di `{m,n}' esiste una versione "minimalista", `{m,n}?' che specifica la corrispondenza di un numero di RE sempre compreso tra m e n, ma questa volta il minimo possibile.

Si noti che {0,} equivale a `*', {1,} a `+' e {0,1} è la stessa cosa di `?'. Il loro uso è semplicemente una questione di concisione e facilità di lettura.

Come viene effettuato il confronto

Riportando un esempio del Regular Expression HOWTO, cerchiamo di fornire una spiegazione pratica del funzionamento del "motore di confronto" (``matching engine'').

Prendiamo l'espressione a[bcd]*b. Corrisponde alle stringhe costituite da "a", zero o più caratteri compresi in [bcd] e che terminano con una "b". Ora immaginiamo di confrontare tale RE con la stringa "abcbd".

PassoConfrontoSpiegazione
1 a La a presente in testa alla RE corrisponde.
2 abcbd Il motore cerca il massimo numero di corrispondenze di [bcd]*, arrivando quindi alla fine della stringa.
3 Fallimento! Il motore cerca di trovare corrispondenza a b, ma la posizione attuale è alla fine della stringa, quindi fallisce.
4 abcb Torna indietro, in modo da far corrispondere a [bcd]* un carattere in meno.
5 Fallimento! Tenta di nuovo con b, ma la posizione attuale è all'ultimo carattere della stringa, che è una "d".
6 abc Di nuovo indietro, adesso [bcd]* viene fatto corrispondere solamente a "bc".
6 abcb Ora la b in coda alla RE trova corrispondenza nel carattere corrente, il penultimo, quindi il confronto ha successo.

Il risultato dell'operazione di confronto è "abcb". In pratica il motore ha cercato di trovare la corrispondenza più estesa possibile, arrivando più lontano possibile all'inizio e poi arretrando e riprovando con sezioni più piccole. Nel caso si fosse trovato a fallire il confronto con zero occorrenze di caratteri compresi in [bcd] (visto l'uso di `*'), avrebbe concluso che non c'è corrispondenza con la RE.

Nella prossima parte ci occuperemo della compilazione delle nostre RE e dei metodi degli oggetti ottenuti.