Compago

...free knowledge

 
  • Increase font size
  • Default font size
  • Decrease font size
Home Manuali Programmazione Esempi pratici di espressioni regolari

Esempi pratici di espressioni regolari

E-mail Stampa PDF

Le espressioni regolari sono una componente fondamentale nei vari linguaggi di programmazione, dato che il loro uso agevola e semplifica il lavoro dei progrmmatori, evitando loro di dover implementare degli algoritmi personalizzati per ogni contesto.

In questo articolo troverete alcuni esempi pratici di espressioni regolari, che in teoria dovrebbero servire anche a capire meglio alcuni meccanismi e alcuni comandi.


Trovare due righe consecutive uguali ed eliminarne una

Questa procedura è molto utile per ripulire un file di testo da ripetizioni inutili. L'espressione regolare sarà:

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

ed eventualmente occorre sostituire quanto trovato con il primo gruppo

\1

Quindi l'espressione troverà un riscontro per tutti i caratteri fino a trovare il separatore di linea.Il funzionamento è semplice, il simbolo "^" corrisponderà solo all'inizio di una linea.
Se il backreference sulla riga successiva non viene trovato l'espressione regolare darà esito negativo e passerà alla riga successiva.Il tutto fino a questo punto tutta la riga viene memorizzata in un gruppo (il gruppo 1),e per verificare se la riga si ripete questa combinazione, l'espressione regolare è seguita da \1 , che  è il primo backreference al primo gruppo che conserva la linea che abbiamo trovato.Il separatore di riga interrogativo potrà essere sia di tipo Windows (\r\n) o UNIX (\n) e il tutto viene condensato in \r?\n.
Infine, il simbolo del dollaro costringe il motore regex per verificare se il testo corrispondente è una linea completa e se si trova alla fine del file utilizzando il simbolo del dollaro.


Validare un indirizzo IP

Giusto per capire la successiva espressione ricordo che un indirizzo ip solitamente è espresso come una stringa composta da quattro elementi separati da un punto:

11.255.008.2

Ogniuna delle quattro parti rappresenta un numero da 0 a 255, quindi il problema si riconduce a verificare che la stringa sia composta da quattro numeri separati da dei punti e che ogni numero sia compreso nel range precedente.
Per fare quest'ultima verifica possiamo usare la regexp:

[01]?\d\d?|2[0-4]\d|25[0-5]

che è equivalente a

[01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5]

in pratica essa è composta da tre possibili alternative:

25[0-5]
2[0-4][0-9]
[01]?[0-9][0-9]?

la prima per i numeri compresi da 250 a 255, la seconda per i numeri compresi tra 200 e 249, la terza per i numeri tra 0 e 199
Quindi l'espressione regolare che convalida un indirizzo IP è la seguente:

^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.
([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$

Sono state inserite delle parentesi tonde per ogni numero che compone l'IP in modo da poterli riutilizzare in seguito, per ulteriori elaborazioni, ma non sono in alcun modo necessarie.

Se l'IP fosse inserito in un contesto e se ne volesse verificare la presenza basterebbe modificare la restrizione iniziale e finale:

\b([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.
([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\b

Indirizzo IPV6

Nel caso l'indirizzo da validare fosse di tipo IPV6, le cose si complicano un po' dato che alcune parti possono essere omesse:

(?:(?:(?:[A-F0-9]{1,4}:){6}
|(?=(?:[A-F0-9]{0,4}:){0,6}
(?:[0-9]{1,3}\.){3}[0-9]{1,3}$)
(([0-9A-F]{1,4}:){0,5}|:)((:[0-9A-F]{1,4}){1,5}:|:))(?:(?:25[0-5]|2[0-4][0-9]|
[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
|(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}
|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}$)(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)) # Anchor address


Eliminare il percorso dal nome del file

Il nostro obbiettivo è quello per esempio con Linux  avendo una stringa del tipo "/tmp/pippo/myfile.txt" ottenere "myfile.txt".
Per fare questo basta ignorare tutti i caratteri fino all'ultimo backslash "/" :

^.*/

oppure partendo dalla fine fino ad carattere "/":

/.*$

ma tralasceremo questa soluzione per motivi di efficienza

Riporto alcuni esempi direttamente in diversi linguaggi che usano le espressioni regolari:

Language Code
Perl $f =~s{^.*/}{};
PHP $f = preg_replace('{^.*/}', '', $f);
java.util.regex f = f.replaceFirst("^.*/", "");
VB.NET f = Regex.Replace(f, "^.*/", "")

con windows le cose si complicano un po', dato che lo slash "\" presente nel percorso è anche un carattere speciale nelle espressioni regolari, quindi a seconda del linguaggio di programmazione sarà necessario modificare la regexp:

Language Code
Perl $f =~s/^.*\\//;
PHP $f = preg_replace('/^.*\\\/', '', $f);
java.util.regex f = f.replaceFirst("^.*\\\\", "");
VB.NET f = Regex.Replace(f, "^.*\\", "")

Se si volesse recuperare sia il percorso che il nome del file, occorrerebbe sempre distinguere tra i due casi in cui il percorso sia specificato o meno, altrimenti ci sarebbe un errore:

if ( $PathFilename =~ m!^(.*)/([^/]*)$! ) {
  # Have a match -- $1 and $2 are valid
  $LeadingPath = $1;
  $FileName = $2;
} else {
  # No match, so there's no '/' in the filename
  $LeadingPath = "."; # so "file.txt" looks like ". / file.txt" ("." is the current directory)
  $FileName = $WholePath;
}


Rimuovere gli spazi bianchi all'inizio e alla fine di una riga

Gli spazi e le tabulazioni nelle espressioni regolari sono identificati con "\s", quindi l'espressione regolare per  identificare gli spazi all'inizio di una riga è:

^\s+

quella per trovare gli spazi alla fine è:

\s+$

unendole insieme otteniamo:

^\s+(.*?)\s+$

che funziona, ma è veramente inefficiente, quindi sarebbe meglio usare:

^\s+|\s+$

o meglio

^\s*((?:.*\S)?)\s*$

in questo caso l'elaborazione della stringa è stat ottimizzata usando un "notcapturing group", infatti i tre caratteri "(?:" aprono  un noncapturing group e la parentesi tonda ")" lo chiude. Un noncapturing group offre le stesse funzionalità di un gruppo(ripetizioni, alternative, etc...), ma non cattura nulla, cioè non sarà possibile accedere all'oggetto riscontrato all'interno delle parentesi tramite una variabile.
quindi avremo un gruppo con all'interno un notcapturing group che può essere ripetuto zero o più volte:

((?:.*\S)?)

l'oggetto identificato dal gruppo interno è ".*\S" , cioè una stringa  composta da caratteri qualsiasi ma che termina con un carattere  non-spazio. Infatti "\S" rappresenta l'opposto di "\s".

Per usare l'espressione precedente basta fare una sostituzione che eliminerà tutti gli spazi alla fine e all'inizio:

s/^\s*((?:.*\S)?)\s*$/$1/s


Inserire il testo catturato da un gruppo nel testo sostitutivo

Prima di iniziare vorrei ricordare brevemente cosa sono i gruppi in una espressione regolare.
In pratica un gruppo è identificato tramite delle parentesi tonde, cioè se io scrivo una espressione regolare o una parte di essa all'interno di parentesi tonde, quando essa viene applicata su di un testo, la parte di esso che ha un riscontro positivo con il gruppo verrà memorizzata all'interno di alcune variabili, che in alcuni casi possono essere richiamate.

L'esempio tipico è con una funzione di sostituzione, nella quale vogliamo inserire una parte del testo originale nel risultato finale senza conoscerlo precisamente.

Esempio, il testo di partenza è:

Hi Peter Smith

e uso il pattern .*\s(.*)\s.* che letteralmente significa :

".*" = Qualsiasi carattere che non sia un fine riga presente zero o infinete volte
"\s" = Uno spazio
"(.*)" = Come prima un carattere qualsiasi ripetuto zero o più volte, solo che questa volta il testo corrispondente verrà conservato in una variabile.

In dettaglio:

"." +       // Match any single character that is not a line break character
"*" +       // Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
"\s"+      // Match a single character that is a “whitespace character” (spaces, tabs, and line breaks)
"(" +       // Match the regular expression below and capture its match into backreference number 1
"." +       // Match any single character that is not a line break character
"*" +       // Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
")" + // End group 1
"\s"+      // Match a single character that is a “whitespace character” (spaces, tabs, and line breaks)
"." +       // Match any single character that is not a line break character
"*"         // Between zero and unlimited times, as many times as possible, giving back as needed (greedy)

Quindi quella che è stata "catturata" è la stringa "Peter", alla quale verrà assegnato il gruppo 1, dato che era l'unico.
Ora vedremo come usarlo!

Una delle operazioni che capita più spesso è quella di usare il testo che rispetta l'espressione regolare in elaborazioni successive, come ad esempio nella sostituzione.

Facciamo un esempio:

<a href="http://test.com">test</a>

Vogliamo "catturare" l'URL e sostituirlo al testo del link, iin modo da avere il seguente risultato:

<a href="http://test.com">http://test.com</a>

quindi la nostra regexp sarà del tipo:

<a href="/(.*?)">(.*?)</a>

ora quella espressione troverà tutti i vari link, e conserverà il contenuto gruppo 1 e del gruppo 2 in alcune variabili, che differiscono a seconda del linguaggio usato, ad esempio per identificare il gruppo 1:

Group 1 Perl PCRE PHP .NET Javascript Pyton Ruby
\1 X X

X X
$1 X X X X

${1} X X X


Ipotizzando di dover usare la regexp in PHP potremo scrivere:

$output = preg_replace('(<a href="/)(.*?)(">)(.*?)(</a>)',$1.$2.$3.$2.$5 , $input);
____1____ _2_ _3_ _4_ _5__

dove in pratica ho sostituito il gruppo 4 con in gruppo 2, ripetendo quindi l'indirizzo del link. Naturalmente questo è solo un esempio banale che poteva trovare tante altre soluzioni e non tiene conto del contesto, ma è servito a mostrare l'uso dei gruppi.

Un altra variabile a cui spesso capita di far riferimento è quella relativa all'intero testo che rispetta l'espressione regolare, e che può differire anche in questo caso a seconda del linguaggio utilizzato:

Regexp match Perl PCRE PHP .NET Javascript Pyton Ruby
\0
X


X
$0
X X


${0} X X X


\&




X
$& X
X X

\g



X


Uso avanzato dei gruppi

Abbiamo visto che le parentesi tonde delimitano un gruppo ed il pattern contenuto in esso consentirà la memorizzazione del testo corrispondente.

(pattern)

Questo non è del tutto vero, dato che è possibile modificare la funzionalità del gruppo, inserendo all'inizio dei particolari caratteri. Per esempio per creare un gruppo che non memorizza il testo corrispondente (non-capturing group):

(?:pattern)

Oppure potrei creare dei gruppi "condizionali", cioè che pongono una condizione per il riscontro della espressione regolare.
Questi gruppi condizionali vengono anche definiti come "lookaround" perché le condizioni sono poste su quello che sta intorno, e si didividono in due categorie:

  • lookahead (controlla davanti) = Impongono una condizione sul testo che viene dopo
  • lookbehind (controlla dietro) = Impongono una condizione sul testo precedente

A loro volta queste condizioni potranno essere negative o positive:

(?=pattern) positive lookahead
(?!
pattern) negative lookahead
(?<=pattern) positive lookbehind
(?
pattern) negative lookbehind

Per chiarire meglio facciamo qualche esempio:

I bring my cat at home

rispetterà la regex "cat(?!s)" ma non "cat(?=s)", infatti con "cat(?!s)" stiamo dicendo che va bene la parola cat, ma che non deve essere seguita dalla lettera "s", se al contrario usiamo "cat(?=s)" imponiamo che va bene cat solo se seguita dalla "s".
Un'altra cosa da sottolineare è che questi gruppi condizionali non memorizzano nulla e quindi sono del tipo non-capturing:

(.*)(cat)(?!s)(.*)  =>   group1="I bring my "   group2="cat"   group3=" at home"

stesse regole valgono per i gruppi di tipo lookbehind, infatti "(?<=my\s)cat" verrà rispetta in quando impone che il testo che precede "cat" debba contenere "my ", mentre "(?cat" sia preceduta da "my ".

Faccio presente che in alcuni linguaggi o di alcune loro versioni potrebbero non includere qualche tipologia di gruppi, come ad esempio in javascript sono presenti i "lookahead" ma non i "lookbehind".

 


Trovare una riga contenente o no una parola

L'esempio più utile che mi viene in mente su come usare i gruppi condizionali è la verifica di una parola all'interno di una riga. In verità dipende un po' dalle esigenze del caso; ma vediamo di arrivarci:

^.*\b(one|two|three)\b.*$

Questa espressione darà esito positivo nel caso all'interno di una riga venisse trovata una delle tre parole "uno","due" e "tre". Abbiamo usato "\b" perché le parole devono essere intere e non parte di altre. Il problema di questa espressione regolare è che il gruppo, in presenza di più parole chiave, memorizza solo una di esse  e sempre quella più a destra questo perché il generico contenuto all'inizio della riga è stato ottenuto solamente con l'asterisco che è "greedy", quindi se volessimo che il controllo dell'espressione si fermasse al primo riscontro, allora dovremo usare:

^.*?\b(one|two|three)\b.*$  

Ma se volessimo verificare che esistano nella riga tutte e tre le parole chiave? In questo caso i gruppi condizionali ci potrebbero aiutare:

^(?=.*?\bone\b)(?=.*?\btwo\b)(?=.*?\bthree\b).*$

Se invece volessimo verificare che non sia presente una determinata parola:

^((?!\bavoid\b).)*$

Anche in questo caso il gruppo condizionale con match negativo ci aiuta a verificare che non vi sia un riscontro della espressione regolare contenuta al sui interno in tutta la linea.

Infine come esempio conclusivo riporto una espressione regolare in cui una parte darà esito positivo solo se la riga contiene la parola chiave, una parola chiave opzionale e infine controlla che non siano presenti altre due parole.

^(?=.*?\bmust-have\b)(?=.*?\bmandatory\b)((?!avoid|illegal).)*$

 


Convalidare un indirizzo email

A seconda del livello di dettaglio che si volesse raggiungere esistono diverse espressioni regolari che svolgono questo compito.

Ad esempio se si volesse solo verificare la presenza del simbolo"@" :

^\S+@\S+$

Se volessimo imporre anche delle restrizioni sui caratteri:

^[A-Z0-9+_.-]+@[A-Z0-9.-]+$

Includendo anche dei caratteri un po' particolari ma sempre previsti dalla RFC 2822:

^[\w!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+$

Potremo anche imporre che non vi siano punti consecutivi sia nel nome che nel dominio:

^[\w!#$%&'*+/=?`{|}~^-]+(?:\.[!#$%&'*+/=?`{|}~^-]+)*@?[A-Z0-9-]+(?:\.[A-Z0-9-]+)*$

e infine potremo imporre che il dominio di primo livello possa essere composto da minimo 2 e massimo 6 caratteri:

^[\w!#$%&'*+/=?`{|}~^-]+(?:\.[!#$%&'*+/=?`{|}~^-]+)*@?(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$

Per quanto riguarda i caratteri permessi, molto dipende dai server che gestiscono le email, e dato che vi potrebbero essere delle restrizioni maggiori, è bene che le precedenti espressioni regolari vengano adattate ai singoli casi.

 


Multiline match in javascript

Come abbiamo mostrato negli altri esempi, ogni linguaggio implementa le espressioni regolari a modo suo. Questo accade anche in javascript, dove ad esempio non esiste il modificatore "s", che in altri linguaggi fa in modo che il carattere speciale "." (punto) esegua un riscontro positivo con tutti i caratteri, inclusi quelli di fine riga, che altrimenti non erano inclusi.

Prendiamo per esempio questo testo:

aaa bbb aaa
bbb

Se volessimo trovare un riscontro per una sequenza "bbb aaa bbb", basterebbe usare una espressione regolare del tipo:

/bbb.*aaa.*bbb/s

che troverebbe la sequenza cercata anche tra righe diverse:

aaa bbb aaa
bbb

Ora vediamo come fare la stessa cosa in javascript.

Innanzitutto occorre chiarire che il modificatore multilinea "m", ha la funzione di modificare il significato dei caratteri speciali "^" e "$", facendoli diventare rispettivamente l'inizio riga e il fine riga, che altrimenti rappresentavano l'inizio e la fine dell'intero testo.
Facciamo un esempio, se avessimo un testo del genere:

aaa bbb

Il pattern "/^aaa bbb$/" avrebbe un riscontro positivo, perchè essendo il testo composto da una sola riga, l'inizio del testo, così come la sua fine, coincidono con l'inizio e la fine della riga.

Ma se avessi un testo diverso:

aaa bbb
aaa

La precedente espressione regolare non troverebbe nessun riscontro. Per risolvere il problema basterebbe usare il modificatore del pattern "m" :

/^aaa bbb$/m

Sperando di aver chiarito la funzione di questo modificatore, dovreste anche aver capito il motivo per cui non possiamo utilizzarlo per un match su più righe, come abbiamo mostrato per il modificatore "s".

La domanda è quindi come facciamo ad emulare il modificatore "s" in javascript?
La risposta è semplice al posto di usare il punto usare una classe di caratteri che comprendano anche i quelli di fine riga.
Riprendendo l'esempio iniziale:

aaa bbb aaa
bbb

La regexp in questo caso diventerebbe:

/bbb[\s\S]*aaa[\s\S]*bbb/

Rispetto alla soluzione iniziale abbiamo quindi sostituito il punto con "[\s\S]", che sta ad indicare un qualsiasi carattere, compresi quelli di fine riga.
Letteralmente "[\s\S]" significa un carattere di tipo spazio (compresi spazi, tabulazioni e fine riga) o di tipo non spazio, quindi tutti i caratteri.
Nel dettaglio:

[\s] matches whitespace = [\f\n\r\t\v\u00A0\u2028\u2029]
[\S] matches anything but a whitespace = [^\f\n\r\t\v\u00A0\u2028\u2029]

dove:

\f     matches form-feed
\r matches carriage return
\n matches linefeed
\t matches horizontal tab
\v matches vertical tab
\uhhhh matches the Unicode character with four characters of hexadecimal code hhhh

Software

Se vi dovesse servire un software per fare qualche test e imparare meglio, vi consiglio RegexBuddy. Online si trovano anche delle versioni demo di questo programma.

Ultimo aggiornamento ( Martedì 15 Aprile 2014 05:04 )  
Loading

Login