Flavio Poletti
Espressioniche?
http://www.perl.it/documenti/articoli/2005/11/espressioniche.html
© Perl Mongers Italia. Tutti i diritti riservati.

Se avete sentito parlare delle espressioni regolari, ma avete sempre

  • pensato che fossero solo concorrenti delle fave di fuca...
  • dichiarato che voi tutto, ma la matematica no...
  • sostenuto che un'accozzaglia di caratteri esprime poco, e non è sicuramente regolare...

... allora forse riusciamo a farvi cambiare idea.

Cominciamo a renderci conto che probabilmente sappiamo già usare dei lontani cugini delle espressioni regolari: le cosiddette wildcard. Vi è mai capitato di usare l'asterisco cercando un file? Tipo: per avere tutti i file PDF, digitare qualcosa come dir *.pdf? Beh, che lo vogliate o no, vi state già piegando al concetto che le espressioni regolari vogliono esprimere.

Analizziamo la wildcard *.pdf. Cosa significa? Semplice: "prendi tutti quei file che hanno una sequenza iniziale qualunque, ma che finiscono con i caratteri .pdf", nell'ordine. E se vogliamo cercare tutti i file PDF di ricette? Beh, probabilmente qualcosa tipo ricette*.pdf potrebbe fare al caso nostro, perché sta chiedendo: "tutti i file che cominciano con ricette, poi hanno una sequenza qualsiasi di caratteri, ma finiscono con .pdf". Nessuno credo abbia da obiettare sulla semplicità di questo strumento.

Il problema è che semplice spesso litiga con flessibile e versatile. Se volessimo trovare tutti i file che "iniziano con ricette, poi hanno una qualunque serie di cifre numeriche, e finiscono con .pdf" saremmo già nei guai. Qualcuno non ce l'ha fatta a star dentro a questa gabbia: voleva tutto. Voleva un sistema per poter descrivere in dettaglio che tipo di struttura deve avere un testo perché possa essere considerato "valido". E sono nate le espressioni regolari.

Regex in Perl

Visto che l'idea di stare in gabbia piace a pochi, si trovano molte varianti delle espressioni regolari, ed ogni linguaggio o strumento (come il mitico grep) presenta le sue peculiarità. Perl però si distingue in modo particolare: le espressioni regolari (le cosiddette regex) fanno parte del linguaggio, non di una qualche libreria esterna, il che rende semplice inserirle nel proprio codice.

Negli esempi che faremo, assumeremo sempre di dover verificare se un dato testo è compatibile con una regex oppure no. Se il nostro testo è contenuto in $testo, questa verifica viene fatta come segue:

  if ($testo =~ /QUI IL CONTENUTO DELLA REGEX/) {
     print("ok, compatibile\n");
  }
  else {
     print("NO, incompatibile\n");
  }

L'operatore =~ è detto operatore di binding, per il fatto che effettua una connessione (il binding) fra la variabile $testo e l'espressione regolare. In sostanza, può essere letto come "applica l'espressione regolare a destra sul testo della variabile a sinistra". Attenzione, dunque: non si tratta né di un'assegnazione né di un confronto.

Inoltre, seguiremo la tradizione dicendo che c'è un match se il testo è compatibile con la regex.

A questo punto, allacciamo le cinture e partiamo!

Le basi

I concetti base per iniziare a giocare con le regex sono fondamentalmente due: gli elementi ed i quantificatori. Gli elementi rappresentano grossolanamente una parte di testo. I quantificatori esprimono invece quante volte ci si aspetta che il dato elemento possa essere ripetuto.

Gli elementi più semplici sono i caratteri alfanumerici stessi. Essi semplicemente rappresentano sé stessi; l'espressione regolare

  /a/

indica che $testo deve contenere almeno una a. Alcuni caratteri, però, non rappresentano sé stessi, come il punto:

  /./

Questo significa che $testo deve contenere almeno un carattere. Qualunque carattere. Va bene, quasi qualunque carattere, ma per ora va bene così.

A questo punto siamo già in grado di scrivere alcune espressioni regolari:

  /ricette.pdf/  ==matcha==>  'ricette-pdf', 'ricettexpdf', ...

Ebbene sì, non si parla sempre di file! Se vogliamo un punto vero e proprio dobbiamo segnalarlo con un backslash:

  /ricette\.pdf/  ==matcha==> 'ricette.pdf' ma NON 'ricette-pdf'

Come controlliamo che ci siano almeno tre caratteri fra ricette e .pdf? Un sistema è ripetere:

  /ricette...\.pdf/  ==matcha==> 'ricetteABC.pdf' e 'ricette....pdf'

