Espressioni Regolari Php
1.Le espressioni regolari

Le espressioni regolari (o regex) definiscono un procedimento che consente di trovare la corrispondenza con stringhe che rispondono a determinati criteri. Il concetto di espressione regolare è di lunga data: discende dall'informatica teorica e si basa sul modello delle macchine a stati finiti. Di seguito sono riportate le principali funzioni PHP che implementano il motore per le espressioni regolari PCRE:

  • preg_match
  • preg_replace
  • preg_split
  • preg_grep

La guida è divisa in due parti: nella prima si spiega la sintassi delle espressioni regolari PCRE, mentre nella seconda sono illustrati alcuni esempi di utilizzo delle regex negli script PHP.

2.Sintassi delle espressioni regolari

Gli elementi di base delle espressioni regolari sono i metacaratteri. È sufficiente eseguire l'escape aggiungendo il carattere backslash (\) per far perdere il significato speciale al metacarattere.


Metacaratteri e loro significato.
Espressione                 Significato
   . Trova corrispondenza con ogni singolo carattere.
   * Trova la corrispondenza con zero o più occorrenze dell'espressione che la precede.
   ? Trova la corrispondenza con 0 oppure 1 occorrenza dell'espressione che la precede; inoltre, rende non greedy l'espressione regolare (più avanti si vedrà un esempio di espressione regolare greedy e non greedy). È utilizzata anche per impostare le opzioni interne.
   + Trova la corrispondenza con 1 o più occorrenze dell'espressione che la precede.
  { } Quantifica l'espressione. \d{3} significa "3 cifre", \s{1,5} significa "uno, due, tre, quattro oppure cinque spazi", Z{1,} significa "una o più lettere Z"; l'ultima espressione è sinonimo di Z+.
   / Delimita l'espressione regolare. Indica l'inizio e la fine dell'espressione regolare.
  [ ] Classi di caratteri: [a-z] indica le lettere minuscole. [Ab9] trova corrispondenza con uno dei caratteri "A", "b" oppure "9". È possibile negare la classe di caratteri scrivendo ^ all'inizio dell'espressione regolare. [^a-z] trova la corrispondenza con qualsiasi carattere diverso dalle lettere minuscole.
   ^ Inizio di una riga.
   $ Fine di una riga.
  ( ) Trova la corrispondenza con gruppi, come verrà spiegato più avanti.
   | Indica l'espressione "or" e separa due sotto-espressioni.

L'espressione regolare .* trova la corrispondenza con qualsiasi carattere. L'espressione ^.*3 indica la corrispondenza con tutti i caratteri da inizio riga fino all'ultima cifra 3 presente nella riga. È anche possibile cambiare il comportamento delle espressioni.


Oltre ai metacaratteri ci sono alcune classi di caratteri speciali:

Classi di caratteri speciali.
Simboli       Significato
\d,\D Il carattere minuscolo \d indica una cifra numerica, mentre il simbolo maiuscolo \D è la sua negazione e indica un carattere diverso da una cifra.
\s,\S Il carattere minuscolo \s trova la corrispondenza con uno spazio, un invio a capo o un carattere di tabulazione. La lettera maiuscola \S indica la sua negazione e trova la corrispondenza con qualsiasi carattere diverso da spazio, invio a capo o tabulazione.
\w,\W La lettera minuscola \w trova la corrispondenza con qualsiasi carattere alfanumerico, ovvero con una lettera oppure con una cifra. Analogamente agli esempi precedenti, \W è la sua negazione e trova la corrispondenza con qualsiasi carattere non alfanumerico.

3.Esempi di espressioni regolari

Innanzitutto consideriamo le date, in questo caso nel formato Sabato, Aprile 30, 2011. Il primo pattern che trova la corrispondenza con una data espressa in questo modo è:

/[A-Z][a-z]{2,},\s[A-Z][a-z]{2,}\s\d{1,2},\s\d{4}/

