Le espressioni regolari sono il coltellino svizzero per la ricerca dell’informazione attraverso certi schemi (pattern). Hanno una vasta gamma di di strumenti, alcuni dei quali sono spesso nascosti o sottoutilizzati. Oggi vi mostrerò alcuni trucchi per lavorare con le espressioni regolari.
Aggiunta di commenti
A volte le espressioni regolari possono diventare complesse e illeggibili. Una espressione che oggi scrivi può sembrare troppo oscura domani anche se l’hai scritta tu. Proprio come nella programmazione in generale, è una buona idea aggiungere commenti per migliorare la leggibilità delle espressioni regolari.
Ad esempio, ecco qualcosa che potrebbe essere utilizzato per controllare i numeri di telefono degli Stati Uniti.
preg_match(“/^(1[-\s.])?(\()?\d{3}(?(2)\))[-\s.]?\d{3}[-\s.]?\d{4}$/”,$number)
Può diventare molto più leggibile con commenti e qualche spaziatura extra.
preg_match(“/^
(1[-\s.])? # optional ‘1-‘, ‘1.’ or ‘1’
( \( )? # optional opening parenthesis
\d{3} # the area code
(?(2) \) ) # if there was opening parenthesis, close it
[-\s.]? # followed by ‘-‘ or ‘.’ or space
\d{3} # first 3 digits
[-\s.]? # followed by ‘-‘ or ‘.’ or space
\d{4} # last 4 digits
$/x”,$number);
Mettiamola all’interno di un segmento di codice.
$numbers = array(
“123 555 6789”,
“1-(123)-555-6789”,
“(123-555-6789”,
“(123).555.6789”,
“123 55 6789”);
foreach ($numbers as $number) {
echo “$number is “;
if (preg_match(“/^
(1[-\s.])? # optional ‘1-‘, ‘1.’ or ‘1’
( \( )? # optional opening parenthesis
\d{3} # the area code
(?(2) \) ) # if there was opening parenthesis, close it
[-\s.]? # followed by ‘-‘ or ‘.’ or space
\d{3} # first 3 digits
[-\s.]? # followed by ‘-‘ or ‘.’ or space
\d{4} # last 4 digits
$/x”,$number)) {
echo “valid\n”;
} else {
echo “invalid\n”;
}
}
/* prints
123 555 6789 is valid
1-(123)-555-6789 is valid
(123-555-6789 is invalid
(123).555.6789 is valid
123 55 6789 is invalid
*/
Il trucco è quello di utilizzare il modificatore ‘x’ alla fine dell’espressione regolare. Questo fa sì che gli spazi bianchi vengano ignorati nel pattern, se non vengono sostituiti dalla corrispondente sequenza di escape (\s). Ciò rende più semplice aggiungere commenti. I commenti iniziano con ‘#’ e terminano con la nuova riga.
Utilizzare le callback
In PHP preg_replace_callback () possono essere utilizzate per aggiungere funzionalità di callback alle sostituzione nelle espressioni regolari.
A volte è necessario effettuare più sostituzioni. Se si chiama preg_replace () o str_replace() per ogni pattern, la stringa viene analizzata più e più volte.
Diamo un’occhiata a questo esempio, dove abbiamo un modello per la posta elettronica.
$template = “Hello [first_name] [last_name],
Thank you for purchasing [product_name] from [store_name].
The total cost of your purchase was [product_price] plus [ship_price] for shipping.
You can expect your product to arrive in [ship_days_min] to [ship_days_max] business days.
Sincerely,
[store_manager_name]”;
// assume $data array has all the replacement data
// such as $data[‘first_name’] $data[‘product_price’] etc…
$template = str_replace(“[first_name]”,$data[‘first_name’],$template);
$template = str_replace(“[last_name]”,$data[‘last_name’],$template);
$template = str_replace(“[store_name]”,$data[‘store_name’],$template);
$template = str_replace(“[product_name]”,$data[‘product_name’],$template);
$template = str_replace(“[product_price]”,$data[‘product_price’],$template);
$template = str_replace(“[ship_price]”,$data[‘ship_price’],$template);
$template = str_replace(“[ship_days_min]”,$data[‘ship_days_min’],$template);
$template = str_replace(“[ship_days_max]”,$data[‘ship_days_max’],$template);
$template = str_replace(“[store_manager_name]”,$data[‘store_manager_name’],$template);
// this could be done in a loop too,
// but I wanted to emphasize how many replacements were made
Si noti che tutte le sostituzioni hanno qualcosa in comune. Esse sono sempre stringhe racchiuse tra parentesi quadre. Possiamo catturarle tutte con una sola espressione regolare e gestire le sostituzioni in una funzione di callback.
Questo è il modo migliore di farlo con le callback:
// …
// this will call my_callback() every time it sees brackets
$template = preg_replace_callback(‘/\[(.*)\]/’,’my_callback’,$template);
function my_callback($matches) {
// $matches[1] now contains the string between the brackets
if (isset($data[$matches[1]])) {
// return the replacement string
return $data[$matches[1]];
} else {
return $matches[0];
}
}
Ora la stringa nel $template viene analizzata una sola volta dall’espressione regolare.
Greedy e non-greedy
Prima di iniziare spiegando questo concetto, vorrei mostrare un esempio. Diciamo che stiamo cercando di trovare il tag ancora (<a>) in un testo html:
$html = ‘Hello <A href=”/world”>World!</A>’;
if (preg_match_all(‘/<A.*>.*<\/a>/’,$html,$matches)) {
print_r($matches);
}
Il risultato sarà come previsto:
/* output:
Array
(
[0] => Array
(
[0] => <A href=”/world”>World!</A>
)
)
*/
Adesso cambiamo l’ingresso e aggiungiamo una secondo tag ancora:
$html = ‘<A href=”/hello”>Hello</A>
<A href=”/world”>World!</A>’;
if (preg_match_all(‘/<A.*>.*<\/a>/’,$html,$matches)) {
print_r($matches);
}
/* output:
Array
(
[0] => Array
(
[0] => <A href=”/hello”>Hello</A>
[1] => <A href=”/world”>World!</A>
)
)
*/
Ancora una volta, sembra che vada tutto bene. Ma non lasciatevi ingannare. L’unica ragione per cui funziona è perché i tag ancora sono su righe separate, e per impostazione predefinita PCRE confronta i pattern solo una riga alla volta (maggiori informazioni su: Pattern Modifiers). Se si incontrano due tag ancora, sulla stessa linea, non funzionerà come previsto:
$html = ‘<A href=”/hello”>Hello</A> <A href=”/world”>World!</A>’;
if (preg_match_all(‘/<A.*>.*<\/a>/’,$html,$matches)) {
print_r($matches);
}
/* output:
Array
(
[0] => Array
(
[0] => <A href=”/hello”>Hello</A> <A href=”/world”>World!</A>
)
)
*/
Questa volta il pattern trova corrispondenza tra il primo tag di apertura e ultimo tag di chiusura e tutto quello che sta nel mezzo è una singola corrispondenza, invece di recuperare due corrispondenze distinte. Ciò è dovuto al fatto che per comportamento predefinito è “goloso” (greedy).
Il comportamento di default dei quantificatori (come * e +) trovano corrispondenza nel maggior numero di caratteri possibili
Se si aggiunge un punto interrogativo dopo il quantificatore (.*?) diventa “non-greedy”:
$html = ‘<A href=”/hello”>Hello</A> <A href=”/world”>World!</A>’;
// note the ?’s after the *’s
if (preg_match_all(‘/<A.*?>.*?<\/a>/’,$html,$matches)) {
print_r($matches);
}
/* output:
Array
(
[0] => Array
(
[0] => <A href=”/hello”>Hello</A>
[1] => <A href=”/world”>World!</A>
)
)
*/
Ora il comportamento è corretto. Un altro modo per attivare il comportamento non-greedy consiste nell’utilizzare il modificatore U.
Le asserzioni Lookahead e Lookbehind
Un’asserzione lookahead cerca una corrispondenza che segue la corrispondenza corrente. Questo potrebbe essere spiegato più facilmente attraverso un esempio.
Il modello seguente prima cerca la corrispondenza di ‘foo’, e poi controlla per vedere se è seguito da ‘bar’:
$pattern = ‘/foo(?=bar)/’;
preg_match($pattern,’Hello foo’); // false
preg_match($pattern,’Hello foobar’); // true
Non sembra molto utile, potremmo controllare semplicemente ‘foobar’. Tuttavia, è anche possibile utilizzare un asserzione lookaheads per fare affermazioni negative. Nell’esempio seguente viene ricercata la corrisponde ‘foo’, solo se non è seguita da ‘bar’.
$pattern = ‘/foo(?!bar)/’;
preg_match($pattern,’Hello foo’); // true
preg_match($pattern,’Hello foobar’); // false
preg_match($pattern,’Hello foobaz’); // true
Le asserzioni Lookbehind funzionano allo stesso modo, ma guardano i pattern prima della corrispondenza corrente. Si può usare (?< per asserzioni positive, e (?
Il pattern seguente trova una corrispondenza se c’è un ‘bar’ non seguito da ‘foo’.
$pattern = ‘/(?<!foo)bar/’;
preg_match($pattern,’Hello bar’); // true
preg_match($pattern,’Hello foobar’); // false
preg_match($pattern,’Hello bazbar’); // true
Patterns condizionali (if-then-else)
Le espressioni regolari forniscono le funzionalità per il controllo condizionale. Il formato è il seguente:
(?(condition)true-pattern|false-pattern)
or
(?(condition)true-pattern)
La condizione può essere un numero. In questo caso si riferisce ad una sotto-regola precedentemente catturata.
Per esempio possiamo usarlo per controllare l’apertura e la chiusura di parentesi angolari:
$pattern = ‘/^(<)?[a-z]+(?(1)>)$/’;
preg_match($pattern, ‘<test>’); // true
preg_match($pattern, ‘<foo’); // false
preg_match($pattern, ‘bar>’); // false
preg_match($pattern, ‘hello’); // true
Nell’esempio precedente, ’1′ si riferisce al sottomodello (<), che è facoltativo in quanto è seguito da un punto interrogativo. Solo se questa condizione è vera, viene cercata la corrispondenza con la parentesi angolare chiusa.
La corrispondenza può essere anche un’asserzione:
// if it begins with ‘q’, it must begin with ‘qu’
// else it must begin with ‘f’
$pattern = ‘/^(?(?=q)qu|f)/’;
preg_match($pattern, ‘quake’); // true
preg_match($pattern, ‘qwerty’); // false
preg_match($pattern, ‘foo’); // true
preg_match($pattern, ‘bar’); // false
Filtraggio dei patterns
Ci sono varie ragioni per filtrare l’input quando si sviluppano applicazioni web. Dobbiamo pulire i dati prima di inserirli in un database, o mostrarli in output al browser. Allo stesso modo, è necessario filtrare qualsiasi stringa arbitraria prima di inserirla in un’espressione regolare. PHP fornisce una funzione denominata preg_quote per fare questo lavoro.
Nell’esempio seguente, usiamo una stringa che contiene un carattere speciale (*).
$word = ‘*world*’;
$text = ‘Hello *world*!’;
preg_match(‘/’.$word.’/’, $text); // causes a warning
preg_match(‘/’.preg_quote($word).’/’, $text); // true
La stessa cosa può essere eseguita anche racchiudendo la stringa tra \Q e \E. Qualsiasi carattere speciale dopo la sequenza \Q viene ignorato fino alla sequenza\E.
$word = ‘*world*’;
$text = ‘Hello *world*!’;
preg_match(‘/\Q’.$word.’\E/’, $text); // true
Tuttavia, questo secondo metodo non è sicuro al 100%, poiché la stringa stessa può contenere \E.
Non catturare i subpatterns
I subpatterns, racchiusi tra parentesi, vengono catturato catturati in un array in modo da poter essere utilizzarli successivamente se necessario. Ma c’è un modo per non catturarli.
Iniziamo con un esempio molto semplice:
preg_match(‘/(f.*)(b.*)/’, ‘Hello foobar’, $matches);
echo “f* => ” . $matches[1]; // prints ‘f* => foo’
echo “b* => ” . $matches[2]; // prints ‘b* => bar’
Ora facciamo una piccola modifica aggiungendo un altro subpattern (H.*) davanti:
preg_match(‘/(H.*) (f.*)(b.*)/’, ‘Hello foobar’, $matches);
echo “f* => ” . $matches[1]; // prints ‘f* => Hello’
echo “b* => ” . $matches[2]; // prints ‘b* => foo’
L’array $matches è cambiato, questo potrebbe causare dei malfunzionamenti nello script, a seconda di ciò che facciamo con questa variabile nel codice. Dobbiamo pertanto trovare ogni occorrenza del’array $matches nel codice e modificare il numero dell’indice.
Se non siamo davvero interessati al contenuto del nuovo subpattern appena aggiunto, possiamo farlo ‘non acquisizione’ in questo modo:
preg_match(‘/(?:H.*) (f.*)(b.*)/’, ‘Hello foobar’, $matches);
echo “f* => ” . $matches[1]; // prints ‘f* => foo’
echo “b* => ” . $matches[2]; // prints ‘b* => bar’
Con l’aggiunta di ‘?: ‘ all’inizio del subpattern, non verrà più catturato nell’array $matches. In questo modo, gli altri valori dell’array non vengono spostati.
Subpatterns nominali
C’è un altro metodo per evitare le insidie dell’esempio precedente. Possiamo effettivamente dare un nome ad ogni subpattern, in modo da potervi fare riferimento più tardi utilizzando il suo nome al posto del numero di indice nell’array. Il formato è: (?P<nome>pattern)
Si potrebbe riscrivere il primo esempio della sezione precedente in questo modo:
preg_match(‘/(?Pf.*)(?Pb.*)/’, ‘Hello foobar’, $matches);
echo “f* => ” . $matches[‘fstar’]; // prints ‘f* => foo’
echo “b* => ” . $matches[‘bstar’]; // prints ‘b* => bar’
Ora possiamo aggiungere un altro subpattern, senza disturbare la corrispondenza esistente nell’array $matches:
preg_match(‘/(?PH.*) (?Pf.*)(?Pb.*)/’, ‘Hello foobar’, $matches);
echo “f* => ” . $matches[‘fstar’]; // prints ‘f* => foo’
echo “b* => ” . $matches[‘bstar’]; // prints ‘b* => bar’
echo “h* => ” . $matches[‘hi’]; // prints ‘h* => Hello’
N.B. Dalla versione 7.0 di PCRE (Perl Compatible REgular Expressions) e quindi dalla versione 5.2.2 di PHP vengono accettate anche le sintassi (?<nome>pattern) e (?’nome’pattern)
Non reinventiamo la ruota
Forse è più importante sapere quando NON vanno utilizzate le espressioni regolari. Ci sono molte situazioni per cui si possono trovare utility esistenti da utilizzare al loro posto.
Parsing [X]HTML
E’ una buona idea prendere un po di tempo per scoprire quali parser sono disponibili XML e HTML e studiarne il funzionamento. Ad esempio, PHP offre diverse estensioni relative a XML (e HTML).
Esempio: Come recuperare l’url del secondo link in una pagina HTML
$doc = DOMDocument::loadHTML(‘
Test
First link
Second link
‘);
echo $doc->getElementsByTagName(‘a’)
->item(1)
->getAttribute(‘href’);
// prints: http://net.tutsplus.com
Validazione dei form
Ancora una volta, è possibile utilizzare le funzioni esistenti per convalidare l’input dell’utente, come ad esempio l’invio dei dati di un form.
if (!filter_var($_POST[‘email’], FILTER_VALIDATE_EMAIL)) {
$errors []= “Please enter a valid e-mail.”;
}
// get supported filters
print_r(filter_list());
/* output
Array
(
[0] => int
[1] => boolean
[2] => float
[3] => validate_regexp
[4] => validate_url
[5] => validate_email
[6] => validate_ip
[7] => string
[8] => stripped
[9] => encoded
[10] => special_chars
[11] => unsafe_raw
[12] => email
[13] => url
[14] => number_int
[15] => number_float
[16] => magic_quotes
[17] => callback
)
*/
Altro
Qui ci sono alcune altre utilities da tenere a mente, prima di utilizzare le espressioni regolari:
strtotime() per l’analisi di date.
Utilizzare le normali funzioni per le stringhe per ricerche e sostituzioni, se il modello non contiene espressioni regolari.
Esempi: str_replace() al posto di preg_replace (), explode() al posto di preg_split(), strpos() al posto di preg_match()