Lezione del 7 dicembre 2010 PSINFORM from
Marco Tonti on
Vimeo.
Espressioni regolari (note anche, e più brevemente, come RegEx da regular expressions.
QUI per chi comprende l'inglese un sito eccellente ma non guardate in "about the site" per non prendere paura!, e
QUI un sito dove fare prove)
Si tratta di un sistema quasi standard (con poche variazioni tra le varie implementazioni) per decrivere in modo più versatile le stringhe che si desidera cercare usando le comuni funzioni "cerca" di molti programmi di editor di testo, in particolare quello adottato dal nostro corso, Writer di OpenOffice. Per eseguire la ricerca nel modo standard è sufficiente aprire la finestra di ricerca e inserire la parola da cercare. Il "match", cioè la corrispondenza tra la paola cercata (in realtà una sequenza di caratteri) e i caratteri che compongono il testo, può essere vincolato in due modi: indicando che la parola trovata sia una parola intera (quindi "
arco" non verrà trovato in "
Marco" ma verrà trovato in "
prese l'arco e centrò la mela": la punteggiatura è considerata un'interruzione di parola), o che la corrispondenza debba essere anche tra maiuscole e minuscole (quindi "
Marco" verrebbe trovato in "
Marco Tonti", ma non in "
una moneta da un marco").
Le regex permettono di introdurre una varietà infinitamente più sofisticata nella modalità di descrizione di una stringa. Le RegEx (attivate in Writer nelle opzioni avanzate della ricerca) permettono per esempio di esprimere una stringa da cercare il cui matching può o meno comprendere un carattere, per esempio cercando "
Marco?" (dove il "
?" è un simbolo del linguaggio delle regex che indica che l'ultimo carattere può o meno essere presente) il programma troverebbe corrispondenze sia con "
Marco Tonti" che con "
Marc Chagall", perché abbiamo espresso nella ricerca l'informazione che la "
o" finale può essere opzionale.
Un'altra possibilità che sarebbe utile è quella di poter indicare delle alternative che può avere un carattere in una certa posizione, per esempio se volessimo cercare (contemporaneamente) tutti i "
Marco" e tutti i "
Mario", potrebbe essere utile un modo per indicare che il quarto carattere possa essere sia una "
c" che una "
i". Nelle espressioni regolari per indicare una serie di scelte accetabili per un carattere si usano le parentesi quadre, all'interno delle quali inserire i caratteri accettabili in quella posizione, quindi se noi cercassimo "
Mar[ic]o" esprimiamo il fatto che il quarto carattere può essere sia una "
i" che una "
c". Nelle parentesi quadre è possibile inserire anche cifre e intervalli, per esempio
[a-z] indica tutti i caratteri dell'alfabeto (senza accentate, cifre ecc. solo le lettere). È possibile fare in modo che le quadre funzionino anche al contrario, cioè per indicare quali lettere NON devono essere presenti, questo lo si ottiene scrivendo
[^ ] (oppure, in certe implementazioni,
[! ]). Cercando "
Mar[^c]io", potremmo trovare tutte le sequenze "
Maraio", "
Marbio", "
mardio", "
mareio"; notate però che "
marcio" non è presente, perché imponiamo che in quella posizione NON possa esserci una "
c" (e inoltre non siamo in
Danimarca).
Esistono inoltre caratteri speciali che si usano come quantificatori. Uno l'abbiamo già introdotto ed è il "
?", che significa che il carattere immediatamente precedente può comparire 0 o 1 volte. Per indicare che il carattere precedente può comparire da 0 a un numero indefinito di volte si usa il quantificatore
*. Quindi se cercassimo "
ab*c" il programma riconoscerebbe qualsiasi sequenza di composta di una "
a" seguita da un qualsiasi numero di "
b" e conclusa con una "
c": "
abc", "
abbc", "
abbbc", "
abbbbc"... ma anche "
ac" dove la "
b" centrale compare 0 volte! Se vogliamo stringere il vincolo possiamo usare il quantificatore
+ che è molto simile al
* ma impone che la sequenza sia lunga almeno un carattere. Scrivendo "
ab+c" riconosceremmo "
abc", "
abbc", "
abbbc" e così via ma non "
ac". Lo stesso risultato lo potremmo ottenere con "
abb*c", che si può leggere come: "una a seguita da una b seguita da una qualsiasi sequenza di b seguita da una c". La notazione col
+ però è più compatta e facile da leggere.
Provando a combinare gli elementi introdotti finora, cercando con la regex "
[abc]+" otterremmo il riconoscimento di tutte le possibili sequenze di combinazioni dei caratteri a, b, c. Potremmo trovare "a", "b", "c", "aa", "ab", "ac", "ba", "bb", "bc", "ca", "cb", "cc", "aaa" e così via: qualsiasi combinazione di qualsiasi lunghezza dei caratteri a b c verrebbe riconosciuta, e SOLO quelle combinazioni verebbero riconosciute. "abcbaabababababacbcabcabcabcabcabacbcabccacbacbacacacbacbacbbcbcbcbbabcbcabbcbcbcbcabcbacb" sì, "aaaaaaaaa" sì, "bbbbbb" sì, "acab" sì, "bacca" sì, "ada" NO.
Un altro simbolo speciale è il carattere "
.", il punto, che fa match con qualsiasi carattere (tranne gli a-capo). "
a..b" troverebbe tutte le sequenza di 4 caratteri ai che cominciano con "a" e finiscono con "b". Toverebbe "acab", troverebbe "aaab", troverebbe "abbb". Anche questo carattere speciale può essere combinato con i quantificatori, quindi "
.*" è una
qualsiasi sequenza di caratteri.
Un altro carattere speciale è il "pipe": "
|". Indica un'alternativa tra due espressioni regolari. Per esempio cercando "
Marco|Mario" troveremmo tutte le occorrenze di Marco o di Mario. Notate che il
| non agisce solo sui caratteri adiacenti, ma si espande a tutte le espressioni regolari verso sinistra e verso destra, quindi è necessario un modo per "contenere" la sua espansione. Per fare ciò in genere le scelte alternative si inseriscono tra parentesi tonde:
(|). Le parentesi però hanno un ruolo tutt'altro che marginale nelle Regex, perché possono racchiudere qualsiasi tipo di espressione regolare e vengono considerate com un tutt'uno. Al quale si può applicare anche qualsiasi quantificatore. Per esempio
(ab|cd)+ riconosce qualsiasi sequenza di caratteri composta di combinazioni qualsiasi delle sottosequenze "ab" o "cd". Riconoscerebbe "abcd", "cdab", "abababcdcdcd", "abcdabcdabcd". Ma NON "abcda", perché la sequenza si deve ottenere come iterazione di due possibili scelte "fisse": "ab" o "cd". Il quantificatore fa ripetere questa scelta per un numero indefinito di volte.
Riassumendo, i caratteri speciali che si usano nelle espressioni regolari sono:
[ ] un insieme di caratteri ammissibili per una posizione
[^ ] un insieme di caratteri NON ammissibili per una posizione
( ) una sotto-espressione regolare, gruppo
| un'alternativa tra due espressioni regolari
. un qualsiasi carattere
? quantificatore: 0 o 1 occorrenza del carattere (o gruppo) immediatamente precedente
* quantificatore: 0 o n occorrenze del carattere (o gruppo) immediatamente precedente
+ quantificatore: 1 o n occorrenze del carattere (o gruppo) immediatamente precedente
(ulteriori informazioni e approfondimenti si possono trovare ovunque cercando "espressioni regolari" con google, in particolare consiglierei:
qui )
L'esempio che abbiamo affrontato a lezione è quello delle date. Le date (in linea di massima) hanno una struttura stabile e prevedibile, in particolare sono sequenze di due cifre, una barra, due cifre, una barra e quattro cifre. Se noi volessimo trovare tutte le date presenti in un documento (cioè tutte le sequenze di caratteri che rispondono a questo pattern) potremmo comodamente usare le espressioni regolari. Un primo approccio può essere il seguente (mettiamo di limitarci alle date del secolo scorso):
[0-9][0-9]/[0-9][0-9]/19[0-9][0-9]
però questa regex riconoscerebbe TUTTE le combinazioni fatte così, per cui anche eventuali sequenze di carattere (come dei codici) che date non sarebbero (riconoscerebbe 74/15/1999). Possiamo fare di meglio, restringendo per via sintattica la variabilità delle date. Intanto consideriamo che i numeri dei giorni possono andare da 01 a 31, quelli dei mesi da 01 a 12. Adeguiamo le descrizioni a questi nuovi vincoli:
[0-3][0-9]/[01][0-9]/19[0-9][0-9]
Anche in questo caso esistono sequenze che possono essere trovate ma che non sono date, per esmepio 39/19/1999. Si può fare di meglio? Certo! Bisogna considerare che certe combinazioni di valori, in particolare i valori dei giorni che cominciano con 3 e dei mesi che cominciano con 1, non sono tutte accettabili e possono essere escluse a priori. Dato che dobbiamo distinguere due casi sia per i giorni che per i mesi, bisogna usare un operatore di | che ci faccia stabilire in quale dei due casi ci troviamo, ricordandosi di raggrupparli in parentesi.
(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[012])/19[0-9][0-9]
Analizziamo la sottoespressione dei giorni:
0[1-9]|[12][0-9]|3[01]
Questa espressione ci pone davanti a tre alternative per il riconoscimento: o la stringa che stiamo cercando comincia con uno 0 (e allora può essere seguta solo da un numero tra 1 e 9: il giorno 00 non esiste), oppure la stringa comincia con un 1 o con un 2 (e allora può essere seguita da qualsiasi cifra), o comincia con un 3 (e allora può essere seguita solo da uno 0 o da un 1). Questa espressione riconosce solo ed esattamente le sequenze di due caratteri che rappresentano numeri compresi tra 01 e 31.
L'espressione dei mesi è analoga:
0[1-9]|1[012]
Se la sequenza comincia con uno 0 allora la cifra dopo dev'essere compresa tra 1 e 9, se comincia con 1 allora può essere seguita solo da 0, 1, 2.
Notate che alcune date sfuggono comuque, ma sono vincoli di natura semantica difficilmente traducibili in sintassi: verrebbe riconosciuta come data anche il 31/02/1999 o il 31/08/1999 (ma febbraio ha al massimo 29 giorni e agosto ne ha 30, quindi queste non sono veramente date). Dopo aver fatto la lezione e dopo aver scritto questo riassunto, guardando sul sito che vi ho segnalato ho trovato questo:
http://www.regular-expressions.info/dates.html praticamente le stesse cose che ho descritto qui.
Nei fatti pratici raramente è utile descrivere le date in tanto dettaglio, ma la tecnica è talmente versatile da poter essere usata per esempio per trovare (o verificare) indirizzi email:
[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}
Per comprendere come mai questa espressione è in grado di riconoscere un'email, bisogna aggiungere un dettaglio sull'ultima espressione fra graffe: è un modo per quantificare apertamente una ripetizione, e significa: il carattere precedente ripetuto da 2 a 4 volte (che cattura per esempio i ".it" o ".com" o ".info"). Per collegarlo a quanto detto prima, il quantificatore ? corisponde esattamente a {0,1}. Inoltre l'ultimo "
." della regex non è un carattere speciale, perché essendo preceduto da una
\ il sistema sa che lo deve trattare come un "letterale" e non come un carattere del linguaggio. Quindi nella sintassi delle regex \. fa match con un "." nel testo, mentre . fa match con qualsiasi carattere.
Per trovare la spiegazione, guardate
qui con tutti i dettagli illustrati ottimamente.
Ciò detto sulla ricerca, e trovato il modo per individuare una generica data, potremmo chiederci che possiamo farcene. Mettiamo di voler trasformare tutte le date espresse all'italiana come abbiamovisto nel formato internazionale: anno-mese-giorno. Se ci fate caso, le informazioni necessarie noi le possiamo già "catturare" con la regex delle date che abbiamo costruito: basterebbe prendere i pezzi e riordinarli diversamente. Questo è possibile farlo usando i gruppi, cioè le parentesi tonde. Tutto ciò che viene a trovarsi tra parentesi tonde è un gruppo, e il sistema che si usa lo memorizza permettendoci di riutilizzarlo nella casella SOSTITUISCI. I gruppi sono identificati con la loro posizione della regex partendo da sinistra, quindi il primo gruppo a sinistra ha il numero 1, il secondo il numero 2 e così via. Per riferirci a loro nella casella "sostituisci" usiamo il $,
$1 è quello che è stato trovato nel primo gruppo,
$2 il secondo e così via.
Riscriviamo la nostra espressione per catturare anche il contenuto dell'anno:
(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[012])/(19[0-9][0-9])
Se noi inseriamo questo in "cerca", e in trova invece inseriamo
$3-$2-$1
Quando clicchiamo "sostituisci tutto" (come si vede nel filmato) il programma effettuerà in modo completamente automatico questo lavoro. Istantaneamente. Gli stiamo di fatto chiedendo: TROVA nel testo una combinazione di caratteri che risponda alla nostra descrizione, e memorizza i caratteri che ricadono nelle posizioni avvolte da parentesi, quindi SOSTITUISCI tutto quello che hai trovato e che hai riconosciuto come data, con la nuova sequenza di caratteri che ottieni prendendo i caratteri che ricadono nel terzo gruppo, seguito da un -, poi il contenuto del secondo gruppo, seguito da un -, poi il contenuto del primo gruppo. In questo modo la sequenza trovata viene ineramente sostituita da una nuova sequenza costruita "al volo" e che varia in base alla stringa riconosciuta. Avremmo ovviamente potuto sostituire qualsiasi cosa alle date, per esmepio delle "xxx" per rimuovere ogni riferimento temporale dal testo! Oppure sostituire la data con
$1/$2 per eliminare l'informazione dell'anno.
Un ultimo elemento introdotto a lezione che permette di aggiungere vincoli alle espressioni regolari sono i segnaposti per inizio e fine del paragrafo. Se vogliamo catturare solo le date (o le etichette, come si vede anche nel filmato) che si trovano esattamente all'inizio della riga, è sufficiente inserire un carattere
^ all'inizio della regex:
^(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[012])/(19[0-9][0-9]) , questo impone il vincolo che non solo la sequenza deve essere trovata, ma deve trovarsi adiacente all'inizio della riga (se vogliamo riconoscere solo date che partono dalla quinta posizione possiamo sempre fare
^....(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[012])/(19[0-9][0-9]) cioè, dato che il punto è un carattere qualsiasi, partendo da inizio riga contiamo quattro caratteri non meglio specificati, e lì cerchiamo la data.
Un'altra applicazione di cui si è discusso è la rimozione degli spazi vuoti ripetuti in un testo: è sufficiente cercare "
+" (uno spazio, seguito da un
+) e sostituirlo con uno spazio singolo " ". Per formattare correttamente la punteggiatura è sufficiente usare un gruppo di cattura (tra parentesi quadre il punto è solo un punto, non rappresenta un carattere speciale):
cerca: "
*([,.;:]) *"
sosstituisci: "
$1 "
In pratica cerca un segno di punteggiatura preceduto e seguito da 0 o più spazi (i vari casi possono essere "ciao
,amico" "ciao
, amico" "ciao
,amico" "ciao
,amico" "ciao
, amico" ecc.). Dato che l'elemento di punteggiatura è registrato in un gruppo di cattura, nel "sostituisci" possiamo utilizzaro per rimpiazzarlo a se stesso, ma circondato dagli spazi giusti: "ciao, amico".