A parte che per 50 caratteri invece di 3 diventa difficile, come facciamo, ad esempio, a dire "una qualunque sequenza (anche vuota) di qualunque carattere"? A questo ci pensano i quantificatori:

  *     ripete l'elemento 0 o più volte
  +     ripete l'elemento 1 o più volte
  ?     l'elemento 0 o 1 volta
  {n}   l'elemento esattamente n volte
  {n,}  l'elemento almeno n volte
  {n,m} l'elemento un numero minimo di n volte e massimo di m

Dunque, la nostra wildcard "tutti i file PDF che hanno ricette e .pdf" si scrive così (per il momento!)

  /ricette.*\.pdf/

C'e ancora qualcosa che non va, però. Le regex sono flessibili, abbiamo detto; questo si traduce nel fatto che assumono il meno possibile su quello che volete realmente fare. La regex sopra dice: "$testo deve contenere la sequenza ricette, seguita da una sequenza qualsiasi di caratteri, seguita da .pdf". Né più, né meno. Che ne dite allora di questo testo?

   pipporicette1234abcdef.pdf-old
        ^^^^^^^^^^^^^^^^^^^^^

Hey! Verifica! Ma noi vogliamo i nomi che cominciano con ricette, e finiscono con .pdf. Beh, va detto utilizzando dei marcatori speciali:

   ^    marca l'inizio della riga
   $    marca la fine della riga

Questi, in realtà, sono degli elementi a tutti gli effetti, solo che la loro lunghezza è nulla (in termini di numero di caratteri su cui è verificato il match) e la loro posizione è fissata. La regex giusta è allora:

   /^ricette.*\.pdf$/

Già sento il brusio. Qualcuno sta borbottando: "non mi sembra che siamo andati molto oltre le wildcard!". Hey, ci stiamo solo scaldando, quindi calma, eh? Avevamo detto che volevamo qualcosa tipo "inizia con ricette, prosegue con caratteri numerici, finisce con .pdf". Mi sembra che siamo a buon punto, manca solo la roba dei numeri. Qui il problema è che "una sequenza di caratteri numerici" vuol dire "una sequenza di caratteri 1, oppure 2, oppure 3, ecc.". Come si dice "oppure"? Semplice, con la barretta verticale:

   /0|1|2|3|4|5|6|7|8|9/

Qui si pone un problema, però. Se scriviamo:

   /^ricette0|1|2|3|4|5|6|7|8|9*\.pdf$/

otteniamo:

  • inizia con ricette0
  • oppure contiene 1
  • oppure contiene 2
  • ...
  • oppure finisce con una sequenza qualsiasi di 9, seguita da .pdf

Oooops. Stiamo facendo l'OR sugli elementi sbagliati! Che faremmo di solito? Useremmo le parentesi. E qui? Pure:

   /^ricette(0|1|2|3|4|5|6|7|8|9)*\.pdf$/

Funziona? Sì, ma è brutto. È brutto per due motivi: perché è brutto (ok, sono ricorsivo), e perché si rischia di sbagliare. Perl di solito mette a disposizione più frecce per colpire la stessa mela; nel nostro caso, la prima è \d, che vuol dire semplicemente "qualunque carattere numerico":

   /^ricette\d*\.pdf$/

Chiaro, conciso, e comincia a sembrare un'accozzaglia. Ma che potenza! Riecco il brusio. È chiaro che il tizio in seconda fila (che esiste davvero!) vuole fare polemica: "e se volessi solo cifre numeriche da 6 a 9?". Beh, a questo ci penso con la seconda freccia: le classi di caratteri. Un esempio prima di tutto:

   /^ricette[6789]*\.pdf$/

Quello che racchiudo fra parentesi quadre è un insieme di caratteri, ognuno dei quali può andare bene. In pratica, [6789] vuol dire "un qualsiasi carattere fra 6, 7, 8 e 9". Visto che sono pigro, e che soprattutto potrei dimenticare qualcosa, è meglio usare un intervallo:

   /^ricette[6-9]*\.pdf$/

Nelle classi posso mettere anche gli altri caratteri, ovviamente; quindi:

   /^ricette[6-9a-fp-z]*\.pdf$/

vuol dire quello che immaginate: "inizia con ricette, seguito da una qualsiasi sequenza di cifre fra 6 e 9, o di lettere fra a e f, o di lettere fra p e z, seguita dalla stringa di chiusura .pdf". Spero che il tizio della seconda sia contento ora.

E adesso, la canna da pesca

Con tutta questa tiritera non abbiamo visto nemmeno una riga di codice utile. Senza contare che abbiamo solo scalfito la superficie! Però io sono pigro, voi pure, per cui vi do una bella canna da pesca, così da mangiare ve lo trovate da soli.

