Espressioni regolari

Introduzione

Mi è capitato di osservare che alcune chiavi di ricerca che hanno portato al mio sito sono tipicamente italiane e alcune di queste riguardano le espressioni regolari o regex. Sul mio sito ne parlo brevemente perché possono essere usate nella sezione di ricerca degli articoli. In particolare affronto le espressioni regolari POSIX.

Conviene ricordare che, in linea di principio, non esistono Le Espressioni Regolari, cioè non c'è un solo modo per dire a un computer, attraverso un linguaggio di programmazione, come cercare pattern in stringhe (sequenze di caratteri), o fare compiti simili.

Esistono degli standard diffusi, o delle convenzioni diffuse, e la loro diffusione è più o meno legata alla diffusione di un linguaggio o di una libreria che fornisce le funzioni necessarie per poter implementare le regex in proprie applicazioni. In inglese si trova ogni sorta di informazione voluta a riguardo.

Tanto per fare un esempio di differenze: il *.txt potrebbe essere inteso come una espressione che cattura tutte le parole terminanti in punto ti-ics-ti, anche se più comunemente in tal caso si parla di globbing; la stessa cosa nelle regex POSIX ma anche in quelle fornite dal Perl (che sono una estensione di quelle POSIX), e dunque in tutti i casi in cui ci si attiene a tali standard, si direbbe .*\.txt; usando il pattern matching stile Amiga, si direbbe #?.txt.

Possiamo dire comunque che la base più usata è quella POSIX, a cui poi possiamo trovare estensioni varie e piuttosto uniformi, come le regex usante in Perl, di stile (e sostanza) del tutto equivalente a quelle che possiamo per esempio usare in JavaScript, a partire dalla version 1.2 (e dunque diciamo dall'ECMA-262 prima edizione; ma le regex che vedremo qua sono tutte disponibili solo dall'ECMA-262 terza edizione, cioè dallo JavaScript version 1.5).

Note su Microsoft Windows

Nell'MSDN 2005 è detto che gli oggetti RegEx in JavaScript sono una estensione della Microsoft. Questo è falso inquanto il documento in mio possesso Core JavaScript Guide 1.5, conforme allo standard ECMA-262 terza edizione, risale al 2000. Dunque se avete a che fare con le regex nel mondo Windows prendete cum grano salis le affermazioni dell'MSDN e tenete presente che potete scrivere codice usabile anche da altri browser che implementano JavaScript 1.5, senza dover differenziare il codice.

In Microsoft Windows (almeno prima di Vista, che non credo abbia cambiato le regex, però forse ha eletto una nuova tecnologia di scripting) è dunque possibile usare le espressioni regolari che vedremo qua per esempio in JScript (così lo chiamano) o VBScript, linguaggi di scripting presenti in Windows.

Le espressioni regolari

Esempi di uso

Le espressioni regolari sono utilissime per identificare dei pattern (figurazioni?) in stringhe, dove una parte è variabili secondo uno schema noto; per validare indirizzi e-mail o URI in generale; per estrarre informazioni da stringhe.

Vediamo degli esempi in cui l'uso delle regex è fondamentale:

  1. la stringa in analisi deve contenere la parola Windows seguita da uno o più spazi (o altri carattere separatori come tab etc.) e poi da XP, o NT, o ME oppure una versione, cioè N.N (numero punto numero), oppure un anno (due cifre o 4 solo per 2000)
  2. un indirizzo e-mail è formato da una prima parte, che contiene solo lettere, numeri e alcuni altri caratteri (cioè _ oppure - oppure .), seguita da @ seguita da una stringa nella forma DOMINIO.TIPO; DOMINIO può contenere caratteri, numeri e alcuni segni speciali (_-. di nuovo) mentre TIPO no, e inoltre potremmo verificare che sia una estensione nota, come it, oppure org e così via (sono molte).
  3. estrarre nome e cognome avendo una stringa del tipo name: Pinco; surname: Pallino.

Tanto per riscaldarci, ecco delle possibili soluzioni:

  1. Windows\s+(NT|XP|ME|\d+\.\d+|\d\d|2000); questo ci dà un risultato positivo con This is Windows NT, o This Windows XP sucks, oppure Questo programma gira su Windows 3.1, oppure Non è Windows 98, ma Windows 2000 (in questo caso, attenzione: solo uno dei due match è considerato); inoltre avremo a disposizione il testo catturato dalle parentesi, ovvero negli esempio: NT, XP, 3.1, 98.
  2. ^[\w-.]+@[A-Za-z][\w-.]*\.(it|org|com|us)$ valida indirizzi mail tipo pippo@topo.it ma non indirizzi come pippo@.topo.it, e nemmeno qualunque altro che non termini in it, org, com o us... Inoltre l'estensione è catturata (è tra parentesi) così può essere controllata per qualche motivo.
  3. name:\s*(\w+);\s*surname:\s*(\w+) questo estrae Pinco e Pallino

Ora che forse abbiamo un'idea, vediamole più sistematicamente.

Caratteri normali

Se vogliamo sapere semplicemente se una stringa contiene una nostra sottostringa, l'espressione regolare sarà la sottostringa stessa, dove avremo avuto però cura di anteporre ai caratteri speciali che vedremo a breve il carattere di escape \.

