Espressioni regolari: 5 esempi pratici per estrarre facilmente dati o verificare testi

regex: esempi pratici

Estrarre email da un testo, oppure un indirizzo IP. Eliminare le righe vuote e/o le righe duplicate, oppure verificare i numeri della carta di credito. Sono tutti esempi pratici di utilizzo delle espressioni regolari. In un precedente post pubblicato recentemente su EOS, abbiamo visto i concetti che stanno alla base delle espressioni regolari, applicandoli con qualche semplice esempio. Questo articolo completa ed espande il precedente, mostrando appunto come le espressioni regolari possano essere applicate per risolvere alcuni tipici esempi pratici che si possono incontrare programmando in linguaggi di scripting come Python, o PHP.

 

Python

Python, oggi giunto alla versione 3.3.0, è un linguaggio di programmazione liberamente utilizzabile, il cui scopo è quello di semplificare il più possibile la risoluzione dei problemi che si devono affrontare nella quotidiana attività di sviluppo. Il linguaggio fu inizialmente sviluppato da Guido von Rossum nel 1990 e deve il suo nome alla commedia inglese “Monty Python’s Flying Circus”. A partire da quella data, il linguaggio è stato sviluppato da un team numeroso di volontari e appassionati, ed è liberamente scaricabile dal sito della Python Software Foundation (sono disponibili le versioni per i più diffusi sistemi operativi quali Linux, Windows, e Mac OS X). Python è un linguaggio di programmazione per impieghi generici, in grado di processare file di testo, numeri, immagini, dati provenienti da simulazioni in campo scientifico, o qualunque altro tipo di informazione che può essere gestita da un comune computer. Python viene utilizzato ogni giorno per processare i dati elaborati dal motore di ricerca Google, dal sistema di condivisione dei video YouTube, dalla NASA, dalla borsa statunitense (New York Stock Exchange) e da molte altre organizzazione sparse per il mondo. Dal punto di vista prettamente tecnico, Python è un linguaggio dinamico e interpretato. Dinamico significa che i tipi delle variabili utilizzate in un programma non vanno espressamente dichiarati, sarà compito dell’interprete Python sollevare un errore se si cerca di leggere una variabile che non è stata settata in precedenza. Questa caratteristica rende il Python semplice da leggere, riducendo notevolmente le dimensioni dei sorgenti generati.

Fatta questa debita premessa, ci poniamo la domanda: come si gestiscono le espressioni regolari in Python? La risposta è semplice, Python (a partire dalla versione 1.5) gestisce le espressioni regolari tramite un modulo integrato il cui nome (banalmente) è “re”. Ciò siginifica che per utilizzare le regex in un programma Python dovremo anzitutto importare questo modulo, cosa che avviene tramite l’istruzione:

import re

Per scrivere e testare le espressioni regolari in Python, useremo IDLE, la GUI inclusa nel pacchetto di installazione di Python stesso. IDLE è stata scritta interamente in Python tramite l’utilizzo del tkinter GUI toolkit. E’ cross-platform (funziona allo stesso modo su sistemi operativi differenti), e include un debugger e una shell tramite i quali è possibile eseguire direttamente lo script, verificandone i risultati. Vediamo ora di applicare quanto appreso sulle espressioni regolari a cinque casi (generici, ma del tutto simili a possibili situazioni reali). Alcune di queste espressioni regolari saranno implementate con il linguaggio Python, altre con il PHP, e una con l’editor di testo Notepad++ (nel primo post della serie abbiamo visto che questo editor supporta egregiamente le espressioni regolari).

1. Ricerca di un indirizzo IP

In questo primo esempio, ci poniamo l’obiettivo di ricercare (ed estrarre) tutti gli indirizzi IP validi contenuti all’interno di un testo. Nella nostra analisi, faremo riferimento agli indirizzi IP tradizionali, cioè quelli espressi nel formato IPv4. Un indirizzo IP di questo tipo è composto da quattro byte, separati tra loro da un carattere punto (‘.’). Il formato è quindi il seguente:

xxx.xxx.xxx.xxx

dove ogni byte xxx può assumere i valori compresi tra 0 e 255. Indirizzi IP validi sono perciò i seguenti: 192.168.1.0, 150.43.2.127, 255.255.0.255. Non rappresentano invece indirizzi IP validi le seguenti sequenze di caratteri: 192.168.00.1, 256.9.1.2, 1234.2.3.4, e così via. Detto questo, se dovessimo scrivere l’espressione regolare per cercare e validare un indirizzo IP, ci verrebbe spontaneo, inizialmente, scrivere qualcosa del tipo:

[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}

In sostanza, vogliamo cercare le sequenze composte da un minimo di 1 cifra fino a un massimo di 3 cifre ([0-9]{1,3}) seguite da un carattere punto (il backslash è necessario, perche il carattere ‘.’, altrimenti, significherebbe qualunque carattere). Già fatto? Cominciamo a verificare questa regex eseguendo il seguente script in IDLE:

import re

str='192.168.1.0'

ipadds=re.findall(r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}', str)

for a in ipadds:
    print(a)

Premiamo il tasto F5 (Run Module) per verificare i risultati prodotti. l’output sarà qualcosa di simile:

Lo script sembra quindi aver funzionato, nel senso che ha estratto un indirizzo IP valido (l’unico) da una stringa. La lettura dello script è immediata. Abbiamo anzitutto un’istruzione per importare il modulo re, poi la definizione di una stringa (in pratica un testo, che potrebbe anche essere letto da un file). Segue quindi la chiamata a una funzione del modulo re (findall) il cui compito è quello di produrre una lista con tutte le occorrenze (indirizzi IP) che “matchano” il pattern (regex) specificato. La scansione della stringa viene eseguita da sinistra verso destra, e i valori che matchano sono prodotti secondo questo ordine. Infine, abbiamo un ciclo for che scandisce tutte le stringhe trovate e le visualizza a video. Siamo sicuri che lo script funzioni con tutti i pattern riconoscendo solo quelli validi? Diamo un’occhiata allo stesso script, eseguito però con un set diverso di potenziali indirizzi IP:

import re

str='192.168.1.0 123.2.345.2 255.255.255.255 300.123.24.2 1.2.3.00 1234.1.2.3'

ipadds=re.findall(r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}', str)

for a in ipadds:
    print(a)

Il risultato è il seguente:

Lo script, quindi, non è proprio a “prova di bomba”, anzi, presenta diverse falle tali per cui vengono creati dei falsi positivi (vengono cioè ritenuti come validi degli indirizzi che in realtà non lo sono). Come correggere allora lo script? L’approccio consigliato è quello di ragionare prima, scrivendo sulla carta un modello o simulazione dell’algoritmo, e solo dopo cominciare a scrivere il codice. Sappiamo che ogni byte può assumere i valori da 0 a 255, e perciò possiamo individuare i seguenti casi:

  • numero composto da 1 sola cifra (0-9). Lo possiamo rappresentare nella regex con [0-9]
  • numero composto da 2 cifre (10-99). lo possiamo rappresentare nella regex con [1-9][0-9]
  • numero composto da 3 cifre. Questo caso è più complesso, e va scomposto in 3 sottocasi:
    1. numero compreso tra 100 e 199: lo possiamo rappresentare nella regex con 1[0-9]{2}
    2. numero compreso tra 200 e 249: lo possiamo rappresentare nella regex con 2[0-4][0-9]
    3. numero compreso tra 250 e 255: lo possiamo rappresentare nella regex con 25[0-5]

Certo, l’espressione regolare risultante sarà più complessa della precedente, però ora sappiamo come funziona, e saremo perciò anche in grado di modificarla se necessario (oltre alla soddisfazione di vederla funzionare). Mettiamo allora tutto assieme, ottenendo lo script Python seguente:

# import del modulo regular expression
import re

# stringa di caratteri da cui estrarre gli indirizzi IP validi
str = '192.168.1.0 1234.1.2.3 192.168.00.1 1.2.3.0'

# estrae tutti gli indirizzi (sequenze di caratteri) che "matchano" l'espressione regolare
indirizzi_IP = re.findall(r'\b(?:(?:2[0-4][0-9]|25[0-5]|1[0-9]{2}|[1-9][0-9]|[0-9])\.){3}\
(?:2[0-4][0-9]|25[0-5]|1[0-9]{2}|[1-9][0-9]|[0-9])\b', str)

# visualizza a video il risultato della ricerca
for indirizzo in indirizzi_IP:
    print (indirizzo)  # NB: Python 3.3 richiede le parentesi per la funzione print

L’output prodotto dalla sua esecuzione sarà il seguente:

Occorre a questo punto fare la seguente osservazione sullo script precedente. Essa riguarda la lettera ‘r’ che precede l’argomento stringa parametro della chiamata a findall. Il suo significato è quello di trattare la stringa medesima nel formato “raw”, cioè di non interpretare le sequenze di escape (caratteri preceduti dal backslash ‘\’), in modo tale che esse possano essere processate dall’engine dell’espressione regolare come fossero dei normali caratteri.

2. Ricerca di un indirizzo email

In questo secondo esempio, ci poniamo l’obiettivo di ricercare (ed estrarre) tutti gli indirizzi email validi contenuti all’interno di un testo. Occorre però fare un’importante precisazione al proposito. Il formato generico di un indirizzo email è il seguente: local-part@domain, dove local-part può avere una lunghezza fino a 64 caratteri, mentre domain name una lunghezza fino a 253 caratteri (la lunghezza complessiva dell’indirizzo email non può comunque superare i 254 caratteri). Il formato effettivo di un indirizzo email è specificato nei documenti RFC 5322 (sezioni 3.2.3 e 3.4.1) e successivi. Il formato non è di per sè semplicissimo da esprimere attraverso una espressione regolare, nel senso che esistono moltissimi casi particolari che, se gestiti, comportano un aumento di complessità e una conseguente perdita di leggibilità dell’espressione regolare stessa. Inoltre, un eccesso di completezza in questo caso potrebbe non essere giustificabile, in quanto la maggiorparte degli indirizzi email utilizzati a livello pratico hanno una forma standard. Meglio quindi allinearsi in quest’ottica, cercando di scrivere un’espressione regolare compatta e leggibile, accontentandosi di ricoprire una casistica di indirizzi tendente al 99% (è proprio quell’1% che, se gestito, farebbe aumentare pesantemente la complessità della regex). Se poi ci si accorge che qualche indirizzo email non viene accettato, si può sempre porre rimedio e modificare opportunamente la regex. Lo script in Python è il seguente:

# import del modulo regular expression
import re

# stringa di caratteri da cui estrarre gli indirizzi email validi
str = 'renna@babbonatale.com numero-9@squadra_calcetto.it Bob.Mellow@abc.com \
come-mi_chia.mo@myplace.it'

# estrae tutti gli indirizzi email (sequenze di caratteri) che "matchano" l'espressione \
regolare
indirizzi_email = re.findall(r'\b[a-z0-9-+_.%]+@[a-z0-9-_.]+\.[a-z]{2,4}\b', str, re.I)

# visualizza a video il risultato della ricerca
for indirizzo in indirizzi_email:
    print (indirizzo)  # NB: Python 3.3 richiede le parentesi per la funzione print

