|
|
|
|
Strumenti |
15-06-2010, 21:46 | #1 |
Senior Member
Iscritto dal: Apr 2004
Città: Livorno
Messaggi: 6612
|
[Tutorial] Regular Expressions
Regular Expressions
Questi oscuri garbugli di caratteri... Sommario Prima parte Il classico problema ___Le regular expressions, come e quando Le basi ed i primi esempi ___Sequenze speciali ___Quantificatori ___Alternanze e raggruppamenti ___Posizionamenti ___Fare a pezzi la stringa trovata! ___Backreferences Modificatori (flags) Sostituzioni Dove posso provare le mie espressioni regolari? Seconda parte Espressioni regolari avanzate ___Lookarounds ___Quantificatori avidi, possisivi e svogliati ___Espressioni atomiche ___Catastrofe! ___Trucchi e funzionalità vari Differenze con le RE tra i linguaggi Links e riferimenti Il classico problema Vi sarà capitato, programmando, di dover effetuare delle ricerche all'interno di una stringa di una particolare sottostringa. Pratica comunissima, tanto che pressoché tutti i linguaggi di programmazione mettono a disposizione gli strumenti per farlo in maniera semplice e concisa. Tuttavia, potrebbe anche esservi capitato di non dover ricercare una stringa precisa, ma qualcosa di più vago. Potrebbe anche essere semplicemente che si voglia cercare "pippo" all'interno di una stringa, ma che possa anche andare bene "Pippo", "PIPPO" o "pIpPo": normalmente, si converte tutta la stringa in minuscolo (o in maiuscolo) con le apposite funzioni e poi si cerca solo "pippo" (o "PIPPO"). Le cose si complicano se si vuole invece cercare una sottostringa tipo "Pippo ha XX anni", dove XX può essere un qualsiasi numero non negativo, nel qual caso si dovrebbe, nell'ordine,
Non parliamo, poi, di dover andare a cercare cose come "Pippo", "PIPPO" ma non "pIPPO", oppure una data, o peggio un codice fiscale, se non, addirittura, un tag X/HTML (). Ecco allora che vengono in nostro soccorso le Regular Expressions, che possono fare tutto questo e molto, molto di più. Le regular expressions, come e quando Le regular expressions (in italiano espressioni regolari, e spesso abbreviato in molti linguaggi come RegExp o RegEx) sono un concetto matematico sviluppato già negli anni '40, e sebbene la mia indole di matematico frema per illustrarvene i concetti, devo astenermi perché, a livello di programmazione, hanno utilità prossima allo zero Per chi si intende un po' di logica e di teoria degli insiemi la voce della Wikipedia relativa offre qualche spunto: http://it.wikipedia.org/wiki/Espressione_regolare Le espressioni regolari (da ora in poi, RE) sono un aiuto importante per mantenere snello e compatto il proprio codice, riducendo ad una sola riga quel che normalmente avremmo fatto magari in decine di righe. I più comuni linguaggi di programmazione offrono pacchetti spesso molto completi e di facile reperibilità per le RE, quando non proprio forniti con le più comuni distribuzioni; alcuni linguaggi hanno addirittura supporto nativo alle RE (Javascript, Perl, PHP, Actionscript 3). Non solo: spesso, molti editor di testo nonché ambienti di sviluppo offrono la possibilità di ricerca e sostituzione del testo tramite RE, e questo può essere uno strumento estremamente utile per risparmiarsi laboriose azioni magari fatte a mano. Ma, se da una parte abbiamo uno strumento potente e flessibile subito a disposizione, dall'altra ci sono da pagare due scotti principali:
Ricordo, inoltre, che le RE sono implementate sì in svariati linguaggi ed ambienti, ma, complice il fatto che non sono mai state standardizzate, il supporto alle varie caratteristiche delle RE potrebbe essere più o meno completo, così come il comportamento (che si distingue in text-driven e regex-driven). Ogni volta che usare le RE con un linguaggio nuovo, consultate la parte relativa al modo in cui agiscono. Le basi ed i primi esempi I vari linguaggi di programmazione hanno diversi modi di rappresentare le RE. Spesso si tratta di stringhe che poi devono essere interpretate o semicompilate dal motore interno (Java, VB, C...), o nei casi di supporto nativo hanno una rappresentazione tutta propria, come nel caso di Javascript e Perl che le racchiudono tra due slash (/). In questa guida verranno rappresentate semplicemente con caratteri a larghezza fissa, senza apici di contorno, come questa: Pippo ha \d+ anni. Sequenze speciali Una RE sarà quindi composta da parti di stringhe, come Pippo ha e anni dell'esempio precedente, e da particolari sequenze di caratteri che vengono poi interpretate. Ad esempio, la sequenza \d+ significa "qualunque ripetizione di una o più cifre decimali". Vediamo dunque quali sono queste sequenze di caratteri speciali.
(Per ulteriori informazioni sulla sintassi dei codici fiscali, c'è sempre la fida Wikipedia: http://it.wikipedia.org/wiki/Codice_fiscale) Quantificatori Innanzitutto, tutte quelle ripetizioni di [A-Z] e \d si possono evitare. A questo proposito ci vengono incontro dei comodi quantificatori:
Nota: fate attenzione ad usare correttamente i quantificatori! Un loro uso improprio, per quanto possa portare a risultati corretti, potrebbe avere conseguenze nefaste sulle performance del programma. Alternanze e raggruppamenti Ma se invece volessimo cercare due o più espressioni diverse tra loro? Ad esempio, ci interessano le stringhe del tipo "Pippo" ma anche "Pluto": dovremmo procedere a due ricerche? No, basta usare il carattere di alternanza, cioè la pipe (|), e creare un'espressione del tipo Pippo|Pluto. In questo modo, viene cercato "Pippo" oppure "Pluto", come volevamo. Se invece fosse solo un pezzo di stringa a differire? Nel caso citato, "Pippo" e "Pluto" hanno le stesse lettere iniziali e finali. In questo caso sarebbe meglio utilizzare i raggruppamenti: essi non sono altro che un modo per intendere una sequenza di caratteri come un'unica entità dall'interprete delle RE. Per creare un gruppo basta includere le sequenze desiderate all'interno di parentesi tonde, ad esempio così: P(ipp|lut)o. In questo modo, l'interprete di RE cercherà ogni stringa che comincia per 'P', cui seguirà un gruppo che potrà essere "ipp" o "lut", cui segue una 'o'. Insomma, troverà "Pippo" e "Pluto", per l'appunto. Le alternanze possono anche essere più di due, ad esempio avremmo potuto cercare P(ipp|lut|aperin)o, per includere anche "Paperino" nella nostra ricerca. Ma, cosa anche più importante, i raggruppamenti possono essere nidificati: la RE abc(def|ghi(123|456))ABC troverà tutte le seguenti stringhe: "abcdefABC", "abcghi123ABC" e "abcghi456ABC". Questo è utile, ad esempio, se volessimo cercare una data il cui anno può essere indicato da 2 o 4 cifre: \d\d-\d\d-(\d\d|\d{4}) è l'espressione regolare che fa al caso nostro, capace di trovare sequenze come "12-05-2004" o anche "22-04-85". (Ma anche "32-13-7865": come già detto, il controllo della correttezza della data può essere fatto tramite codice e non con le RE, anche se è possibile farlo. Io stesso ho creato una RE per trovare tutte le date comprese tra l'1-1-1901 e il 31-12-2099, senza bisogno di ulteriori controlli, e con una piccola modifica potrebbe essere usato per qualsiasi data, anche avanti Cristo. Se vi interessa, ditelo.) Un altro grosso vantaggio dei raggruppamenti è la loro possibilità di essere usati in combinazione a quantificatori, in modo da ricercare una, nessuna o più ripetizioni dello stesso gruppo. Ad esempio, ba(na)+ troverà "bana", ma anche "banana", "bananana", "bananananana" e così via. Posizionamenti Cosa si può fare se vogliamo trovare delle espressioni solo in alcuni punti particolari del testo? Ad esempio, se volessimo trovare "Pippo" solo quando è all'inizio o alla fine di una stringa, oppure "ente" ma solo quando è alla fine di una parola? In questo caso intervengono delle sequenze di caratteri speciali che non indicano nessun carattere, ma servono per il posizionamento all'interno della stringa. Vediamoli.
Fare a pezzi la stringa trovata! Ebbene, abbiamo visto che \d\d-\d\d-\d{4} trova le date nel formato gg-mm-aaaa, mentre [A-Z][a-z]+ ha \d+ anni trova "Pippo ha 20 anni" ma anche "Paperone ha 75 anni". A questo punto possiamo procedere a recuperare i "pezzi" che ci servono, cioè i numeri indicanti giorno, mese e anno per la data, oppure nome ed età nel secondo esempio. Dovrebbe risultare piuttosto semplice, ma a volte può essere dispendioso, barboso e pure inutilmente complicato mettersi a fare ciò che la RE ha, in sostanza, già fatto. Ecco che in nostro soccorso arrivano i riferimenti. In realtà, i riferimenti li abbiamo già creati negli esempi visti finora: vengono infatti ottenuti dai raggruppamenti tra parentesi tonde. Questi riferimenti tengono in memoria ("catturano") la sottostringa che ha un match con il gruppo. In questo modo, ([A-Z][a-z]+) ha (\d+) anni troverà sì "Pippo ha 20 anni", ma ci terrà comodamente in memoria le sottostringhe "Pippo" e "20" pronte per la nostra elaborazione! I riferimenti vengono di solito salvati dai linguaggi in particolari oggetti, che di solito contengono sia tutta la stringa trovata, sia un elenco (solitamente un array) di stringhe contenenti i singoli riferimenti, e magari anche le loro posizioni iniziali e finali all'interno della stringa. I riferimenti sono numerati in ordine crescente a partire dal primo che viene trovato da sinistra, così "Pippo" sarà il primo riferimento, mentre "20" sarà il secondo. Il modo per reperire i riferimenti varia da linguaggio a linguaggio: in Java, ad esempio, un oggetto della classe Matcher consentirà, con il metodo find(), di trovare le corrispondenze e, una volta trovate, il metodo group(n) restituirà il riferimento nella posizione n, mentre start(n) e end(n) restituiranno la posizione del riferimento all'interno della stringa. In JavaScript, più semplicemente il metodo delle stringhe match(regex) restituirà un array il cui primo elemento è l'intera stringa trovata, cui seguiranno i diversi riferimenti catturati (non c'è indicazione della loro posizione, solo l'indicazione di inizio e fine della stringa trovata nell'oggetto globale RegExp o nell'array restituito da match). Ma i gruppi sono stati usati anche per altre cose, ad esempio le alternanze: creeranno dei riferimenti anche in quel caso? La risposta è sì, anche se non vorremmo. Il modo per usare raggruppamenti senza creare riferimenti è quello di usare la sequenza (?:...) al posto di (...). Così, Il programma (\w+) contiene (\d+) bac(?:o|hi) troverà "Il programma helloWorld ha 1 baco" oppure "Il programma fooBar ha 7 bachi", ma i riferimenti saranno solo due: quello al nome del programma e quello al numero di bachi. Perché evitare di creare riferimenti? La risposta dovrebbe essere ovvia: occupano memoria. Finché le espressioni sono così semplici non c'è problema, ma quando le cose si complicano sono dolori... Un altro dubbio può essere: nel caso già visto di ba(na)+, ad esempio, viene creato un riferimento per ogni ripetizione di "na"? No. Il riferimento sarà sempre e solo uno, identificato da un numero assegnato in partenza, e conterrà solo l'ultima sottostringa "na" trovata. Nota: che succede se una cattura viene fallita? Questo può capitare, ad esempio, nel caso di gruppi di cattura indicati in un'alternanza, come in (Pippo)|(Pluto) (sì, l'esempio è scemo). Nella stringa "Ciao Pippo!" la RE troverà "Pippo" e lo memorizzerà nella cattura con numero 1, ma non troverà "Pluto". La cattura 2 che valore avrà? Solitamente sarà un valore nullo, come null, nil o che altro in tanti linguaggi. In JavaScript e gli altri dialetti ECMAScript, invece, avrà valore "" (stringa vuota), così come una cattura del tipo (). Tenetelo a mente. Backreferences Il nostro problema ora diventi quello di voler cercare un semplice tag XML, aperto da una sequenza tipo <tag>, contenente un po' di testo ed infine chiuso dalla sequenza </tag>, ad esempio "<ciao>Pippo</ciao>". Una RE del tipo <\w+>(?:[^<]|<[^/])*</\w+> sembrerebbe che possa andare bene (esercizio: si presti attenzione alla parte compresa tra (?:...)* e si cerchi di capire cosa vuol dire). Tuttavia, è facile accorgersi che in caso di XML non ben formato, come ad esempio "<ciao>Pippo</saluto>", questa RE dapprima trova "<ciao>", poi trova "Pippo" e poi trova "</saluto>". Noi invece avremmo voluto che la corrispondenza fallisse, perché "</saluto>" non chiude il tag "<ciao>", e ci saremmo risparmiati un "falso positivo". I gruppi di cattura ci possono dare una mano: creando dei riferimenti, li possiamo usare per indicare di star cercando qualcosa che è stato trovato in precedenza e catturato. Come già detto, ai gruppi di cattura viene assegnato un numero progressivo da 1 in poi, a partire da quello indicato più a sinistra: per farvi riferimento si usa una backreference, che si indica dalla sequenza \#, dove # è il numero assegnato alla cattura (può anche essere di due cifre, fino a 99). La nostra RE dovrà quindi catturare il nome del tag e riutilizzarlo dopo per indicare il tag di chiusura, in questo modo: <(\w+)>(?:[^<]|<[^/])*</\1>. Così siamo sicuri che l'eventuale tag di chiusura corrisponda a quello di apertura. Note:
Modificatori (flags) Con una semplice opzione è anche possibile cambiare il modo in cui il motore applica le RE. Ad esempio, gli si può dire di ignorare le differenze tra maiuscole e minuscole. I modificatori vengono solitamente abbreviati con una singola lettera minuscola. Vediamoli:
La modalità free-space può essere molto comoda per le RE più complesse ed usate di comune. Quella per la data, ad esempio, si può scrivere così: Codice:
( \d \d? ) # Trova e cattura il giorno [ \- \/ ] # Trova il carattere di separazione ( \d \d? ) # Trova e cattura il mese [ \- \/ ] # Altro separatore ( \d{4} ) # Trova e cattura l'anno Sostituzioni Ora che abbiamo i riferimenti (e magari anche le loro posizioni) per noi diventa facile manipolare i dati. Un altro annoso problema che potremmo incontrare è quello della sostituzione di sottostringhe all'interno di una stringa. Solitamente abbiamo da parte la posizione iniziale e finale della stringa trovata, per cui il problema si semplifica... ma può essere ulteriormente semplificato grazie ai comuni metodi di sostituzione che vengono offerti dai vari linguaggi ed ambienti di sviluppo. Supponiamo, ad esempio, di voler trasformare una data nel formato gg-mm-aa nel corrispondente gg/mm/19aa. Normalmente, quello che faremmo è, una volta che la RE ha trovato la corrispondenza, prendere la stringa iniziale, prendere la sottostringa dall'inizio fino all'inizio della corrispondenza, concatenarla con la stringa che abbiamo creato con la data nel formato che vogliamo, ed infine concatenarla con il resto della stringa. Noioso, vero? Invece è comune che i linguaggi mettano a disposizione i metodi replace(string, string) (o qualcosa di simile), ma che abbiano un overload anche nel metodo replace(regex, string), in modo che la sostituzione avvenga con i match trovati con le RE. In questo caso, la stringa di sostituzione può contenere le indicazioni ai riferimenti trovati (backreferences), che vengono solitamente rappresentate da sequenze come "$X" oppure "\X", dove X indica il numero del riferimento trovato. (Nei linguaggi ho trovato più comune l'uso di "$X", mentre negli editor mi è parso più comune "\X". Consultate l'help relativo, comunque.) Dunque, per ottenere il nostro scopo possiamo procedere con replace() usando (\d\d)-(\d\d)-\(\d\d) come RE, e "$1/$2/19$3" come stringa da sostituire. In questo modo, una data come "12-08-82" viene trasformata subito in "12/08/1982". Si noti che i riferimenti, nella stringa di sostituzione, vengono spesso numerati a partire da 1 e non da 0 come ci si potrebbe aspettare da prassi comune nei linguaggi di programmazione. In alcuni linguaggi, soprattutto quelli che non supportano un metodo replaceAll() o simile (tipo il JavaScript), può tornare utile usare RE con flag global, in modo da ottenere una sorta di "sostituisci tutto". Dove posso provare le mie espressioni regolari? Solitamente, un editor di testo che le supporti potrebbe fare a caso vostro. Se volete sapere quali supportano le RE, questa pagina di Wikipedia vi può aiutare: http://en.wikipedia.org/wiki/Compari...Basic_features Personalmente, uso Notepad++. Se invece volete qualcosa di graficamente più carino e che dia subito un colpo d'occhio dei risultati trovati, forse questo sito ha quello che cercate: http://www.gskinner.com/RegExr/ ********************** Il contenuto di questo post è rilasciato con licenza Creative Commons Attribution-Noncommercial-Share Alike 2.5 Creative Commons Attribution-Noncommercial-Share Alike 2.5
__________________
HWU Rugby Group :'( - FAQ Processori - Aurea Sectio - CogitoWeb: idee varie sviluppando nel web Ultima modifica di MaxArt : 15-06-2010 alle 21:50. |
15-06-2010, 21:46 | #2 |
Senior Member
Iscritto dal: Apr 2004
Città: Livorno
Messaggi: 6612
|
Espressioni Regolari avanzate
Quello che abbiamo visto finora è una guida di base, che già consente di fare parecchio con le espressioni regolari. Ma le RE possono fare ancora di più, ed inoltre si possono apprendere diversi trucchetti per migliorare le performance. Perché le basi delle RE si imparano in fretta, ma molto più ostico è creare delle RE che funzionino bene e che non uccidano le performance del nostro codice: spesso è facile creare delle RE la cui complessità è O(2^n) rispetto alla lunghezza della stringa. Immaginatevi le conseguenze... Lookarounds Supponiamo di avere una stringa al cui interno vogliamo identificare una... stringa, cioè una sequenza di caratteri racchiusa tra apici. È un caso comune se ci capita di dove fare il parsing di un codice o proto-codice, o semplicemente di un elenco di valori che possono essere anche delle stringhe delimitate da apici. Un'espressione regolare adeguata potrebbe essere '[^']*'. Non è difficile da interpretare: si cerca una stringa che inizi con un apice, prosegua con una sequenza di qualsivoglia lunghezza di caratteri che non siano apici, e poi si conclude con un apice. Ma supponiamo che all'interno della stringa sia permesso inserire degli apici, purché adeguatamente preceduti da una backslash: in fondo, gli apici ci servono anche come apostrofi. Si potrebbe, infatti, avere qualcosa tipo 'un\'automobile', e la nostra RE si fermerebbe al solo 'un\'. Quello che dobbiamo fare è, allora, cercare di includere questi apici con escape, e per farlo il modo migliore sono i lookarounds, cioè delle sequenze di simboli che ci consentono di specificare da cosa dev'essere preceduta (lookbehind) o succeduta (lookahead) un gruppo perché esso venga considerato. Si noti che l'argomento dei lookaround non viene incluso nel gruppo di ricerca, ed è questo il loro punto fondamentale.
Codice:
' # indica che la stringa deve iniziare con un apice (?: # dentro questo gruppo ci mettiamo i caratteri che vogliamo, cioè tutti tranne gli apici, a meno che non siano preceduti da \ [^'] # questo vuol dire "tutti i caratteri che non siano apici" | # quest'alternanza ci serve per dire che vogliamo includere anche gli apici preceduti da \ (?<=\\)' # questa sequenza significa "gli apici preceduti da \" (ricordiamo che le backslash devono essere a loro volta precedute da backslash) ) # chiudiamo il gruppo e diciamo di cercare la sequenza più lunga possibile ' # infine, la stringa deve finire con un apice. Nota: in termini computazionali, i lookarounds sono piuttosto dispendiosi. Quello che fanno, in effetti, è andare ad esplorare le parti precedenti o successive al punto raggiunto e verificare una condizione in più, e poi tornare indietro. Conoscendo meglio il modo in cui funzionano le RE, però, ci si rende conto che molto spesso se ne può fare a meno. Se avessimo usato la RE '(?:[^']|\\')*' ci saremmo accorti che non avrebbe funzionato ma, semplicemente cambiando l'ordine dall'alternanza, ci si accorge che '(?:\\'|[^'])*' invece va bene. Dal punto di vista logico questo può lasciarci confusi, perché dovrebbe essere un or e quindi una sorta di operatore simmetrico. Qual è la differenza? Il fatto è che nel primo caso il motore RE dapprima cerca tutti i caratteri che non siano degli apici: nella stringa 'un\'automobile', quindi, trova "u", poi trova "n", poi trova "\" ed infine trova l'apice. A questo punto il gruppo [^'] non va bene e allora prova con l'altro, cioè \\': ma anche questo non va bene. Dunque, il motore conclude che il gruppo (?:[^']|\\') non va più bene, quindi torna indietro di un carattere e verifica se il carattere dopo è un apice... ed in effetti lo è. Quindi produce solo 'un\' come risultato. Nell'altro caso, invece, il motore RE cerca per prima cosa se siamo di fronte ad una sequenza \', e poi se è un qualsiasi carattere che non sia un apice. Quindi, dopo aver passato il primo apice, non trova "\'" ma trova "u"; poi non trova ancora "\'" ma trova "n"; infine trova "\'"! Di conseguenza la ricerca non si blocca e può andare avanti, trovando correttamente tutta la stringa 'un\'automobile'. Quindi, il consiglio generale è: usando le alternanze, mettere sempre per primi i gruppi più lunghi, o si rischia che il motore RE si blocchi anzitempo. Trucco: i lookahead, come detto, sono utili per trovare gruppi seguiti da un altro gruppo specifico, senza che questo venga incluso nel risultato. Ad esempio, se volessimo cercare un tag di chiusura HTML, potremmo cercare i simboli < che siano seguiti dalla /, dal nome del tag e poi da >. Una RE tipo <(?=/div>) farebbe a caso nostro. Come visto, in questi casi il lookahead va messo dopo il simbolo '<'. È utile far notare che i lookahead, però, possono messere anche prima di un certo gruppo. In effetti, i lookaround non sono per forza legati ad un gruppo di riferimento: semplicemente dicono al motore delle RE di effettuare una verifica, in avanti o indietro. Cosa vuol dire, quindi, mettere un lookahead prima di un gruppo? In sostanza, signfica verificare che ciò che segue soddisfi la condizione specificata. Ad esempio, la RE \b(?=\w{10}\b)\w*ente\b va a cercare tutte le parole che finiscono in "ente", dopo però aver verificato che fossero lunghe 10 caratteri (sì, lo so, \b\w{6}ente\b avrebbe prodotto lo stesso risultato, ma era un esempio). Si noti che i lookahead usati in questo modo possono anche essere più di uno, da usare in una sorta di and logico. Quantificatori avidi, svogliati e possessivi Supponiamo di avere una stringa come "12,$4,25a,2 4,65=,3pippo" e di voler isolare i singoli valori separati da virgole. I valori possono essere numeri, ma anche lettere o altri simboli, in un totale che sarebbe dispendioso contenere in una classe [...]. A questo punto, usiamo il carattere universale . (il punto) e procediamo con la semplice RE (.*), ottenendo come risultato "12,$4,25a,2 4,65=,". Cos'è successo? Semplicemente, il motore RE, non appena ha trovato la prima virgola, non ha pensato che fosse la fine di quel che stavamo cercando, ma invece l'ha considerata un carattere qualsiasi (quindi presa dal .) ed ha proseguito. Dunque è giunto fino in fondo alla stringa, è dovuto tornare indietro di un carattere, verificare che dopo ci fosse una virgola e fallendo, quindi di nuovo tornare indietro di un carattere e così via, per 7 volte, finché non ha infine trovato una virgola. Ecco spiegato il risultato finale.Il fatto è che il quantificatore * è "avido" (greedy), e tende ad andare avanti a ripetere il gruppo finché gli è possibile. Tuttavia, ci sono modi per differire questo comportamento. Il primo di questo è rendere il quantificatore "svogliato" (lazy), tramite l'aggiunta di un punto interrogativo immediatamente dopo: l'espressione, quindi, diventerà (.*?), e funzionerà proprio al caso nostro. Un quantificatore lazy fa agire il motore in maniera diversa: esso andrà avanti per il numero minimo di ripetizioni necessarie per non interrompersi. Nell'esempio, quindi, si è detto dapprima detto "cerca le sequenze qualsiasi seguite e fermati quando dopo c'è una virgola", mentre dopo "cerca le sequenze qualsiasi e fermati alla prima virgola trovata". Dunque, il motore dapprima verificherà che ci sia una virgola (fallendo), quindi troverà '1' e verificherà di nuovo la presenza di una virgola (fallendo di nuovo), poi troverà '2' e finalmente la virgola: la stringa di match sarà dunque "12," ed il gruppo catturato "12", proprio come volevamo. Infine, c'è un altro tipo di quantificatore: quello possessivo (possessive), che si ottiene ponendo un '+' dopo il quantificatore. La sua filosofia si può riassumere in: o tutto, o niente! I quantificatori possessivi sono simili a quelli avidi, ma c'è una sottile differenza che li rende sia potenzialmente molto più efficienti, sia più difficili da usare correttamente. Operativamente parlando, i quantificatori avidi tengono memoria di "dove sono stati", pronti a ritornarvi in caso di fallimento. Nel caso visto (.*), si è notato che il motore ha dovuto fare il backtracking di ben 7 caratteri prima di ottenere un risultato positivo. Il fenomeno può diventare poco gestibile nel caso di stringhe molto più lunghe. I quantificatori possessivi, invece, non tengono conto delle posizioni precedenti e dunque non effettuano backtracking in caso di fallimento. Cosa succede, dunque? Semplice: vanno avanti con la stringa, ignorando completamente il pezzo preso in esame. Vediamo come questo si traduce al lato pratico. La potenzialità dei quantificatori possessivi esce tutta quando il motore RE fallisce la corrispondenza. Supponiamo di avere una stringa che assomiglia ad un XML, tipo "<persona><nome>Pippo</nome><eta>anni<100</eta></persona>", e di volerne isolare i singoli tag. La nostra RE potrebbe essere <[^<>]*>: in questo caso troverebbe, nell'ordine, "<persona>", "<nome>", "</nome>", "<eta>", "</eta>" e "</persona>". Tutto bene? Sembra di sì. Un problema, però, si presenta quando il motore RE arriva a quel "<100", dove trova un altro '<' (quello di "</eta>") prima di trovare un '>': in questo caso, il motore ritorna indietro di uno, poi di due, infine di tre caratteri, ogni volta verificando che dopo venga un '>' e sempre fallendo, per poi rinunciare del tutto e proseguire nell'analisi della stringa. Un sacco di calcoli per qualcosa che, in fondo, era già ben evidente...Un quantificatore possessivo, invece, appena nota che dopo il "100" c'è un altro '<', rinuncia da subito e riprende l'analisi direttamente da "</eta>". I risultati sono uguali, ma le performance sono nettamente diverse. I veri miglioramenti, a direi il vero, si ottengono soprattutto nel caso di raggruppamenti nidificati, in cui i backtracking possono essere numerosi e potrebbero inficiare molto le prestazioni. Nota: alcuni motori RE si accorgono quando un gruppo sottoposto a quantificatore ed i caratteri successivi sono mutualmente esclusivi, rinunciando da subito al backtracking e di fatto rendendo automaticamente possessivi i quantificatori. Questo, ad esempio, è il caso del framework .NET. L'assenza di quantificatori possessivi, quindi, potrebbe essere benissimo una scelta voluta per non includere una caratteristica inutile. Gruppi atomici Un altro metodo per ottimizzare le RE è quello di usare i gruppi atomici. Tali gruppi vengono racchiusi entro il costrutto (?>...) e, al pari di (?:...) e dei lookarounds, non costituiscono un gruppo di cattura. Si usano solitamente in congiunzione a quantificatori ed alternanze, ed hanno un comportamento che per certi versi ricorda i quantificatori possessivi, nel senso che in caso di fallimento i gruppi atomici non effettuano backtracking. L'esempio per cui sono usati più spesso è quello della ricerca delle parole. Si supponga che si vogliano cercare le parole "atono", "atomi" e "atomica" in un testo: la RE \b(atomica|atomi|atono)\b farebbe al caso nostro (è una RE un po' scema, ma ricordiamoci che le RE si possono anche generare, e non è facile creare un algoritmo che generi RE veramente "intelligenti"). Se nella stringa s'incontra una delle tre parole, va tutto bene; se tuttavia si incontra una parola tipo "atomicamente", il motore RE prima troverà che "atomica" va bene ma poi non c'è la fine della parola, quindi tornerà indietro e proverà prima "atomi" e poi "atono", fallendo in entrambi i casi. Se avessimo usato un gruppo atomico, una volta che si è visto che "atomica" poteva andare bene ma che poi non c'è la fine della parola, il motore RE non avrebbe provato le altre due alternative e sarebbe andato avanti. Il vantaggio è un sacco di tempo e calcoli risparmiati. Nota: bisogna fare attenzione con i gruppi atomici. Se al posto di*\b(?>atomica|atomi|atono)\b avessimo usato \b(?>atomi|atomica|atono)\b, pur essendo la parola "atomica" nel nostro testo non sarebbe stata trovata la corrispondenza. Questo perché la RE avrebbe prima trovato che "atomi" poteva starci, ma poi non avrebbe trovato la fine della parola e dunque non avrebbe provato le altre due alternative, tra cui c'era proprio la parola "atomica". Performance catastrofiche Ora vediamo in che senso un'espressione regolare può avere performance davvero disastrose. I veri problemi cominciano quando si fa uso di quantificatori non limitati superiormente (come *, + e {n,}). Supponiamo di* cercare una sequenza di lettere maiuscole seguita da almeno una cifra. La RE \b([A-Z]+\d+)\b fa il lavoro che ci serve. In effetti, una riga di testo come "AS400" viene elaborata senza rallentamenti. I veri problemi cominciano, come sempre, quando le corrispondenze falliscono: in questi casi, è possibile che si scatenino una serie di backtracking che aumentano esponenzialmente con la lunghezza della stringa. Se ad esempio la stringa fosse stata "AUTHENTICAMD", il motore RE avrebbe prima trovato tutto "AUTHENTICAMD", poi avrebbe non trovato una cifra e dunque sarebbe tornato indietro di un carattere, avrebbe riprovato a trovare una cifra e così via, eseguendo un backtracking ben 12 volte prima di accorgersi che non c'è nulla da fare. Ma fin qui va ancora bene. Supponiamo invece la seguente situazione: abbiamo delle righe di valori separati da virgole, e noi vogliamo delle righe contenenti esattamente tre valori. Inoltre, non sappiamo come sono formati i nostri valori: in questo caso sarebbe comodo usare il carattere universale . (il punto). Ovviamente, però, l'espressione ^(.*,){3}$ (con flag multi-line) non va bene perché la sequenza .* ci "mangia" subito tutta la stringa, perché si tratta di un quantificatore avido: usiamo dunque la forma svogliata, ottenendo*^(.*?,){3}$ . Cosa succede, dunque, in caso di fallimento? Ipotizziamo ad esempio che nella riga i valori non siano tre ma quattro o più, tipo "asino,banana,cane,dattero,elefante": allora il motore RE troverà subito "asino,banana,cane," e poi si accorgerà che non c'è la fine della riga. Quindi, il punto troverà la virgola, poi cercherà la virgola (fallendo) e andando avanti, cosicché l'ultimo gruppo catturato diventerà "cane,dattero," e non più solo "cane,". Alla fine arriverà a "cane,dattero,elefante", non troverà la virgola e tornerà indietro. Sì, ma dove? In questa tornata è stato "espanso" l'ultimo gruppo, ma a questo punto verrà espanso il secondo, che diventerà "banana,cane," ed il terzo prima "dattero," e poi non troverà la fine della riga, quindi il secondo gruppo diventa "banana,cane,dattero," e via dicendo, con una quantità di backtracking evidentemente crescente in maniera esponenziale. Il nostro problema, in questo caso, è quel dannato punto che ci "mangia" la virgola: se avessimo usato [^,] al posto del punto, sarebbe andato tutto liscio. Le tecniche per evitare i backtracking esponenziali sono diverse:
Trucchi e funzionalità vari Ecco varie funzionalità offerte dalle ultime caratteristiche sviluppate per le RE. Gruppi con nomi Abbiamo visto che i gruppi catturati vengono referenziati da sinistra a destra (in "ordine di apparizione") con un numero progressivo da 1 in poi. Tuttavia, usare i numeri per i riferimenti può confondere le idee, e sarebbe bello poter chiamare i gruppi con un loro nome. Il Python è stato il primo linguaggio ad offrire questa funzionalità: un gruppo con nome può essere indicato con (?<...>...), dove la parte tra parentesi angolate è il nome del gruppo. Il gruppo può essere referenziato con la sintassi (?P=...) oppure con la vecchia sintassi \# (con # un numero), perché anche i gruppi con nomi ricevono un numero di assegnazione. Il framework .NET permette i gruppi con nomi, con qualche differenza. Come sequenza, oltre a quella già vista si può usare anche (?'...'...) che è più utile con linguaggio ASP (dove si fa abbondante uso di < e >). I riferimenti si fanno con la sintassi \k<...> o \k'...' indifferentemente. Inoltre, col framework .NET è possibile assegnare lo stesso nome a più gruppi, qualora si escludano a vicenda con un'alternanza. Se, infatti, volessimo catturare la cifra che segue una "a" seguita da 0-3, oppure una "b" seguita da 4-7, possiamo scrivere a(?<cifra>[0-3])|b(?<cifra>[4-7]) senza che questo ci dia un errore di compilazione. Commenti Oltre alla modalità free-space, diversi motori RE supportano i commenti "in linea" delle RE. Basta includerli tra la sequenza (?# e la parentesi chiusa. Generalmente, i linguaggi che supportano la modalità free-space supportano anche i commenti in linea, con la rilevante eccezione del Java. Modificatori locali Tutti gli interpreti di RE consentono di specificare le modalità, come visto nei paragrafi precedenti. Non tutti, però, consentono di applicare i modificatori solo a parte della RE. Per quelli che lo consentono, il gioco è semplice: basta indicare tra in mezzo a (?...) i modificatori che si vogliono usare, e questi verranno applicati sono a partire dalla destra della sequenza. Come si è detto, i modificatori sono indicati dalle varie lettere: 'i' per ignore-case, 'm' per multi-line e così via. Per "spegnere" le modalità basta indicarle dopo il segno '-' (meno). Dunque, per attivare la modalità ignore-case si deve usare (?i), mentre per disattivarla ed attivare invece quella single-line si usa (?s-i). In alternativa, anziché "accendere" e "spegnere" i modificatori, si può inserire la parte interessata dal modificatore direttamente all'interno della sequenza (?...:...), dove tra ? e : si mettono i modificatori, e tra : e ) la parte interessata. Espressioni condizionali Alcuni linguaggi supportano dei particolari costrutti di branching, per a seconda del risultato di un test viene applicata una parte della RE oppure un'altra. Si tratta delle espressioni condizionali, e possono esistere in varie forme. La forma più utile a mio avviso è quella che verifica se un gruppo di cattura è andato a buon fine: ha la sintassi (?(#)...|...), dove # indica il numero della cattura, l'espressione prima del simbolo '|' è l'espressione da applicare se il gruppo # è stato catturato, mentre quello dopo è la parte da applicare se invece non è stato catturato. Ad esempio, la RE (Grande\s)(?(1)Punto|Panda) troverà corrispondenza in "Grande Punto" e in "Panda", ma non in "Grande Panda". Un'altra forma di espressione condizionale consiste nell'usare i lookarounds: semplicemente, al posto di (#) si mette un lookaround che rappresenti il nostro test. Ad esempio, (?(?=\w{1,6}\b)\w*|.{3}) significa che si prenderanno tutte le lettere se ciò che viene dopo è una parola di 6 lettere, altrimenti si prendono i primi 3 caratteri. Nota: i più esperti avranno notato che quest'ultima forma di espressione condizionale è quasi del tutto inutile, perché lo stesso identico effetto si può ottenere con (?:(lookaround)...|...). In effetti il significato profondo di quella sintassi sfugge anche a me, ma suppongo che ce ne sia uno... Differenze tra i vari linguaggi Questa tabella riassume il supporto alle varie caratteristiche delle RE presenti nei vari linguaggi e con i pacchetti più comuni. Ne risulta alla fine che il supporto alle RE di XSLT/XPath/XQuery è molto povero, con quello di JavaScript appena migliore (ma ci si rende conto che è sufficiente per la stragrande maggioranza delle applicazioni), mentre il più completo risulta essere il framework .NET, seguito a ruota da Java e dal motore PCRE. Links e riferimenti RegExr: Online Regular Expression Testing Tool Sito per testare le proprie RE. Manca delle funzionalità più avanzate, come gruppi atomici. http://www.gskinner.com/RegExr/ Regular-Expressions.info Un'autentica miniera d'oro di informazioni. C'è tutto quello che c'è in questa guida e molto di più. Manca solo un accenno alle RE ricorsive. http://www.regular-expressions.info/ ********************** Il contenuto di questo post è rilasciato con licenza Creative Commons Attribution-Noncommercial-Share Alike 2.5 Creative Commons Attribution-Noncommercial-Share Alike 2.5
__________________
HWU Rugby Group :'( - FAQ Processori - Aurea Sectio - CogitoWeb: idee varie sviluppando nel web Ultima modifica di MaxArt : 15-06-2010 alle 21:49. |
15-06-2010, 21:51 | #3 |
Senior Member
Iscritto dal: Apr 2004
Città: Livorno
Messaggi: 6612
|
Post di servizio, non si sa mai che mi venisse in mente di aggiungere altro...
Le espressioni regolari sono molto potenti e si stanno avvicinando sempre di più ad essere un vero e proprio linguaggio di programmazione, ma... per quanto io ne faccia largo uso, invito sempre alla cautela nel loro utilizzo.
__________________
HWU Rugby Group :'( - FAQ Processori - Aurea Sectio - CogitoWeb: idee varie sviluppando nel web |
15-06-2010, 23:54 | #4 |
Senior Member
Iscritto dal: Oct 2007
Città: Padova
Messaggi: 4131
|
Un applauso per il lavoro encomiabile.
Penso però che sarebbe stato più appropriato aprire il thread nella sottosezione Tutorials . Ci penserà un moderatore a spostare il tutto e cancellare questo mio post!
__________________
As long as you are basically literate in programming, you should be able to express any logical relationship you understand. If you don’t understand a logical relationship, you can use the attempt to program it as a means to learn about it. (Chris Crawford) |
16-06-2010, 08:40 | #5 | |
Senior Member
Iscritto dal: Apr 2004
Città: Livorno
Messaggi: 6612
|
Quote:
Quindi si posta qui, poi cionci sposta. Cancellare il tuo post, e perché? La discussione può proseguire qui. Mica tutto si esaurisce alla guida: si possono fare domande e chiedere spiegazioni.
__________________
HWU Rugby Group :'( - FAQ Processori - Aurea Sectio - CogitoWeb: idee varie sviluppando nel web |
|
16-06-2010, 09:16 | #6 |
Senior Member
Iscritto dal: Apr 2000
Città: Vicino a Montecatini(Pistoia) Moto:Kawasaki Ninja ZX-9R Scudetti: 29
Messaggi: 53970
|
Lasciamolo qualche giorno qui. Intanto l'ho già aggiunto ai collegamenti
|
16-06-2010, 17:42 | #7 |
Senior Member
Iscritto dal: Oct 2006
Città: Roma
Messaggi: 1383
|
be', l'argomento a me non interessa molto ma i complimenti te li meriti tutti perché mi sembra un lavoro molto ben fatto!
|
03-04-2012, 11:40 | #8 |
Member
Iscritto dal: Mar 2007
Messaggi: 207
|
Grazie mille
__________________
Tutti gli imbecilli della Borghesia che pronunciano continuamente le parole: immorale, immoralità, moralità nell’arte e altre bestialità mi fanno pensare a Louise Villedieu, puttana da cinque franchi, che accompagnandomi una volta al Louvre, dove non era mai stata, si mise ad arrossire, a coprirsi la faccia, e tirandomi a ogni momento per la manica, mi domandava davanti alle statue e ai quadri immortali come si potesse esporre pubblicamente simili indecenze. (Charles Baudelaire)
|
01-05-2013, 13:23 | #9 |
Junior Member
Iscritto dal: Jul 2008
Messaggi: 18
|
Riprendo l'argomento per una cosa che mi sta facendo ammattire .
Ma, prima, un GRAZIE così Devo usare, sotto ubuntu, il comando rename che fa uso delle espressioni regolari. Ho la seguente situazione: un gruppo di file ai quali devo aggiungere in testa al nome una stringa ma solo se questa stringa non è già presente (e che può essere in un qualsiasi punto del nome) e solo se nel nome file è contenuta una certa parola. Esempio: Nomi dei file attuali: ABC-nome1 ABC-nome2-PAROLA ABCGHI-nome3-DEF-PAROLA ABCGHIMNPXYZ-PAROLA-nome4 ABCDEF-PAROLA-nome5 Devo aggiungere, se non già presente, la stringa DEF se nel nome file compare PAROLA. Di sicuro è che la stringa DEF appare nel nome file sempre prima di PAROLA. Quindi: nome1 non va cambiato (non compare PAROLA), nome2 deve diventare DEFABC-nome2-PAROLA, nome3 non va cambiato (appare già DEF), nome4 deve diventare DEFABCGHIMNPXYZ-PAROLA-nome4, nome5 non va cambiato (appare già DEF). Ho provato con i vari lookaround, ma senza successo; probabilmente perché "DEF" può essere in qualsiasi punto. Chi può aiutarmi? P.S.: Buon 1 maggio! |
01-05-2013, 23:35 | #10 |
Senior Member
Iscritto dal: Apr 2004
Città: Livorno
Messaggi: 6612
|
Di solito cerco di evitare i quantificatori lazy, ma nel caso di "cerca solo se la stringa (non) contiene xyz" sono la soluzione più semplice. L'uso di due lookahead fa al caso tuo, anche se può essere un po' pesante:
(?=.*?PAROLA)(?!.*?DEF)(.+) Da sostituire con "DEF$1". Testa su http://www.gskinner.com/RegExr/
__________________
HWU Rugby Group :'( - FAQ Processori - Aurea Sectio - CogitoWeb: idee varie sviluppando nel web |
02-05-2013, 05:37 | #11 |
Junior Member
Iscritto dal: Jul 2008
Messaggi: 18
|
Testato a terminale con le opzioni -v-n per simulare senza fare casini.
Funziona alla grandissima. Grazie! |
23-05-2013, 14:55 | #12 |
Senior Member
Iscritto dal: Jan 2003
Città: Milano - Udine
Messaggi: 9418
|
Per quanto riguarda il framework .NET, confermo, è davvero completo.
Di recente ho dovuto fare un lavoro di ottimizzazione delle espressioni regolari utilizzate su un'applicazione MVC piuttosto complessa e ho scoperto opzioni davvero interessanti, tra cui la possibilità di compilare ed utilizzarle in un assembly ad hoc oppure on the fly, oltre all'interpretazione usata by design dal runtime. Nella Base Class Library, è l'oggetto Regex ad identificare il motore che si fa carico di manipolare le espressioni regolari. Consiglio la lettura (avida) dei 3 articoli scritti su MSDN. Parte 1. |
09-12-2018, 10:23 | #13 |
Member
Iscritto dal: Aug 2006
Messaggi: 105
|
sostituzione regex con la stessa
buongiorno
vorrei chiedere come sostituire la stessa regex impostata come origine e cambiare solo una parte della stessa come risultato la regex è questa \d?\dx\d\d (\p{Ll} in questo caso è numero numero x numero numero spazio seguito da un carattere minuscolo ecco io vorrei convertire solo il carattere minuscolo in maiuscolo e lasciare il resto così come è o altrimenti cercare qualsiasi soluzione alternativa che restituisce lo stesso risultato grazie |
12-12-2018, 16:53 | #14 | |
Member
Iscritto dal: Aug 2006
Messaggi: 105
|
Quote:
|
|
13-12-2018, 11:02 | #15 |
Senior Member
Iscritto dal: Jan 2004
Città: Montignoso(MS)
Messaggi: 9382
|
a occhio e croce quell'espressione regolare mi sembra sintatticamente scorretta: cosa sarebbe quella parentesi tonda aperta (senza relativa parentesi chiusa) ?
__________________
"Il Meglio che si possa ottenere è evitare il peggio." I.C. |
14-12-2018, 13:07 | #16 | |
Member
Iscritto dal: Aug 2006
Messaggi: 105
|
Quote:
mi puoi aiutare sulla richiesta che avevo fatto? Ultima modifica di lcd86 : 14-12-2018 alle 13:10. Motivo: aiuto alla richiesta |
|
19-12-2018, 10:01 | #17 |
Member
Iscritto dal: Aug 2006
Messaggi: 105
|
nessuno mi può aiutare?
cercavo semplicemente la sintassi per la conversione minuscolo della prima lettera in maiuscolo di una espressione regolare... Ultima modifica di lcd86 : 20-12-2018 alle 09:06. |
Strumenti | |
|
|
Tutti gli orari sono GMT +1. Ora sono le: 23:37.