Per esempio se vogliamo controllare che la stringa Non sono parolacce non contenga la sottostringa on s, useremo l'espressione on s, che nel caso specifico darà vero.

Alternative

Consideriamo la semplice espressione regolare abc, che dà vero se la stringa in esame contiene la sottostringa abc. L'espressione regolare abc|def ritorna vero se esiste la sottostringa abc o (inclusivo) la sottostringa def.

In generale | separa diverse alternative. E dunque

A|B

darà una corrispondenza se l'espressione regolare (comunque complessa) A dà una corrispondena oppure B dà una corrispondenza. Dunque, sottolineamo che A e B qui sono due espressioni regolari qualunque.

Essendo | un carattere speciale, per trovare la stringa 0x0f|val in un sorgente C (o altro) dovremo scrivere \|, e dunque l'espressione regolare generica sarà: 0x[A-Fa-f\d]+\s*\|\s*val.

Qualunque

Per indicare un carattere qualunque si usa un punto, .. Per matchare abaco, obici, abici etc. possiamo usare .b.c., questo beccherà qualunque sequenza di caratteri del tipo XbXcX, dove X è appunto una qualunque cosa, anche uno spazio, un numero, un tab etc.

Per cercare un punto dovremo usare \..

Caratteri speciali e classi di caratteri

Attraverso i caratteri speciali, introdotti da \ seguito da una lettera o altro (vedremo), possiamo considerare non solo i caratteri raggiungibili attraverso la tastiera, ma anche sequenze di controllo e quanto altro.

Ecco i caratteri speciali

Le classi di caratteri permettono di specificare in una volta sola un insieme di caratteri, una sorta di famiglia.

Esistono di alcuni le negazioni, cioè le famiglie complementari:

La sintassi più generale per specificare una famiglia o insieme di caratteri è specificarli tra parentesi quadre. Per esempio [abc] corrisponde a un carattere qualunque tra quelli nell'insieme specificato. Possiamo usare le famiglie precedenti: [\d\s] indica un carattere qualunque tra cifre e caratteri di spaziatura.

Per costruire l'insieme complementare, basta che il primo carattere dopo la parentesi quadra sia ^: [^\d] è equivalente a \D, e [^abc] è un qualunque carattere che non sia a o b o c.

Possiamo specificare anche degli intervalli, qualora abbia un senso l'ordinamento (in generale vale la pena solo per alfabetici e numerici): [A-Z] corrisponde a una lettera maiuscola qualunque; [A-Za-z0-9_] è equivalente a \w e così via.

Se vogliamo mettere nell'insieme il meno, ovviamente lo dovremo mettere in coda per evitare ambiguità di interpretazione; p. es. per prendere un carattere che sia o A, o Z o - dovremo scrivere [AZ-], o anche [-AZ].

