venerdì 19 dicembre 2014

L'asteroide che ucciderà questo dinosauro deve ancora arrivare (seconda parte)

L'articolo è diviso in tre parti:
Prima Parte
Terza Parte

La volta scorsa abbiamo visto un po' di automi e le basi delle espressioni regolari (ed abbiamo intuito una relazione tra i due). Questa volta faremo un po' meno teoria e un po' più pratica: vi illustrerò i metodi che adopero per leggere e scrivere le regex.

Leggere le espressioni regolari

Il primo impatto con le regex è (quasi) sempre traumatico, specialmente se nessuno vi ha mai spiegato prima che quelle sono istruzioni per il computer e non usi creativi della punteggiatura ("ma che c[a-z]{0,} !!?").

Anche sapendo che il punto ha un significato, il più un altro e l'asterisco un altro significato ancora è facile confondersi e sbagliare.

Siccome in Rete si possono trovare numerosi esempi di espressioni regolari che eseguono i compiti più disparati molti si limitano a fare copia & incolla senza fermarsi a capire cosa effettivamente facciano quegli scampoli di lettere e caratteri apparentemente causali.

Alla luce di ciò mi pare giusto che, prima di andare avanti, vi spieghi come si leggono le regex. Specialmente perché potreste ritrovarvi a dover modificare un'espressione scritta da qualcun altro molto tempo fa (anche il vostro ego di 8 mesi fa conta come "qualcun altro").

Siccome scriverò parecchie regex e siccome le scriverò in mezzo al testo dell'articolo userò la seguente convenzione: le regex saranno scritte in monospace e racchiuse tra singoli slash (/). Gli slash NON vanno interpretati come parte della regex ma solo come delimitatori. Questa convenzione è adottata sia dal linguaggio AWK che dal perl e da sed (un tool che vedremo nel prossimo articolo).

La prima regola è: comportarsi come la macchina.

Le espressioni regolari si leggono un carattere alla volta da sinistra a destra. Non provate a saltare dei pezzi o a fare assunzioni, perché la macchina NON lo fa.

La seconda regola è: puntare sempre a riconoscere il più possibile.

Facciamo un esempio con questo testo di input:

Vuolsi così colà dove si puote ciò che si vuole e più non dimandare.
L'espressione regolare /co.*/ applicata al testo precedente ritornerà come stringa trovata la seguente porzione:

così colà dove si puote ciò che si vuole e più non dimandare.
Siccome il . significa "un carattere qualsiasi" e la stella (*) significa "zero o più" quell'espressione si legge come "co seguito da zero o più caratteri qualsiasi". Non essendoci un limite superiore il match prende il primo co, quello di così, e copia in uscita tutto quello che trova fino a raggiungere la fine del testo.

Se invece avessimo usato l'espressione /co../? Avremmo avuto qualcosa del tipo "co seguito da un carattere qualsiasi seguito a sua volta da un altro carattere qualsiasi. Il risultato sarebbe stato solamente così.

Il che mi permette di introdurre la terza regola: salvo indicazione contraria fermati al primo risultato corretto che trovi.

Questa regola è importante quando ci si trova davanti ad una espressione regolare che, pur essendo corretta, non riconosce tutto quello che ci interessa riconoscere. La colpa spesso non è della regex, ma delle opzioni che abbiamo passato al programma che la interpreta.

La seconda regola invece spiega perché certe espressioni regolari funzionano su certi input ma riconoscono anche input che non si voleva riconoscere: di solito perché si è stati troppo generosi nell'uso di ., * e +.

Armati di queste nuove conoscenze vediamo di capire cosa riconosce la seguente espressione:

/[0-9]*\.[0-9]+([eE][-+]?[0-9]+)?/

Piuttosto complessa, vero? Ma adesso sappiamo come procedere a leggerla (si spera).

Il primo carattere è una parentesi quadra aperta e quindi quello che segue è la definizione di una classe di caratteri, segue uno 0 che ci dice che la classe contiene quel carattere. Il - ci dice che stiamo componendo un range di caratteri e il 9 è il carattere di chiusura del range. La parentesi quadra chiusa termina la definizione della classe.

Riepilogando: crea una classe composta da tutti i caratteri da 0 a 9. Segue un * che ci dice "zero o più di quello che mi precede", quindi adesso sappiamo che la stringa cercata può cominciare con una o più cifre decimali.

Il carattere successivo è un backslash (\) che ci informa che il prossimo carattere va interpretato diversamente da come lo si interpreta di solito; infatti il punto che segue normalmente vorrebbe dire "un carattere qualsiasi", ma in questo caso significa "un punto". Quindi lo interpretiamo come un punto letterale.

Segue una replica della classe che abbiamo definito all'inizio seguita a sua volta da un +. Questo si legge come "una o più cifre decimali".

La parentesi tonda aperta ci informa che stiamo definendo un gruppo e la parentesi quadra aperta che la segue ci dice che stiamo definendo un'altra classe che contiene una e ed una E. La classe ci dice che il gruppo comincia con una lettera "e" minuscola oppure maiuscola. La classe che la segue ci mostra che il - inserito all'inizio di una classe non viene considerato come un separatore di range ma come un carattere letterale. L'espressione /[-+]/ significa quindi "un meno oppure un più", seguita da un punto di domanda (come in questo caso) diventa: "può esserci un meno oppure un più, ma può anche non esserci nulla".

Segue la classe delle cifre con un più che adesso sappiamo vuol dire "una o più cifre decimali", quindi arriva la parentesi tonda che chiude il gruppo e un punto interrogativo.

Il punto di domanda è un quantificatore che si riferisce al gruppo e quindi il gruppo che abbiamo appena definito può essere presente in toto una volta oppure non esserci affatto.