La prima parte dell’indirizzo (quella che precede il carattere ‘@’) può comprendere qualunque lettera dell’alfabeto o cifra decimale, e ciascuna di esse può essere ripetuta, quindi possiamo rappresentare ciò con l’espressione: [a-z0-9]+. Sappiamo però dalla pratica che possiamo inserire altri caratteri ugualmente validi in questa parte, come ad esempio il ‘.’ (molto utilizzato, ad esempio, per separare il nome dal cognome), il trattino (‘-‘), il carattere di sottolineatura (‘_’), e lo stesso dicasi per i caratteri ‘%’ e ‘+’. Si ottiene quindi la prima parte della regex come indicato nello script ([a-z0-9-+_.%]+). Questa parte deve essere necessariamente seguita dall’AT (‘@’), e da una seconda parte che può contenere (quasi) gli stessi tipi di carattere della prima. Abbiamo così aggiunto la seconda parte di regex (@[a-z0-9-_.]+). Siamo così giunti alla terza e ultima parte dell’indirizzo email, quella che inizia con il carattere ‘.’. Quest’ultima può comprendere, nella nostra rappresentazione semplificata ma molto efficace, da 2 a 4 caratteri alfabetici (ad esempio ‘it’, oppure ‘com’, ‘uk’, ‘org’, e così via). Il pezzo di espressione regolare corrispondente sarà pertanto: \.[a-z]{2,4}. Componendo tutte le parti di espressione regolare, si ottiene quanto riportato nello script. Un paio di osservazioni. La prima è che la regex viene inclusa tra i caratteri ‘\b’. Questi, in ambito regex, hanno il significato di “word boundary”: servono quindi a delimitare una parola (intesa come sequenza di caratteri) in modo tale, ad esempio, di non “matchare” l’espressione regolare a metà di una parola. La seconda è che non abbiamo fatto distinzione tra minuscole e maiuscole. Negli indirizzi email, tuttavia, si possono utilizzare caratteri alfabetici sia minuscoli che maiuscoli. Dov’è allora l’inghippo? Semplicemente, abbiamo utilizzato una “flag” (opzione) applicata alla chiamata della funzione findall, in modo tale da rendere il match case-insensitive. L’opzione è re.I, dove I sta per IGNORECASE. Questo è il motivo per cui nella regex abbiamo identificato i caratteri dell’alfabeto con a-z. Potevamo usare anche A-Z con lo stesso risultato, ma solo perchè è presente la flag re.I. In caso contrario, le due notazione sarebbero state differenti. L’output prodotto dallo script è il seguente:

3. Eliminazione da un file di tutte le linee blank

Supponiamo di avere a che fare con un file di testo, e di voler eliminare da esso tutte e sole le linee vuote (blank lines, in inglese). Oltre che un utile esercizio di applicazione delle regex, questo esempio può anche rappresentare una necessità a livello pratico, e da essa si potrebbero poi derivare situazioni simili, come ad esempio eliminare tutte le righe di commento, oppure contare le linee blank o di commento, e così via. L’espressione regolare che “matcha” una linea vuota è molto semplice, ed è semplicemente espressa dalla forma: ‘^$’. Come già sappiamo, il carattere caret (‘^’) individua l’inizio di una riga, mentre il carattere dollaro (‘$’) la fine di una riga. Il fatto che tra questi due caratteri non ve ne sia alcun altro, permette proprio di selezionare tutte e sole le linee blank. Possiamo a questo punto dare forma a questa espressione regolare calandola in uno script completo in Python che esegue tutte le necessarie operazioni:

# import del modulo regular expression
import re

# apertura, lettura, e chiusura del file di testo
fd = open('testo.txt', "r")
contenuto = fd.readlines()
fd.close()

# crea un file di output contenente tutte e sole le linee non blank, visualizzandole \
anche a video
fd = open('testo_output.txt', "w")
for linea in contenuto:
    result= re.match('^$', linea)
    if not result:
        fd.writelines(linea)
        print(linea.rstrip('\n'))
fd.close()

