PHP: espressioni regolari
Ho necessità di appuntare qualche dritta sulle espressioni regolari (in particolare con PCRE), causa la mia scadente memoria. Al momento è un work in progress.Convenzioni
- soggetto - stringa su cui si desidera far matchare la regex.
- regex - espressione regolare.
- engine - software che interpreta la regex sul soggetto
- matchare - verbo inglese italianizzato per indicare l'azione di "soddisfare" e "consumare" (la parte del soggetto soddisfatta).
Metacaratteri & co
- \a - bell
- \e - escape
- \f - form feed
- \n - newline
- \r - carriage return
- \t - tab
- \v - vertical tab
- \d - Una cifra fra 0-9.
- \b - Detta word boundary, è un ancora, matcha all'inizio o fine di una parola se quest'ultima è preceduta o postceduta da un non word character. Ha senso solo se nelle regex viene prima o dopo di un word character. Praticamente è come se la regex dovesse matchare \w\W o vicecersa.
- \B - Opposto di \b.
- \D - Qualsiasi carattere che non sia 0-9.
- \w - Qualsiasi word character, ossia lettera, cifra o underscore.
- \W - Qualsiasi carattere che non sia una lettera, cifra o underscore.
- \s - whitespace ossia spazio, tab e newline.
- \S - Qualsiasi carattere non matchato da \s.
- (?i) - Rende case insensitive da quel punto in poi la regex.
- (?s) - Abilita per la regex che segue la modalità "dot matches line breaks".
- (?m) - Abilita per la regex che segue la modalità "^ and $ match at line breaks".
- \Q - Rende letterale tutto quelle che segue (block escape).
- \E - Termina il block escape.
- \A - Se posto all'inizio di una regex, indica che il soggetto deve matchare la regex fin dal suo primo carattere.
- \Z - Se posto alla finw di una regex, indica che il soggetto deve matchare la regex completamente per la sua ultima parte.
- * - è un quantificatore (vedi paragrafo dedicato), indica che la regex che lo precede può ripetersi 0 o più volte nel soggetto.
- + - è un quantificatore (vedi paragrafo dedicato), indica che la regex che lo precede può ripetersi 1 o più volte nel soggetto.
- ? - è un quantificatore (vedi paragrafo dedicato), indica che la regex che lo precede può ripetersi 0 o 1 volta (ossia la rende opzionale) nel soggetto.
- $ - Se posto alla finw di una regex, indica che il soggetto deve matchare la regex completamente per la sua ultima parte. Se l'opzione "^ and $ match at line breaks" è abilitata ed è posto alla fine di una regex, ha il significato di indicare (ancora) la fine della linea (e non del soggetto).
- ^ - Se posto all'inizio di una regex, indica che il soggetto deve matchare la regex fin dal suo primo carattere. Se posto come primo carattere dentro una classe di caratteri ha il significato di negare quella classe (è matchato qualsiasi carattere non indicato nella classe). Se l'opzione "^ and $ match at line breaks" è abilitata ed è posto all'inizio di una regex, ha il significato di indicare (ancora) l'inizio della linea (e non del soggetto).
- . - Matcha ogni carattere. Di default non matcha anche il newline, occorre indicarlo esplicitamente attraverso l'opzione "dot matches line breaks".
- | - Divide la regex (o solo una parte) in alternative (es. 'cane|gatto|topo' indica che la regex matcha cane o gatto o topo). La regex matchera usando la prima alternativa valida (nell'ordine da noi indicato).
Quantificatore
E' possibile far ripetere una regex, facendola seguire da un quantificatore 'regex{min, max}' (variable ripetition). Se si omette uno due indici (compresa la vigola) '{numero}' allora la regex è ripetuta esattamente quel numero di volte (fixed ripetition). Se si omette l'indice max (lasciando la virgola) allora si impone un numero minimo di ripetizioni ma non un massimo (infinite ripetition) che corrisponde ad usare il quantificatore '*'. Se si usa 'regex{1,}' è come se si stesse usando il quantificatore '+'. Un quantificatore può presentarsi dopo un gruppo di cattura, ma sarà possibile effettura backreference solo un singolo gruppo e questo corrispondera alla parte del soggetto matchata per ultima dal (ultimo) gruppo di cattura. Un modo per catturare tutta la parte del soggetto matchata da tutti i gruppi di cattura ripetuti è quello di rendere il gruppo di cattura non più di cattura (gioco di parole) e rendere quest'ultimo, assieme al quantificatore, un gruppo di cattura (esempio vogliamo catturare un numero di 8 cifre, se prima la regex era '\d{8}', ora sarà '((?:\d)){8}' in cui '\1' ci restituira l'intero numero di 8 cifre e '\2&' ci restituira solo l'ultima cifra).
Classe di caratteri
Un insieme di caratteri racchiusi fra due parentesi quadre (es. [ae]) ha il significato di creare una classe di alternative da matchare (l'esempio precedente risulterebbe valido incontrando sia una a che una e). Al suo interno è possibile inserire range di caratteri come: a-z, A-Z, 0-9.
Gruppo di cattura
Una regex (o parte di essa) racchiusa fra parentesi tonde ha la proprietà di catturare quello che matcha (per essere eventualmente usato). E' possibile usare la parte del soggetto catturato anche all'interno della stessa regex, basta indicarlo con \numero_gruppo_cattura (esempio '(\d\d)\1'), dove i gruppi di cattura sono numerati a partire da 1, nell'ordine in cui si presentano nella regex. Non ha senso richiamare un gruppo prima che esso sia stato catturato. Il richiamo è detto backreference. Se i gruppi catturati sono maggiori di 9, allora dal decimo gruppo in poi sarà '\10' (fino a 99). Ai gruppi di cattura può essere anche assegnato un nome nei seguenti modi: '(?<nome>regex)' richiamato con '\k<nome>', '(?'year'regex)' richiamato con '\k'magic'', '(?P<nome>regex)' richiamato con '(?P=nome)'. Per effettuare backrefence ad un gruppo di cattura all'esterno della regex (ad esempio durante un operazione di find & replace) è possibile utilizzare le forme '$numero_gruppo', '\numero_gruppo', e '${numero_gruppo}' ('$10', '\10' e i rispettivi successori sono sempre trattati come il decimo gruppo di cattura e mai ambiguamente come '\1' seguiti da un letterale '0'). Per effettuare backreference esternamente alla regex ad un gruppo con nome assegnato (esempio '(?<nome>.*)') è necessario usare comunque il classico backrefence numerico (esempio '\1', '$1' o '${1}').
Gruppo di non cattura
Catturare un parte del soggetto matchata è un operazione onerosa, pertanto se in alcuni casi si ha la necessità di "raggruppare" una regex ma non si ha necessità della cattura si può racchiudere la regex interessata tra '(?:' e ')'. E' possibile anche inserire dei "mode modifiers" per solo quel gruppo (esempio (?i:exp_regolare) oppure per disabilitare il mode modifier '(?-i:exp_regolare)' ).
Comportamento "greedy"
Di default ogni quantificatore matcha il maggior numero possibile di caratteri (esempio '(.*)' catturera al primo ed unico, per questa regex, tentativo tutto il soggetto). Se la regex non riuscisse al primo tentativo a matchare il soggetto allora effettuerebbe un indietreggiamento (backtracks), diminuendo di un carattere (in realtà diminuisce di una volta il matching, ossia se la regex è '(..)*\d', al primo tentativo il quantificatore matcharebbe tutto il soggetto, se composto da un numero pari di caratteri, al secondo tentivo matcharebbe tutto il soggetto esclusi i suoi ultimi due caratteri e così via) la parte del soggetto matchata dal quantificatore greedy (esempio '.*\d', al primo tentativo sicuramente fallirebbe perchè il quantificatore greedy '.*' matchare l'intero soggetto non lasciando possibilità a '\d' di matchaere un carattere numerico, così al secondo tentativo il quantificatore greedy matcherà tutto il soggetto tranne un carattere, se '\d' non risultasse matchata, allora si procederebbe ancora indietreggiando). Ad ogni passaggio dell'indietreggiamento l'engine tiene traccia di posizioni (backtracking position) a cui poter ritornare nel caso il resto della regex (che segue il quantificatore) fallisse nel tentivo di matching.
Comportamento "lazy"
E' il comportamento opposto al "greedy" per cui un quantificatore matcha il minor numero possibile di caratteri (esempio, se abilitato il comportamento "lazy", '(.*)' non catturera alcun carattere al primo tentativo). E' possibile abilitare questo comportamento ad esempio posizionando un '?' dopo un quantificatore (esempi: '*?', '+?', '??' e '{2,8}?'). Anche con il comportamento "lazy" viene effettuata l'operazione di backtracks (se necessario), solo che in modo opposto (ossia avanzando). Se dopo il primo tentativo la regex non risultasse soddisfatta, allora il lazy quantificatore si ripeterebbe (nel matching) una volta in più del precedente tentativo (anche in questo caso, l'engine tiene traccia della backtrack position).
Quantificatore possessivo e gruppo atomico
Non sempre l'operazione di "backtracks" è una cosa desiderata. Un primo valido motivo è che tale operazione, in oppurtune situazioni, può portare ad un gran numero di tentativi (che facilmente raggiunge una complessità in tempo esponenziale) da parte dell'engine per far matchare la regex sul soggetto. Esistono due metodi per evitare tale operazione: quantificatori possessivi e gruppi atomici. I quantificatori possessivi sono quantificatori seguiti da un '+' (esempio '*+'). Un quantificare possessivo non tiene traccia delle backtrack position e pertanto, per conto suo, richiedere un unica possibilità di matching per l'engine a seconda del suo comportamento greedy o lazy. Un gruppo atomico, realizzabile con '(?>regex)', è un gruppo di non cattura che, una volta matchata la parte del soggetto da esso desiderata, non tiene traccia della backtrack position. La differenza nel comportamento tra un quantificatore possessivo e un gruppo atomico è che il primo non tiene mai realmente traccia della backtrack position, al contrario il secondo ne tiene traccia ma, appena matcha la parte del soggetto desiderata, l'engine "getta" via la backtrack position. Pertanto a livello pratico (stesso risultato) non ci sono differenze nell'uso di uno rispetto all'altro (e spesso non ve ne è neanchè a livello implementativo). Quello che è importante notare è che il gruppo atomico può racchiudere un intera espressione regolare, al contrario un quantificatore possessivo è applicato solo ad un token (che però può essere un gruppo e quindi rendendolo ugualmente equivalente al suo collega). Pertanto '\w++\d++' e '(?>\w+\d+)' non sono la stessa cosa, con la prima regex il soggetto "abc123" non risulterebbe valido, in quanto '\w++' consumerebbe l'intero soggetto e '\d++' fallirebbe (nessun altro tentativo verrebbe fatto dall'engine in quanto non si è tenuto traccia di alcuna backtrack position), con la seconda e lo stesso soggetto, al primo tentativo '\w+' consumerebbe tutto il soggetto e '\d+' fallirebbe, a questo punto, l'engine, non essendo ancora "uscito" dal gruppo atomico (perchè non soddisfatto), al secondo tentativo effettuerebbe indietreggiamento per '\w+' di un carattere in modo tale che essa consumasse 'abc12' permettendo a '\d+' di venir soddisfatta con '3'.
Lookahead
Alcune volte si rende necessario controllare la presenza di alcuni caratteri senza che la regex impiegata matchi (o consumi) quest'ultimi. Tale operazione può essere svolta con un lookahead (zero-length assertion), che è realizzabile con '(?=regex)'. L'engine in presenza ad esempio di una regex del tipo '\w(?=\d).', controlla che nel soggetto sia soddisfatta '\w' (consumando la parte del soggetto che la soddisfa) successivamente entra nel lookahead controlla che sia soddisfatta '\d' (se ciò non accade la regex è dichiarata non soddisfatta) e si dimentica di quanto ha consumato per quest'ultima regex (interna al lookahead) e segue nella verifica per '.' (che parte a controllare il soggetto dove si era fermata la regex '\w'). Un lookheak può essere negato (negative lookahead) con la forma '(?!regex)'. E' importante notare che un lookahead è un gruppo atomico, una volta soddisfatto, non ricorda in alcun modo la backtrack position. E' legale inserire un gruppo di cattura dentro un lookahead e fare backreference all'esterno di esso.
Lookbehind
In analogia a quanto visto per un lookahead, un lookbehind, realizzabile con '(?<=regex)', controlla che il soggetto matchi una certa regex (senza consumarlo), attraversando il soggetto da destra verso sinistra (unico esempio). Con una regex del tipo '..(?<=\d)\w', l'engine procederà a testare il soggetto finchè non troverà soddifatta '..', a questo punto controllera che la regex contenuta all'interno del lookbehind '\d' sia soddisfatta (controllerà il carattere subito a sinistra della posizione raggiunta dalla precedente regex, ossia controllera che l'ultimo carattere consumato sia un carattere numerico) e in caso affermativo (senza consumare o comunque alterare in alcune modo lo stato delle cose presentato a seguito della prima regex '..') procedererà nel testare '\w'. Un lookbehind può essere negato con la forma '(?<!regex)'. Per una questione implementativa (è l'unica caso in cui il test di una regex procede da destra verso sinistra) non è possibile usare all'interno di un lookbehind quantificatori "infiniti" come '*','+' e '{min,}'. E' importante notare che un lookbehind è un gruppo atomico, una volta soddisfatto, non ricorda in alcun modo la backtrack position. E' legale inserire un gruppo di cattura dentro un lookbehind e fare backreference all'esterno di esso.
Espressioni condizionali
E' possibile inserire delle regex che verifichino alcune condizioni. Tale opportunità è limitata alla cattura o meno di un gruppo. La regex condizionale è realizzato con '(?(numero_gruppo_cattura)then|else)' dove la regex, ovviamente, deve possedere il gruppo di cattura di cui si vuole verificare la cattura e then/else sono regex "usate" dall'engine rispettivamente nel caso positivo della cattura o nel caso negativo (il gruppo non è stato catturato). 'then' e 'else' possono essere una qualunque regex (quindi anche omesse), purchè non contengano '|' (in questo caso è necessario creare un gruppo, ad esempio '(?(1)(cane|gatto)|(topo|lucertola))' ). E' possibile usare un espressione condizionale con un lookahead od un lookbehind (al posto della verifica sul gruppo di cattura) con la seguenti forme '(?(?=regex)then|else)' e '(?(?<=regex)then|else)' (è possibile usarle anche nella loro forma negata).
Commenti
E' possibile inserire commenti nella regex con '(?#commento)'. Un'altra alternativa è abilitare la modalità "free-spacing" con cui l'engine ignora gli spazi (su cui, se richiesti, occorre effettuare escape '\ ') e qualsiasi eventuale carattere che segue il simbolo '#' (fino al termina della singola linea). La modalità "free-spacing" non altera il comportamento dentro una classe di caratteri, pertanto eventuali spazi o # verrano considerati con il solito significato.
Regole per l'escaping
Occorre effettuare l'escaping (attraverso l'inserimento di '\' prima del carattere sui cui si effettua l'escape) su '$numero', '\numero', '${' e '\'. E' necessario effettuare l'escape su ogni carattere che abbia un significato non letterale di default.
Saluti.
• 1 commento • Inserisci un commento • Pubblicato il 31 luglio 2009 • Ultima modifica 16 maggio 2011 • Feed commenti •
1.
vvv
- 11 aprile 2012 @ 15:08
ddddd