Similmente se vogliamo nell'elenco ^, non lo metteremo in prima posizione dove il suo significato è di negazione. Possiamo dunque dire che la negazione dell'insieme è [^ e non il solo ^ che se non preceduto dalle quadre o se non è il primo carattere dell'espressione regolare (cfr. dopo), viene considerato come carattere normale.

Le classi brevi (\+carattere) su viste sono dunque delle abbreviazioni; per esempio \s è [\f\n\r\t\v\u00A0\u2028\u2029].

Lo standard POSIX (esteso?) contempla anche la sintassi, usabile nelle quadre, [:nome_classe:], tipo [:digit:]. Sono:

Al solito per non far interpretare come inizio o fine di insieme le quadre, le possiamo far precedere da \. Se non c'è una aperta, possiamo anche scrivere la chiusa senza il backslash (non so se alcune implementanzioni possono lamentarsi): abaco] cattura la sottostringa abaco seguita da una quadra chiusa.

Quantificatori

Attraverso i quantificatori possiamo dire quanti oggetti del tipo precedente ci aspettiamo. Per esempio per indicare che una sequenza di zero o più caratteri di qualunque tipo usiamo .*: il punto indica un carattere qualunque, mentre l'asterisco indica che ce ne possono essere 0 o più; dunque prendiamo sia la stringa vuota, sia una qualunque seguenza di caratteri. Ora dovrebbe essere chiaro che .*\.txt corrisponde a un nome di file qualunque terminante con l'estensione txt.

I quantificatori si pongono dopo il carattere o il gruppo (che vedremo) che vogliamo quantificare.

*
Zero o più del precedente
?
Zero o una occorrenza del precedente
+
Una o più occorrenze del precedente
{N}
Esattamente N occorrenze del precedente
{N,M}
Minimo N, massimo M occorrenze del precedente
{N,}
Minimo N occorrenze del precedente

Come si comportano i quantificatori quando c'è una corrispondenza corta e una lunga? Per esempio a.*ba in pensa alla base in batteria c'è a alla ba ma anche a alla base in ba (considerando l'ancora sempre sulla prima a). Normalmente viene presa la corrispondenza più lunga. Per cambiare questo comportamento aggiungiamo un ? dopo il quantificatore: a.*?ba prende la corrispondenza più corta.

Similmente vale per gli altri quantificatori.

Punti di riferimento

Normalmente la corrispondenza viene cercata nella stringa in una posizione qualunque. Ma se vogliamo ancorare la ricerca all'inizio o alla fine, o a qualche altra condizione possiamo usare altri caratteri speciali opportuni.

^
All'inizio, e solo all'inizio dell'espressione regolare, fa sì che questa sia applicata solo all'inizio della stringa: ^abaco non trova nulla in Questo abaco perché abaco non compare all'inizio; mentre dà una corrispondenza in abaco sconosciuto. Se vogliamo trovare un ^ all'inizio, dovremo usare il backslask: \^2 trova qualcosa in x^2 per esempio (notazione TeX per scrivere x2)
$
Ancora alla fine della stringa: ciao$ trova qualcosa solo se ciao compare alla fine della stringa.
\b
Confinamento di parola: aggancia la ricerca lì dove può iniziare o finire una parola; per esempio i trova le i in Questi istrioni sono buoni (il primo match), mentre i\b trova solo le i a fine parola, oppure \bi a inizio parola.
\B
La negazione del precedente.

Raggruppamenti

Con cattura

Le parentesi tonde raggruppano espressioni regolari e nello stesso tempo memorizzano la corrispondenza. Come questa poi sia accessibile dipenderà dal linguaggio di programmazione in uso.

A parte nel linguaggio però possiamo usare quanto catturato anche nell'espressione regolare stessa.

Per esempio Win(\d{1,2}) cattura il numero che segue Win, per esempio Win8, Win16, Win32, Win64... ma anche, perché sia chiaro, cattura 12 in Win128!

Riferimenti all'indietro (back referencing)

Quando usiamo le parentesi tonde (raggruppamenti con cattura), catturiamo la corrispondenza e possiamo successivamente usarla.

La sintassi è \N dove N è un numero maggiore di 0.

Per esempio [a-z]+([aeio])\s*?bell\1 trova corrispondenze in sostantivi (parole) seguiti dall'aggettivo bello che concorda con l'ultima lettera, trova dunque corrispondenze in casa bella, ramo bello e così via, ma non in eco bella oppure cane bello... né tantomeno se la prima parola non termina in una delle vocali dell'insieme dato.

Senza cattura

Talvolta è necessario raggruppare ma è inutile forzare il sistema a memorizzare il match del gruppo. In tal caso si usa la sintassi (?:X) dove X è l'espressione regolare in questione.

Per esempio Win\(?:\d+|XP) trova WinXP oppure Win seguito da un numero di una o più cifre, senza possibilità di usare quanto catturato, né nel programma né nell'espressione stessa tramite i riferimenti all'indietro.

Match condizionati

Alcune espressioni ci consentono di guardare in avanti (look ahead) e qui io le ho definite, arbitrariamente, match condizionati.

A(?=B)
Trova A solo se è seguito da B. Per esempio Win(?=XP|NT|32) trova Win in WinXP ma non in WinPOP oppure in Zak32
A(?!B)
L'opposto di prima: trova A solo se non è seguito da B.

Qualche aggiunta dal Perl

Le espressioni regolari fin qui viste sono disponibili per esempio in JavaScript (tranne quando ho parlato di classi di caratteri con la sintassi [:NOME:], che è POSIX). Il Perl accetta lo standard POSIX. Inoltre accetta altre sintassi sue, di cui qui metto qualcosa:

\N{NOME}
Un carattere con nome standard NOME. Per esempio \N{Sigma} corrisponde a una sigma greca maiuscola.
(?<=X)A
Trova A solo se è preceduto da X. X deve essere una espressione a lunghezza fissa (non si possono usare *, + e simili)
(?<!X)A
Trova A solo se non è preceduto da X. X deve essere una espressione a lunghezza fissa, cfr. prima.

Anche il PHP (tramite apposita libreria) permette di usare le espressioni regolari con la sintassi Perl (tra l'altro si dice che siano più efficienti)

Le espressioni regolari in Amiga

Come vi dovrebbe essere chiaro ormai le regex sono una sintassi per specificare pattern anche piuttosto complessi. A livello dell'implementazione non sono altro che un pezzo di software e dunque basta che qualcuno fornisca le opportune funzioni, e che qualcuno le usi, ed ecco che abbiamo su questo o quel sistema operativo, su questo o quel linguaggio le espressioni regolari tal dei tali. La libreria PCRE fornisce per esempio tutto quanto serve per usare le regex Perl.

Ciò detto... l'Amiga ha il supporto per le sue regex nel sistema operativo (usate specialmente per il pattern matching di nomi di file o cartelle, come filtro nei requester etc.), cioè ci sono le apposite funzioni facenti parte del sistema per usare le espressioni regolari.

Non mi risulta ci siano cose come look ahead o look behind, abbreviazioni per classi di caratteri e altre cosette (almeno non fino ad AmigaOS 3.1). Tuttavia ci sarebbe stato sempre spazio per migliorarle ed espanderle...

Qui volevo dare qualche rudimento sparso, tanto per farsene un'idea.

Facciamo qualche esempio:


Home page