La prima parte dello script apre, legge il contenuto, e chiude il file di testo (chiamato ‘testo.txt’, ma ovviamente potete scegliere un altro nome). Il file viene aperto in sola lettura. Nella seconda parte dello script viene aperto in scrittura un file di output (‘testo_output.txt’) nel quale verranno scritte tutte le linee non blank. Per fare ciò, l’espressione regolare verrà applicata (tramite la chiamata alla funzione match) a tutte le linee del file di input: quelle che non “matchano” la regex (le linee cioè non blank) verranno scritte nel file di output, le altre no. Ad esempio, copiamo nel file ‘testo.txt’ proprio lo script di cui sopra, e poi eseguiamo lo script Python in IDLE. Il risultato è mostrato nell’immagine seguente (il file di output prodotto, ‘testo_output.txt’, è ovviamente analogo).

4. Eliminazione da un file di tutte le linee duplicate

Come ulteriore applicazione, supponiamo ora di voler eliminare da un file di testo tutte le linee duplicate, cioè le linee tra loro identiche. Per applicare questo esempio supponiamo anzitutto che il contenuto del file venga ordinato, e che si voglia mantenere solo un’istanza di ogni linea, eliminando quindi i doppioni. Ad esempio, si consideri il seguente file:

10
11
12
13
14
14
15
16
17
18
19
19
20
20
21
22
23
23
23
24
24
25
25
25

In questo caso l’operazione di eliminazione delle entry multiple (2 o più linee identiche) può essere agevolmente eseguita da un editor in grado di supportare le espressioni regolari (ad esempio vi, vim, emacs, o Notepad++). La nostra scelta è ricaduta proprio su quest’ultimo, ma il discorso è generale, per cui si può applicare analogamente agli altri editor. L’approccio che si segue in questo caso è quello di trovare le occorrenze di linee identiche, e poi sostituirle con una e una sola occorrenza; in pratica si esegue un search and replace basato sulle regex. L’espressione regolare da utilizzare per la ricerca è la seguente:

^(.*)(\r?\n\1)+$

Vediamo anzitutto che la regex è delimitata dai consueti caratteri di inizio linea (‘^’) e fine linea (‘$’). Abbiamo poi un primo gruppo di caratteri, (.*), il cui significato è: prendi tutti i caratteri sulla linea e memorizzane il contenuto in un ‘backreference’. Il backreference è una sorta di variabile temporanea, e viene definita racchiudendo i caratteri (.* in questo caso) tra una coppia di parentesi tonde. Abbiamo poi i caratteri \r?\n. \r equivale al carriage return, mentre \n al new line. In Windows il fine riga è delimitato da entrambi i caratteri (il cosiddetto linefeed+carriage return), ma in Unix esiste solo il new line. Per generalità, la regex accetta come fine riga 0 oppure 1 occorrenza di \r (0 in Unix, 1 in Windows), ma sempre 1 occorrenza di \n (in entrambi i sistemi operativi). Seguono i caratteri \1: ciò significa che siamo interessati a “matchare”, dopo la fine della riga corrente, esattamente i caratteri memorizzati precedentemente nella backreference. Quindi con le parentesi () si è creata la backreference, e con la notazione \1 (\2, \3, ecc. se ve ne fossero più di una) si richiama la backreference, cioè il suo contenuto. Il carattere + successivo dice che accettiamo più occorrenze della stessa linea, non soltanto due (ad esempio 3, 4, o più linee consecutive identiche). Apriamo quindi con Notepad++ il file mostrato precedentemente, e attiviamo la funzione di search and replace, come indicato nell’immagine seguente:

Nella casella “Replace With” inseriamo \1, intendendo con ciò che desideriamo sostituire ciascun gruppo di linee duplicate con una sola occorrenza di linea (il backreference). Il risultato, come atteso, è il seguente:

5. Controllo numero carta di credito