L'espressione nel suo complesso si legge: "Un punto preceduto eventualmente da alcune cifre decimali e seguito da almeno una cifra decimale e da un gruppo opzionale di caratteri che comincia con una 'e' o una 'E' seguita da un'eventuale indicazione di segno seguita da una o più cifre decimali". In breve si tratta di un qualsiasi numero decimale in virgola mobile positivo con eventuale indicazione dell'esponente.

Scrivere un'espressione regolare

A quanti non hanno ancora desistito e stanno proseguendo la lettura va il mio più sentito ringraziamento. Per premiarvi di tanta dedizione adesso vi confiderò il metodo che adopero per scrivere una regex (nella segreta speranza che vi sarà utile un giorno).

Per rendere le cose più facili (o più difficili) espanderemo l'espressione vista nella sezione precedente, che riporto di seguito per ridurre l'usura delle rotelline dei mouse:

/[0-9]*\.[0-9]+([eE][-+]?[0-9]+)?/

Questa espressione ha due difetti:

  • Non riconosce i numeri decimali negativi nè quelli positivi preceduti da un più (+).
  • Non riconosce i numeri decimali come 1. oppure 47.
Se volessimo riconoscere tutti i numeri decimali in virgola mobile quell'espressione mancherebbe diversi bersagli.

La prima cosa che faremo sarà aggiungere il segno. Sappiamo che il segno può essere un - oppure un + oppure nulla e sappiamo che se è presente si trova al primo posto, prima cioè di qualsiasi altro carattere che compone un numero decimale in virgola mobile.

Dalla nostra analisi dell'espressione abbiamo già trovato l'espressione che riconosce il segno: /[-+]?/. Ci limiteremo ad inserirla nel posto giusto:

/[-+]?[0-9]*\.[0-9]+([eE][-+]?[0-9]+)?/

Risolto il nostro primo problema dobbiamo cercare una maniera per risolvere il secondo problema.

La prima cosa che si deve fare quando si crea una regex è cercare un schema generale che si ripeta. Nel caso dei numeri decimali con il punto alla fine lo schema è proprio il punto alla fine: deve esserci sempre, altrimenti non stiamo leggendo un numero decimale in virgola mobile.

Quindi il punto dev'esserci e deve stare alla fine, ma davanti cosa ci va? Una o più cifre decimali eventualmente precedute dal segno.

Sappiamo che il simbolo + indica che quello che lo precede dev'essere presente "una o più volte" e che il simbolo ? significa che ciò che lo precede dev'essere presente zero oppure una volta ma non di più.

Quindi l'espressione per cercare i numeri decimali in virgola mobile composti da un numero seguito da un punto è la seguente:

/[-+]?[0-9]+\./

Abbiamo usato due classi, due quantificatori e un simbolo letterale per trovare tutti e soli i numeri simili a 47.. L'inizio di questa espressione somiglia moltissimo all'inizio dell'espressione precedente e potremmo essere tentati di sostituire la stella (* che significa "zero o più senza limite superiore") con il più (che significa "uno o più senza limite superiore"). Ricaveremmo la seguente espressione:

/[-+]?[0-9]+\.[0-9]+([eE][-+]?[0-9]+)?/

Facendo così però abbiamo imposto che ci siano delle cifre prima e dopo il punto, restringendo la gamma di numeri che riconosciamo anzichè ampliarla.

Se invece sostituissimo i due + dell'espressione appena trovata con due stelle? Otterremmo questa:

/[-+]?[0-9]*\.[0-9]*([eE][-+]?[0-9]+)?/

E riconosceremmo sia i numeri che hanno delle cifre prima del punto ma niente dopo che quelli che hanno delle cifre dopo il punto. Peccato che la nuova espressione sia fin troppo generosa e riconosca anche le seguenti stringhe come valide:

  • .
  • -.e0
  • .E-0
E noi non le vogliamo perché quelle stringhe non sono numeri validi!

Che si fa? O siamo troppo rigorosi oppure siamo troppo permissivi... Uhm... Non c'era un operatore che ci permetteva di dire "questo oppure quello ma non tutti e due"? Sì, Virginia, c'è e si tratta della barra verticale (|) che i fan di UNIX chiamano "pipe" perché ha un significato ben preciso nella shell UNIX.

Il segno opzionale all'inizio ci va bene e lo lasceremo dov'è, creeremo invece un gruppo che conterrà al suo interno un | per spezzare in due le possibilità di riconoscimento. Nella prima parte metteremo l'espressione per i numeri come 47. e nella seconda metteremo l'espressione originale che abbiamo visto nella sezione precedente.

Quindi abbiamo il segno:

/[-+]?/

Poi apriamo un gruppo e ci mettiamo i numeri con prima le cifre e poi il punto e basta seguiti da un "oppure":

/[-+]?([0-9]+\.|/

E infine rimettiamo l'espressione originaria e chiudiamo il gruppo:

/[-+]?([0-9]+\.|[0-9]*\.[0-9]+([eE][-+]?[0-9]+)?)/

Rileggiamo la nostra nuova espressione: un segno opzionale seguito da una o più cifre seguite da un punto oppure da zero o più cifre seguite da un punto seguito da una o più cifre seguito da un esponente che si compone della lettera e (maiuscola o minuscola) seguita da un segno opzionale seguito da una o più cifre.

Vi lascio come compito per casa l'espansione di quest'ultima espressione regolare affinché riconosca anche i numeri decimali interi: una o più cifre decimali precedute da un simbolo di segno opzionale e nessun punto.

La prossima volta ci occuperemo di alcuni programmi della shell UNIX che fanno largo uso delle regex.

Aloha!

Prima Parte
Terza Parte

Nessun commento:

Posta un commento