5.9. Introduzione alle espressioni regolari

Le espressioni regolari sono un strumento potente (e piuttosto standardizzato) per cercare, sostituire o analizzare del testo contenente complessi schemi di caratteri. Se avete già usato espressioni regolari in altri linguaggi (come il Perl), potete saltare questa sezione e leggere direttamente il sommario del modulo re per avere una panoramica delle funzioni disponibili e dei loro argomenti.

I tipi stringa hanno i propri metodi di ricerca (index, find e count), di sostituzione (replace) e di analisi (split), ma questi sono limitati ai casi più semplici di utilizzo. Il metodi di ricerca operano con una singola sotto-stringa dal valore predefinito e sono sempre sensibili alla differenza tra maiuscole e minuscole; per effettuare ricerche in una stringa s, volendo ignorare tale differenza, occorre chiamare i metodi s.lower() o s.upper() ed essere sicuri che la stringa da cercare abbia lo stesso tipo di carattere (maiuscolo/minuscolo) di s. I metodi replace e split hanno le stesse limitazioni. Laddove è possibile conviene usarli (sono veloci e leggibili), ma per cose più complesse è necessario passare alle espressioni regolari.

Esempio 5.19. Cercare corrispondenze alla fine di una stringa

Questa serie di esempi sono ispirati da un problema reale che ho avuto durante il mio lavoro ufficiale, dove dovevo adattare e standardizzare indirizzi stradali estratti da un sistema precedente, prima di inserirli in un nuovo sistema (come vedete non mi invento niente: queste cose sono utili realmente).

>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')               1
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')               2
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.') 3
'100 NORTH BROAD RD.'
>>> import re                              4
>>> re.sub('ROAD$', 'RD.', s)              5 6
'100 NORTH BROAD RD.'
1 Il mio obiettivo è di standardizzare un indirizzo stradale in modo tale che 'ROAD' sia sempre abbreviato come 'RD.'. A primo acchito, avevo considerato la cosa abbastanza semplice da poter usare il metodo replace. Dopo tutto, i dati erano già tutti in lettere maiuscole, in modo che la differenza tra maiuscolo e minuscolo non sarebbe stata un problema. E la stringa di ricerca, 'ROAD', era una costante. In effetti, in questo caso ingannevolmente semplice, s.replace funziona come ci si aspetta.
2 La vita, sfortunatamente, è piena di controesempi, e scoprii presto quanto ciò sia vero. Il problema qui è che 'ROAD' appare due volte nell'indirizzo, una volta come parte del nome della strada, 'BROAD', ed una volta come parola a sé stante. Il metodo replace trova le due occorrenze e ciecamente le sostituisce entrambe; in questo modo, il mio indirizzo viene distrutto.
3 Per risolvere il problema di indirizzi con più di una sottostringa 'ROAD', potremmo ricorrere a qualcosa del genere: cercare e sostituire 'ROAD' solo negli ultimi 4 caratteri dell'indirizzo (s[-4:]), e lasciare intatto il resto della stringa (s[-4:]). Ma potete vedere come la cosa stia già diventando poco gestibile. Per esempio, questo schema dipende dalla lunghezza della stringa che vogliamo sostituire (se volessimo sostituire 'STREET' con 'ST.', dovremmo usare s[:-6] e s[-6:].replace(...)). Vi piacerebbe ritornare su questo codice dopo sei mesi per cercarvi un “baco”?. Io so che preferirei evitarlo.
4 È tempo di passare alle espressioni regolari. In Python, tutte le funzioni collegate alle espressioni regolari sono contenute nel modulo re.
5 Date un'occhiata al primo parametro 'ROAD$'. Si tratta di una espressione regolare molto semplice, che corrisponde a 'ROAD' solo quando questa sottostringa si trova alla fine. Il carattere $ significa “fine della stringa”. Esiste un carattere corrispondente, il carattere ^, che significa “inizio della stringa”.
6 Usando la funzione re.sub, si cerca nella stringa s per sottostringhe corrispondenti all'espressione regolare 'ROAD$' e le si rimpiazza con 'RD.'. In questo modo si rimpiazza la sottostringa ROAD alla fine della stringa s, ma non quella che è parte della parola BROAD, perché questa è in mezzo alla stringa s.