Supponiamo ora che si voglia controllare se un numero di carta di credito è valido oppure no. Attenzione, non si vuole controllare se la carta associata al numero è valida, ma soltanto il numero stesso. Quest’ultimo avrà tipicamente una forma di questo tipo: xxxx-yyyy-zzzz-wwww dove x,y,z,w sono cifre decimali (0-9). Per implementare questo script utilizzeremo il PHP, vale a dire l’arcinoto linguaggio di scripting utilizzato ampiamente nel mondo della programmazione web (insieme al DBMS MySql e al web server Apache forma una “triade” che muove migliaia di siti, molti per attività commerciali, sul web). Un modo molto semplice per utilizzare le regex all’interno di uno script PHP è rappresentato dalle funzioni con il prefisso preg (preg_match, preg_match_all, preg_grep, preg_replace, ecc.), che in sostanza formano un wrapper sulla libreria PCRE (Perl Compatible Regular Expression). Uno script in grado di eseguire il controllo di validità (in questo caso si è utilizzata la funzione preg_match di PHP) è il seguente:

<?php

$num_cc = "4321-3333-1464-8703";
$result = preg_match("/^(\d{4}[\s\-]?){4}$/", $num_cc);

if ($result) {
  echo "Il numero della carta è valido";
  echo "<br />";
}

?>

Anzitutto, l’espressione regolare viene racchiusa tra due slash (‘/’), uno all’inizio e uno alla fine della stessa, come richiesto dalla forma di regex utilizzata in PHP (quindi si tratta di una regola generale). Vediamo poi che si vogliono matchare anzitutto 4 caratteri numerici posti all’inizio della linea di testo da processare: ^(\d{4}. Questo gruppo di 4 cifre può poi essere seguito da 0 o più spazi (\s) o trattini (-). Il gruppo risultante che si ottiene (4 cifre seguite da spazio o trattino) deve poi essere presente in totale 4 volte prima di incontrare la fine della linea da processare ({4}$). Vediamo così che, anche cambiando il linguaggio di scripting, la sostanza delle regex è sempre la stessa, fatta eccezione al limite per qualche piccola differenza sintattica. Il succo, comunque, è lo stesso.

Conclusioni

Le espressioni regolari sono un utile strumento per eseguire ricerche, sostituzioni, e validazioni di dati di vario tipo, da utilizzarsi in applicazioni destinate sia al mondo web che non (ricordiamo che anche il mondo .NET permette di gestire agevolmente delle regex all’interno di codice scritto in C#, C++, o un altro linguaggio supportato da .NET stesso). Le regex sono potenzialmente una “bestia nera”, vuoi perchè non sono sempre semplici da interpretare, vuoi perchè non sono altrettanto agevoli da debuggare. Con un pò di esercizio, comunque, qualunque sviluppatore è in grado di scrivere delle semplici espressioni regolari, e apprezzarne l’utilità. Alcuni linguaggi, come ad esempio Python, permettono inoltre di “compilare” le regex, in modo tale da aumentare l’efficienza del codice eseguito, in termini di prestazioni e tempo macchina utilizzato. Concludiamo ricordando che, come “rule of thumb” generale, è bene che una regex non sia troppo complessa o troppo lunga. Se ciò avviene, conviene fermarsi un attimo a riflettere e verificare se esiste una soluzione più semplice e lineare (nessuno, soprattutto gli ingegneri, ama complicarsi troppo la vita!)

 

Quello che hai appena letto è un Articolo Premium reso disponibile affinché potessi valutare la qualità dei nostri contenuti!

 

Gli Articoli Tecnici Premium sono infatti riservati agli abbonati e vengono raccolti mensilmente nella nostra rivista digitale EOS-Book in PDF, ePub e mobi.
volantino eos-book1
Vorresti accedere a tutti gli altri Articoli Premium e fare il download degli EOS-Book? Allora valuta la possibilità di sottoscrivere un abbonamento a partire da € 2,95!
Scopri di più

3 Comments

  1. Piero Boccadoro Piero Boccadoro 15 gennaio 2013
  2. Emanuele Emanuele 15 gennaio 2013
  3. Boris L. 21 gennaio 2013

Leave a Reply