Quell'impareggiabile archivio che è CPAN contiene un modulo preziosissimo per chi si affaccia nel mondo delle regex: YAPE::Regex::Explain (YAPE sta per Yet Another Parser/Extractor, ossia Ancora un altro parser/estrattore di testo). Voi gli date una regex, lui vi dice cosa fa. L'uso è piuttosto semplice:

#!/usr/bin/perl
use strict;    # Sempre!
use warnings;  # Sempre in debug!
use YAPE::Regex::Explain;

$|++; # Disabilita buffering
print("Regex: ");
while (<>) {
   chomp;            # Elimina il carattere a-capo
   last if /quit/;   # Esci se richiesto
   print(YAPE::Regex::Explain->new($_)->explain());
   print("Regex: ");
}

Il programma effettua un ciclo sull'input, plausibilmente da tastiera. Voi inserite una riga: se contiene la parola quit esce dal ciclo, altrimenti considera quello che avete immesso come una regex, e vi spiega cosa vuol dire. Ad esempio, immettendo una nostra vecchia conoscenza:

   ^ricette[6-9a-fp-z]*\.pdf$

otteniamo:

NODE                     EXPLANATION
----------------------------------------------------------------------
(?-imsx:                 group, but do not capture (case-sensitive)
                         (with ^ and $ matching normally) (with . not
                         matching \n) (matching whitespace and #
                         normally):
----------------------------------------------------------------------
  ^                        the beginning of the string
----------------------------------------------------------------------
  ricette                  'ricette'
----------------------------------------------------------------------
  [6-9a-fp-z]*             any character of: '6' to '9', 'a' to 'f',
                           'p' to 'z' (0 or more times (matching the
                           most amount possible))
----------------------------------------------------------------------
  \.                       '.'
----------------------------------------------------------------------
  pdf                      'pdf'
----------------------------------------------------------------------
  $                        before an optional \n, and the end of the
                           string
----------------------------------------------------------------------
)                        end of grouping
----------------------------------------------------------------------

Il raggruppamento più esterno può essere ignorato, viene sempre aggiunto da Y::R::E per motivi suoi. Per il tale in seconda fila che si agita: sì, sì, sono spiegati nel manuale. Tutto il resto della stampata corrisponde a quanto abbiamo già spiegato sopra.

E per chi non ama la console?

Il tizio in seconda fila oggi non dà tregua (sai che novità...): non gli piacciono gli strumenti a riga di comando! Attenzione, qui chiamiamo la cavalleria:

     1  #!/usr/bin/perl
     2  use strict;
     3  use warnings;
     4  use Tk;
     5  use YAPE::Regex::Explain;
        
     6  my $mw = MainWindow->new();
        
     7  my $top_frame = $mw->Frame();
     8  $top_frame->pack(-side => 'top');
        
     9  my $text = $mw->Scrolled(qw/Text -scrollbars e/);
    10  $text->pack(-side => 'bottom', -expand => 1, -fill => 'both');
        
    11  $top_frame->Label(-text => 'Espressione regolare:')->pack(-side => 'left');
        
    12  my $entry = $top_frame->Entry();
    13  $entry->pack(-side => 'left');
        
    14  $top_frame->Button(
    15     -text    => 'Spiega',
    16     -command => sub {
    17        my $yape = YAPE::Regex::Explain->new($entry->get());
    18        $text->selectAll();
    19        $text->Insert($yape->explain());
    20        $text->unselectAll();
    21     }
    22  )->pack(-side => 'left');
        
    23  MainLoop();

Anche se non è proprio in tema, diciamo cosa fa! Viene creato l'oggetto che rappresenta la finestra principale (riga 6); tale finestra è divisa in due parti, una superiore ($top_frame) ed una inferiore ($text). La prima contiene un'etichetta di testo ("Espressione regolare"), un campo per l'immissione ed il bottone per chiedere la spiegazione (riga 14). La seconda è dove verrà stampata la spiegazione della Regex. Alla pressione del tasto "Spiega", viene chiamata la sub associata al parametro -command, che cancella il contenuto di $text e vi immette la spiegazione data da Y::R::E.

L'eliminazione dei numeri di riga è lasciata come esercizio :)

Per fare un grosso buco con il trapano...

... è consigliabile cominciare con una punta piccola e fare un invito. Questo è esattamente lo scopo di questo articoletto: invitarvi ad entrare nel mondo delle regex, dal quale non uscirete più dopo averne appreso i concetti basilari e le potenzialità.

Il materiale di riferimento è piuttosto copioso. Nel manuale di Perl troviamo il riferimento principe - perlre, in inglese - accanto a dissertazioni introduttive più complete di questa, come ad esempio perlrequick, in italiano, e perlretut, in inglese. Volontari per le traduzioni?