Esempio 5.20. Cercare la corrispondenza con parole complete

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)     1
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)  2
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)  3
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)  4
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s) 5
'100 BROAD RD. APT 3'
1 Continuando con la mia storia degli indirizzi da aggiustare, scoprii presto che l'esempio precedente, in cui cercavo corrispondenze con 'ROAD' alla fine della stringa, non era abbastanza efficiente perché non tutti gli indirizzi includevano una designazione del tipo di strada; alcuni terminavano con il solo nome della strada. Il più delle volte, questo mi andava bene, ma se il nome della strada era 'BROAD', allora l'espressione regolare avrebbe corrisposto con 'ROAD' alla fine della stringa, anche se questi era parte della parola 'BROAD', e questo non era quello che volevo.
2 Quello che realmente volevo era di trovare corrispondenze con 'ROAD' quando si trovava alla fine della stringa ed era una parola a se stante, non una parte di una parola più grande. Per esprimere questo con una espressione regolare, occorre usare il codice \b, che significa “in questo punto ci deve essere un limite alla parola”. In Python, l'uso di questo codice è complicato dal fatto che per inserire il carattere '\' in una stringa, esso deve essere preceduto da un altro carattere '\' (“escaped” ndt). A questo fatto si fa qualche volta riferimento come alla “piaga del backslash” (backslash == barra inversa, ndt), e questa è una delle ragioni per cui le espressioni regolari sono più facili in Perl che in Python. D'altra parte, Perl mischia le espressioni regolari con altre notazioni sintattiche, per cui se c'è un “baco” può essere difficile stabilire se esso sia nell'espressione regolare o negli altri componenti sintattici.
3 Per evitare la piaga del backslash, si possono usare le cosiddette stringe grezze, facendo precedere il '...' con la lettera r (raw cioè grezzo, ndt). Questo dice a Python che niente nella stringa deve essere interpretato come carattere speciale. La stringa '\t' indica di solito il carattere di tabulazione, ma la stringa r'\t' è proprio il carattere barra inversa \ seguito dalla lettera t. Io di solito raccomando di usare sempre stringe grezze quando si ha a che fare con le espressioni regolari, altrimenti le cose diventano subito troppo confuse ( e le espressioni regolari già di per se fanno presto a diventare poco chiare).
4 *sigh* Sfortunatamente, scoprii presto che molti casi concreti contraddicevano la mia logica. In questo caso, l'indirizzo conteneva la parola 'ROAD' come parola a se stante, ma non era alla fine, perché l'indirizzo includeva il numero di un appartamento dopo la specifica della strada. Dato che 'ROAD' non è alla fine della stringa, non corrisponde, e quindi la chiamata a re.sub finisce per non sostituire proprio niente, e noi ci ritroviamo con la stringa originale, che non è quello che volevamo.
5 Per risolvere questo problema, ho rimosso il carattere $ ed ho aggiunto un'altro \b. Ora l'espressione regolare si legge: “corrisponde a 'ROAD' quando è una parola a se stante in qualunque punto della stringa”, sia all'inizio che alla fine che da qualche parte nel mezzo.

Questo rappresenta appena la punta estrema dell'iceberg rispetto a quello che le espressioni regolari possono fare. Si tratta di uno strumento estremamente potente e vi sono interi libri a loro dedicate. Non sono però la soluzione migliore per ogni problema. È importante saperne abbastanza da capire quando sono adeguate e quando invece possono causare più problemi di quanti ne risolvano.

 

Alcune persone, quando affrontano un problema, pensano: “Lo so, finirò per usare le espressioni regolari”. A questo punto, hanno due problemi.

 
-- Jamie Zawinski, in comp.lang.emacs  

Ulteriori letture