Il significato dell'espressione regolare è "Una lettera maiuscola, seguita da almeno due lettere minuscole e da una virgola, cui segue uno spazio, una lettera maiuscola, almeno due lettere minuscole, uno spazio, 1 o 2 cifre, una virgola, uno spazio e, infine, quattro cifre per indicare l'anno".

Il Listato che segue è una porzione di codice PHP che sottopone a test le espressioni regolari.

    <?php
    $expr = '/[A-Z][a-z]{2,},\s[A-Z][a-z]{2,}\s\d{1,2},\s\d{4}/';
    $item = 'Sabato, Aprile 30, 2011.';
    if (preg_match($expr, $item)) {
      print "Matches\n";
      }
    else {
    print "Doesn't match.\n";
    }
    ?>

L'espressione regolare valida correttamente il contenuto della variabile $item nonostante la stringa si concluda con un punto. Per evitare questo problema è possibile "ancorare" l'espressione regolare alla fine della riga, scrivendo:

/[A-Z][a-z]{2,},\s[A-Z][a-z]{2,}\s\d{1,2},\s\d{4}$/. Il simbolo del dollaro aggiunto in fondo significa "fine della riga" e indica che l'espressione regolare non trova corrispondenza se dopo l'anno ci sono altri caratteri. Analogamente, si può ancorare l'espressione all'inizio della riga utilizzando il metacarattere ^. L'espressione regolare che trova corrispondenza con l'intera riga, a prescindere dal suo contenuto, è /^.*$/.

Vediamo allora un formato differente per le date, ovvero AAAA-MM-GG. L'attività da svolgere implica l'analisi della data e l'estrazione dei componenti.

È necessario non solo verificare che la riga contenga una data valida, ma occorre anche estrarre il valore dell'anno, del mese e del giorno. Per estrarre queste informazioni si deve trovare la corrispondenza con gruppi di caratteri o sotto-espressioni. I gruppi di caratteri possono essere pensati come sotto espressioni, ordinate in base a un numero in sequenza. Di seguito è riportata l'espressione regolare che esegue direttamente l'attività richiesta:

/(\d{4})-(\d{2})-(\d{2})/ 

Si utilizzano le parentesi per indicare i gruppi da cui estrarre la corrispondenza. I gruppi sono sotto-espressioni, che si possono ritenere analoghe a variabili distinte tra loro. Il Listato A.2 mostra come eseguire le operazioni richieste utilizzando la funzione nativa preg_match.

Listato A.2 Raggruppamenti da far corrispondere con la funzione nativa preg_match.

<?php
$expr = '/(\d{4})-(\d{2})-(\d{2})/';
$item = 'Event date: 2011-05-01';
$matches=array();
if (preg_match($expr, $item,$matches)) {
foreach(range(0,count($matches)-1) as $i) {
printf("%d:-->%s\n",$i,$matches[$i]);
}
list($year,$month,$day)=array_splice($matches,1,3);
print "Year:$year Month:$month Day:$day\n";
} else {
print "Doesn't match.\n";
}
?>

In questo script la funzione preg_match accetta un terzo argomento, l'array $matches. Di seguito è riportato l'output che si ricava dall'esecuzione dello script:

./regex2.php 
0:-->2011-05-01
1:-->2011
2:-->05
3:-->01
Year:2011 Month:05 Day:01

L'elemento di ordine 0 dell'array $matches è la stringa che corrisponde all'intera espressione e non coincide con la stringa completa di input. A seguire, ogni gruppo successivo è rappresentato da un elemento dell'array. Vediamo ora un altro esempio, più complesso, che deve esaminare un URL. In generale, il formato di un URL è:

http://hostname:port/loc?arg=value

Ovviamente, può mancare una parte qualsiasi dell'espressione. Di seguito è indicata un'espressione regolare che esegue il parsing di un URL scritto nella forma definita in precedenza:

/^https?:\/\/[^:\/]+:?\d*\/[^?]*.*/

In questa espressione ci sono diversi nuovi elementi degni di nota. In primo luogo occorre comprendere la parte s? di ^http[s]?:. Questa trova la corrispondenza con http: oppure con https: all'inizio della stringa. Il carattere ^ impone l'ancoraggio dell'espressione all'inizio della riga, mentre il carattere ? significa "0 o 1 occorrenza dell'espressione precedente". In questo caso l'espressione precedente è la lettera s e pertanto il comando diventa "0 o 1 occorrenza della lettera s". Inoltre i caratteri / includono un prefisso definito dal backslash \ per eliminare qualsiasi significato speciale.

PHP è molto indulgente nei confronti dei delimitatori delle espressioni regolari, in quanto consente di modificare il carattere impiegato. PHP riconosce le parentesi o il carattere |, pertanto l'espressione precedente poteva diventare [^https?://[^:/]+:?\d*/[^?]*.*]; in alternativa, si può utilizzare come delimitatore il carattere pipe: |^https?://[^:/]:?\d*/[^?]*.*|. La regola generale che consente di escludere il significato particolare dei caratteri speciali richiede l'inserimento del prefisso costituito da un backslash. Questa procedura di esclusione prende il nome di escape dei caratteri speciali. Le espressioni regolari sono intelligenti al punto da comprendere il significato dei caratteri in un determinato contesto. L'escape del punto interrogativo in [^?]* non era necessario, poiché è chiaro dal contesto che indica la classe di caratteri e non un punto interrogativo. Questa interpretazione non vale per il delimitatore definito da un backslash: in questo caso è stato necessario impostare il suo escape. È interessante notare inoltre la parte [^:\/]+ dell'espressione regolare, il cui significato è "uno o più caratteri diversi da due punti o dalla barra". Questa espressione regolare può essere utile perfino nel caso di URL più complessi. Si consideri il Listato A.3.

Listato A.3 Espressioni regolari e formati URL complessi.

<?php 
$expr = '[^https*://[^:/]+:?\d*/[^?]*.*]';
$item = 'https://myaccount.nytimes.com/auth/login?URI=http://';
if (preg_match($expr, $item)) {
print "Matches\n";
} else {
print "Doesn't match.\n";
}
?>

Questo è il formato URL per il login del New York Times, da cui si possono estrarre gli elementi host, port, directory e la stringa argument, utilizzando i raggruppamenti, analogamente a quanto fatto nel Listato A.2. Si consideri il Listato A.4.

Listato A.4 Estrazione di host, porta, directory e della stringa argument.

<?php 
$expr = '[^https*://([^:/]+):?(\d*)/([^?]*)\??(.*)]';
$item = 'https://myaccount.nytimes.com/auth/login?URI=http://';
$matches = array();
if (preg_match($expr, $item, $matches)) {
list($host, $port, $dir, $args) = array_splice($matches, 1, 4);
print "Host=>$host\n";
print "Port=>$port\n";
print "Dir=>$dir\n";
print "Arguments=>$args\n";
} else {
print "Doesn't match.\n";
}
?>

L'esecuzione dello script produce il seguente output:

./regex4.php 
Host=>myaccount.nytimes.com
Port=>
Dir=>auth/login
Arguments=>URI=http://

4.Opzioni interne

Il valore della porta non è specificato nell'URL, pertanto non c'è nulla da estrarre. Gli altri elementi sono stati estratti correttamente. A questo punto ci si può chiedere cosa sarebbe successo se l'URL fosse stato scritto in lettere maiuscole, per esempio:

HTTPS://myaccount.nytimes.com/auth/login?URI=http://

Questa forma non trova corrispondenza, dato che l'espressione regolare dell'esempio specifica che i caratteri devono essere minuscoli; tuttavia si tratta di un URL valido, che può essere riconosciuto da qualsiasi browser. Nell'espressione regolare occorre ignorare la presenza di lettere maiuscole oppure minuscole, impostando l'opzione "ignora maiuscole e minuscole" all'interno dell'espressione regolare. Tenendo conto di questa considerazione, l'espressione diventa:

[(?i)^https?://([^:/]+):?(\d*)/([^?]*)\??(.*)]

Le corrispondenze che seguono (?i) ignorano la condizione di avere lettere maiuscole oppure minuscole. L'espressione regolare /Mladen (?i)g/ trova corrispondenza con le stringhe "Mladen G" e "Mladen g", ma non con "MLADEN G".

Un'altra opzione utilizzata di frequente è data dalla lettera m, che indica "righe multiple". In genere il parsing dell'espressione regolare si interrompe quando si incontra il carattere di nuova riga \n. È possibile modificare questo comportamento impostando l'opzione (?m), grazie alla quale il parsing si interrompe solo quando si raggiunge la fine dell'input. Anche il simbolo del dollaro trova le corrispondenze dei caratteri di nuova riga, a meno di impostare l'opzione D, il cui significato indica che il metacarattere $ trova corrispondenza solo con la fine dell'input e non i caratteri di nuova riga che si trovano all'interno della stringa.

È possibile raggruppare le opzioni. L'utilizzo di (?imD) all'inizio dell'espressione imposta le tre opzioni simultaneamente: ignora maiuscole e minuscole, prende in considerazione le righe multiple e "il dollaro trova corrispondenza solo con la fine dell'input".

Esiste anche una notazione alternativa e più convenzionale, che consente di impostare le opzioni globali con modificatori da specificare dopo aver delimitato l'espressione regolare. Adottando questa notazione, l'espressione regolare dell'esempio diventa

[^https?://([^:/]+):?(\d*)/([^?]*)\??(.*)]i

Il vantaggio della nuova notazione è che può essere indicata in un punto qualsiasi dell'espressione, e in questo caso avrà effetto solo sulla parte dell'espressione che segue il delimitatore, mentre impostandola nella posizione che segue l'ultimo delimitatore avrà effetto sull'intera espressione regolare.

5.Comportamento greedy

In genere le espressioni regolari manifestano un comportamento greedy (vorace). Ciò significa che il parsing tenta di trovare corrispondenze con la massima porzione possibile della stringa di input. Utilizzare l'espressione regolare '(123)+' con la stringa di input 123123123123123A significa che si trova corrispondenza con tutto quello che precede la lettera A. Di seguito è riportato uno script di esempio. Si vuole estrarre il tag img solo dalla riga HTML e non da altri tag. La prima versione dello script non funziona in modo adeguato ed è costituita dalle istruzioni del Listato A.5.

Listato A.5 Prima versione dello script con comportamento greedy.

<?php 
$expr = '/<img.*>/';
$item = '<a><img src="file">text</a>"';
$matches=array();
if (preg_match($expr, $item,$matches)) {
printf( "Match:%s\n",$matches[0]);
} else {
print "Doesn't match.\n";
}
?>

L'esecuzione dello script produce l'output:

./regex5.php 
Match:<img src="file">text</a>

Alcuni browser, in particolare Google Chrome, tentano di correggere il markup non corretto, pertanto l'output greedy e quello non greedy ignorano il tag isolato </a>.

Le corrispondenze sono in numero superiore a quelle desiderate, poiché il pattern .*> ha trovato corrispondenza con tutti i caratteri possibili fino a raggiungere l'ultimo >, che fa parte del tag </a> e non del tag <img>. Il punto interrogativo fa diventare non greedy gli elementi * e +, che trovano il numero minimo di caratteri di corrispondenza, non quello massimo. È sufficiente modificare l'espressione regolare in '<img.*?>' perché il pattern trovi corrispondenza fino a quando incontra il primo > e produca il risultato desiderato:

Match:<img src="file">

Il parsing di codice HTML o XML è una situazione tipica in cui si usano modificatori non greedy, proprio perché è necessario trovare la corrispondenza con i limiti del tag.

6.Funzioni PHP per le espressioni regolari

Quanto visto finora ha riguardato la verifica che una determinata stringa trovi corrispondenza con l'espressione specificata, scritta nella forma contorta delle espressioni PCRE, seguita dall'estrazione di elementi della stringa, in base a quanto indicato nell'espressione regolare. Con le espressioni regolari si possono svolgere altre operazioni, per esempio sostituire stringhe oppure suddividerle in array. Questo paragrafo illustra le altre funzioni PHP che implementano il meccanismo delle espressioni regolari, che si aggiungono all'ormai nota preg_match. La funzione più interessante è preg_replace.

7.Sostituzione di stringhe: preg_replace

La funzione preg_replace utilizza la sintassi indicata di seguito:

$result = preg_replace($pattern,$replacement,$input,$limit,$count);

Il significato degli argomenti $pattern, $replacement e $input dovrebbe risultare chiaro. L'argomento $limit limita il numero di sostituzioni: il valore –1 significa nessun limite ed è l'opzione di default. L'ultimo argomento, $count, viene impostato dopo aver effettuato le sostituzioni e, se definito, contiene il numero di sostituzioni andate a buon fine. Tutto ciò può sembrare piuttosto semplice, ma occorre non trascurare le ulteriori ramificazioni della funzione. Innanzitutto, va ricordato che il pattern e la riga da sostituire possono essere array, come nell'esempio del Listato A.6.

Listato A.6 Il pattern e la stringa da sostituire sono array.

<?php 
$cookie = <<<'EOT'
Now what starts with the letter C?
Cookie starts with C
Let's think of other things that starts with C
Uh ahh who cares about the other things

C is for cookie that's good enough for me
C is for cookie that's good enough for me
C is for cookie that's good enough for me

Ohh cookie cookie cookie starts with C
EOT;
$expression = array("/(?i)cookie/", "/C/");
$replacement = array("donut", "D");
$donut = preg_replace($expression, $replacement, $cookie);
print "$donut\n";
?>

L'esecuzione dello script produce un risultato poco interessante:

./regex6.php 
Now what starts with the letter D?
donut starts with D
Let's think of other things that starts with D
Uh ahh who cares about the other things

D is for donut that's good enough for me
D is for donut that's good enough for me
D is for donut that's good enough for me

Ohh donut donut donut starts with D

L'aspetto più importante da notare è che pattern e stringa da sostituire sono array che devono avere lo stesso numero di elementi. Se i valori da sostituire sono inferiori ai pattern, allora le stringhe che mancano sono sostituite da stringhe null, il che vanifica le corrispondenze relative alle stringhe ulteriori che sono specificate nell'array dei pattern.

Le potenzialità delle espressioni regolari diventano evidenti nel Listato A.7. Lo script produce comandi truncate table SQL-ready che si rifanno all'elenco disponibile dei nomi di tabella. Si tratta di un'attività piuttosto comune. Per brevità di trattazione, nell'esempio l'elenco delle tabelle è già incluso in un array, anche se in genere deve essere letto da un file.

Listato A.7 Espressioni regolari e sostituzioni di stringhe.

<?php 
$tables = array("emp", "dept", "bonus", "salgrade");
foreach ($tables as $t) {
$trunc = preg_replace("/^(\w+)/", "truncate table $1;", $t);
print "$trunc\n";
}
L'esecuzione dello script produce il risultato che segue:
./regex7.php 
truncate table emp;
truncate table dept;
truncate table bonus;
truncate table salgrade;

L'utilizzo di preg_replace evidenzia parecchie cose. Innanzitutto, l'espressione regolare include un gruppo (\w+). Nel paragrafo precedente è stata studiata l'estrazione degli elementi di una data da una stringa nel Listato A.2. Il gruppo compare anche nell'argomento da sostituire come $1. Il valore di ogni sotto-espressione viene acquisito dalla variabile $n, dove n può variare da 0 a 99. Ovviamente, come per preg_match, la variabile $0 contiene l'intera espressione di corrispondenza, mentre le variabili successive contengono i valori delle sotto-espressioni, numerate da sinistra a destra. È da notare inoltre la presenza delle doppie virgolette. Non c'è pericolo di confondere la variabile $1 con qualcosa d'altro, poiché le variabili espresse nella forma $n, 0<=n<=9 sono riservate e non possono impiegate in altre parti dello script. I nomi delle variabili PHP devono iniziare con una lettera o con il carattere di underscore, come indicato dalle specifiche del linguaggio.

8.Altre funzioni per le espressioni regolari

Ci sono altre due funzioni per le espressioni regolari che vale la pena citare: preg_split e preg_grep. La prima, preg_split, è più potente della funzione explode, che suddivide la stringa di input in un array di elementi in base alla stringa di delimitazione indicata. In altri termini, se la stringa di input è $a="A,B,C,D", allora la funzione explode, impostata con la stringa "," come separatore, produce un array che include gli elementi "A", "B", "C" e "D". La domanda da farsi è: come si suddivide la stringa nel caso in cui il separatore non ha un formato fisso e la stringa è $a='A, B,C .D'? In questo esempio ci sono caratteri spazio prima e dopo la virgola di separazione e inoltre è presente un punto di separazione, il che rende impossibile utilizzare semplicemente la funzione explode. preg_split non ha alcun problema di questo genere. Basta impostare l'espressione regolare che segue per suddividere la stringa nei suoi componenti:

$result=preg_split('/\s*[,.]\s*/',$a);

Il significato dell'espressione regolare è "0 o più spazi, seguiti da un carattere che può essere un punto oppure virgola, seguito da 0 o più spazi". L'inserimento dell'esecuzione di un'espressione regolare è ovviamente un'operazione più dispendiosa del semplice confronto tra stringhe, pertanto è bene non impiegare la funzione preg_split quando è sufficiente impostare la funzione explode, tuttavia è interessante sapere che si ha a disposizione uno strumento così potente. I costi aggiuntivi derivano dal fatto che le espressioni regolari sono operazioni complesse per il compilatore.

Le espressioni regolari non fanno magie. Richiedono attenzione e prove accurate. Se non sono studiate con cura, i risultati possono essere diversi da quelli desiderati e non adeguati. L'impiego di un'espressione regolare al posto di una funzione nativa non è di per sé garanzia dell'ottenimento dei risultati voluti.

La funzione preg_grep dovrebbe essere familiare a chi utilizza l'utility da riga di comando chiamata grep, da cui trae il nome. Di seguito è indicata la sintassi di preg_grep:

$results=preg_grep($pattern,$input);

La funzione preg_grep valuta l'espressione regolare $pattern per ciascun elemento dell'array di input $input e memorizza l'output corrispondente nell'array dei risultati. In definitiva, si ottiene un array associativo con offset che derivano dall'array originale, forniti come chiavi. Il Listato A.8 illustra un esempio basato sull'utility grep del file system:

Listato A.8 Array associativo e utility grep.

<?php 
$input = glob('/usr/share/pear/*');
$pattern = '/\.php$/';
$results = preg_grep($pattern, $input);
printf("Total files:%d PHP files:%d\n", count($input), count($results));
foreach ($results as $key => $val) {
printf("%d ==> %s\n", $key, $val);
}
?>

Il punto nell'estensione .php ha richiesto l'escape con un carattere backslash, perché il punto è un metacarattere; per escludere il suo significato speciale è stato aggiunto il prefisso \. Nel computer impiegato per scrivere questa appendice, l'esecuzione dello script ha prodotto come risultato

./regex8.php 
Total files:35 PHP files:12
4 ==> /usr/share/pear/DB.php
6 ==> /usr/share/pear/Date.php
8 ==> /usr/share/pear/File.php
12 ==> /usr/share/pear/Log.php
14 ==> /usr/share/pear/MDB2.php
16 ==> /usr/share/pear/Mail.php
19 ==> /usr/share/pear/OLE.php
22 ==> /usr/share/pear/PEAR.php
23 ==> /usr/share/pear/PEAR5.php
27 ==> /usr/share/pear/System.php
29 ==> /usr/share/pear/Var_Dump.php
32 ==> /usr/share/pear/pearcmd.php

Il risultato può essere diverso in un file system differente, dove ci possono essere altri moduli PEAR installati. La funzione preg_grep consente di evitare l'esecuzione di un ciclo di controllo delle espressioni regolari, il che è decisamente utile.