Scopo | Cercare e/o modificare del testo usando modelli formali |
Versione Python | 1.5 e superiore |
A partire dal 1 gennaio 2021 le versioni 2.x di Python non sono piu' supportate. Ti invito a consultare la corrispondente versione 3.x dell'articolo per il modulo re
Le espressioni regolari sono dei modelli, descritti con una sintassi formale, per cercare corrispondenze in un testo. I modelli sono interpretati come un insieme di istruzioni, le quali sono poi eseguite con una stringa come input per produrre un sottoinsieme di corrispondenze oppure una versione modificata della stringa originale. Il termine "espressione regolare" (regular expression in inglese - n.d.t.) viene spesso abbreviato come "regex" oppure "regexp" nella conversazione. Le espressioni possono comprendere corrispondenza letterale del testo, ripetizione, composizione di modello, diramazione ed altre sofisticate regole. Un grande numero di problemi di elaborazione di testo sono più semplici da risolvere usando una espressione regolare invece che creare un analizzatore ed elaboratore lessicale per lo scopo specifico.
Le espressioni regolari sono tipicamente usate in applicazioni che comprendono molta elaborazione di testo. Ad esempio sono comunemente usate come modelli di ricerca in programmi per la modifica di testo (text editors) usati dagli sviluppatori, compreso vi, emacs fino ai moderni ambienti integrati di programmazione (IDE). C'è anche una intera serie di utilità da riga di comando Unix tipo sed , grep ed awk . Molti linguaggi di programmazione includono il supporto per le espressioni regolari nella sintassi del linguaggio (Perl, Ruby, Awk e Tcl). Altri linguaggi tipo C, C++ e Python supportano le espressioni regolari attraverso librerie di estensione.
Ci sono varie implementazioni open source di espressioni regolari, ognuna delle quali condivide una sintassi base ma con diverse estensioni o modifiche rispetto alla loro caratteristiche avanzate. La sintassi usata nel modulo Python re si basa sulla sintassi usata nelle espressioni regolari in Perl, con pochi arricchimenti specifici di Python.
L'uso più comune per re è la ricerca di modelli nel testo. Questo esempio cerca due stringhe letterali: "questo" e "quello", in una stringa di testo.
import re
patterns = [ 'questo', 'quello' ]
text = 'questo testo ha corrispondenza nel modello?'
for pattern in patterns:
print 'Ricerca di "%s" in "%s" ->' % (pattern, text),
if re.search(pattern, text):
print 'trovata corrispondenza!'
else:
print 'nessuna corrispondenza'
search()
riceve il modello ed il testo da scorrere, e restituisce un oggetto
Match
se si trova corrispondenza, altrimenti
search()
ritorna
None
.
$ python re_simple.py Ricerca di "questo" in "questo testo ha corrispondenza nel modello?" -> trovata corrispondenza! Ricerca di "quello" in "questo testo ha corrispondenza nel modello?" -> nessuna corrispondenza
L'oggetto
Match
restituito da
search()
contiene informazioni circa la natura della corrispondenza, compresa la stringa in input originale, l'espressione regolare usata e la locazione all'interno della stringa originale nella quale si trova la corrispondenza.
import re
pattern = 'questo'
text = 'questo testo ha corrispondenza nel modello?'
match = re.search(pattern, text)
s = match.start()
e = match.end()
print 'Trovato "%s" in "%s" da %d a %d ("%s")' % \
(match.re.pattern, match.string, s, e, text[s:e])
I metodi
start()
ed
end()
forniscono gli interi indice all'interno della stringa mostrando dove si trova il testo che corrisponde al modello.
$ python re_simple_match.py Trovato "questo" in "questo testo ha corrispondenza nel modello?" da 0 a 6 ("questo")
re
comprende delle funzioni a livello di modulo per lavorare con le espressioni regolari come stringhe di testo, ma in genere è più efficace
compilare
le espressioni che un proprio programma usa frequentemente. La funzione
compile()
converte una espressione stringa in un oggetto
RegexObject
.
import re
# Pre-compilazione del modello
regexes = [ re.compile(p) for p in [ 'questo',
'quello',
]
]
text = 'questo testo ha corrispondenza nel modello?'
for regex in regexes:
print 'Ricerca di "%s" in "%s" ->' % (regex.pattern, text),
if regex.search(text):
print 'trovata corrispondenza!'
else:
print 'nessuna corrispondenza'
Le funzioni a livello di modulo mantengono una cache delle espressioni compilate, ma la dimensione della stessa è limitata e l'usare espressioni compilate direttamente significa che si può evitare il tempo necessario per la ricerca nella cache. Precompilando una qualsiasi espressione che il proprio modulo utilizza quando viene caricato si ottiene lo spostamento del lavoro di compilazione alla fase di inizializzazione dell'applicazione, invece che in un punto nel quale il programma risponde ad una azione dell'utente.
$ python re_simple_compiled.py Ricerca di "questo" in "questo testo ha corrispondenza nel modello?" -> trovata corrispondenza! Ricerca di "quello" in "questo testo ha corrispondenza nel modello?" -> nessuna corrispondenza
Fino a qui in tutti i modelli di esempio si usa
search()
per cercare singole istanze di stringhe di testo letterali. La funzione
findall()
restituisce tutte le sottostringhe dell'input che trovano corrispondenza nel modello senza sovrapporsi.
import re
text = 'abbaaabbbbaaaaa'
pattern = 'ab'
for match in re.findall(pattern, text):
print 'Trovato "%s"' % match
Ci sono due istanze di ab nella stringa in input.
$ python re_findall.py Trovato "ab" Trovato "ab"
finditer()
restituisce un iteratore che produce delle istanze di
Match
in luogo delle stringhe restituite da
findall()
import re
text = 'abbaaabbbbaaaaa'
pattern = 'ab'
for match in re.finditer(pattern, text):
s = match.start()
e = match.end()
print 'Trovato "%s" a %d:%d' % (text[s:e], s, e)
In questo esempio si cercano le stesse due occorrenze di ab , e l'istanza di Match mostra dove si trovano nell'input originale
$ python re_finditer.py Trovato "ab" a 0:2 Trovato "ab" a 5:7
Le espressioni regolari supportano modelli molto più potenti che semplici stringhe letterali di testo. I modelli possono essere ripetuti, possono essere ancorati a diverse locazioni logiche all'interno dell'input e possono essere espressi in forme compatte che non richiedono che ognuno dei caratteri letterali sia presente nel modello. Tutte queste caratteristiche vengono usate per combinare valori di testo letterali con metacaratteri che sono parte della sintassi dei modelli delle espressioni regolari implementata da re . Gli esempi che seguono utilizzeranno questo programma di test per esplorere le varianti nei modelli.
import re
def test_patterns(text, patterns=[]):
"""Dato un testo sorgente ed un elenco di modelli, cerca
corrispondenze per ogni modello all'interno del testo e le
stampa a stdout
"""
# Mostra la posizione dei caratteri ed il testo in input
print
print ''.join(str(i/10 or ' ') for i in range(len(text)))
print ''.join(str(i%10) for i in range(len(text)))
print text
# Cerca le corrispondenze per ogni modello nel testo e stampa i risultati
for pattern in patterns:
print
print 'Corrispondenza con "%s"' % pattern
for match in re.finditer(pattern, text):
s = match.start()
e = match.end()
print ' %2d : %2d = "%s"' % \
(s, e-1, text[s:e])
return
if __name__ == '__main__':
test_patterns('abbaaabbbbaaaaa', ['ab'])
L'ouput di
test_patterns()
mostra il testo in input, comprese le posizioni dei caratteri, così come gli estremi della sottostringa da ogni porzione dell'input che corrisponde al modello.
$ python re_test_patterns.py 11111 012345678901234 abbaaabbbbaaaaa Corrispondenza con "ab" 0 : 1 = "ab" 5 : 6 = "ab"
Ci sono cinque modi per esprimere ripetizione in un modello. Un modello seguito dal metacarattere * viene ripetuto zero o più volte (il che significa che il modello può anche non essere presente per costituire una corrispondenza). Con il metacarattere + il modello deve apparire almeno una volta. Usando ? si desidera che il modello appaia zero od una volta . Per uno specifico numero di corrispondenze si usa {m} dopo il modello, dove m viene sostituito dal numero di volte per le quali la corrispondenza col modello dovrebbe ripetersi. Infine, per consentire un numero di ripetizioni variabile, ma limitato, si usa {m,n} dove m è il numero minimo di ripetizioni ed n è quello massimo. Non valorizzare ( n {m,} ) vuol dire che il valore deve apparire almeno m volte, senza tetto massimo.
from re_test_patterns import test_patterns
test_patterns('abbaaabbbbaaaaa',
[ 'ab*', # a seguito da zero o più b
'ab+', # a seguito da uno o più b
'ab?', # a seguito da zero od una b
'ab{3}', # a seguito da tre b
'ab{2,3}', # a seguito da due a tre b
])
Si noti quante corrispondenze in più ci sono per ab* ed ab? rispetto a ab+ .
$ python re_repetitions.py 11111 012345678901234 abbaaabbbbaaaaa Corrispondenza con "ab*" 0 : 2 = "abb" 3 : 3 = "a" 4 : 4 = "a" 5 : 9 = "abbbb" 10 : 10 = "a" 11 : 11 = "a" 12 : 12 = "a" 13 : 13 = "a" 14 : 14 = "a" Corrispondenza con "ab+" 0 : 2 = "abb" 5 : 9 = "abbbb" Corrispondenza con "ab?" 0 : 1 = "ab" 3 : 3 = "a" 4 : 4 = "a" 5 : 6 = "ab" 10 : 10 = "a" 11 : 11 = "a" 12 : 12 = "a" 13 : 13 = "a" 14 : 14 = "a" Corrispondenza con "ab{3}" 5 : 8 = "abbb" Corrispondenza con "ab{2,3}" 0 : 2 = "abb" 5 : 8 = "abbb"
Il normale comportamento per una istruzione di ripetizione è di consumare tanto input quanto possibiile mentre si ricerca la corrispondenza del modello. Questo comportamento detto greedy (avido - n.d.t.) potrebbe generare meno corrispondenze individuali, oppure le corrispondenze potrebbero comprendere più testo in input di quanto voluto. Questo comportamento può essere disabilitato facendo seguire l'istruzione di ripetizione da ? .
from re_test_patterns import test_patterns
test_patterns('abbaaabbbbaaaaa',
[ 'ab*?', # a seguito da zero o più b
'ab+?', # a seguito da uno o più b
'ab??', # a seguito da zero od una b
'ab{3}?', # a seguito da tre b
'ab{2,3}?', # a seguito da due a tre b
])
Disabilitando l'opzione di consumo greedy dell'input per tutti i modelli dove zero o più occorrenze di b sono consentite significa che le sottostringhe nelle quali si trova corrispondenza non includono alcun carattere b .
$ python re_repetitions_non_greedy.py 11111 012345678901234 abbaaabbbbaaaaa Corrispondenza con "ab*?" 0 : 0 = "a" 3 : 3 = "a" 4 : 4 = "a" 5 : 5 = "a" 10 : 10 = "a" 11 : 11 = "a" 12 : 12 = "a" 13 : 13 = "a" 14 : 14 = "a" Corrispondenza con "ab+?" 0 : 1 = "ab" 5 : 6 = "ab" Corrispondenza con "ab??" 0 : 0 = "a" 3 : 3 = "a" 4 : 4 = "a" 5 : 5 = "a" 10 : 10 = "a" 11 : 11 = "a" 12 : 12 = "a" 13 : 13 = "a" 14 : 14 = "a" Corrispondenza con "ab{3}?" 5 : 8 = "abbb" Corrispondenza con "ab{2,3}?" 0 : 2 = "abb" 5 : 7 = "abb"
Un insieme di caratteri è un gruppo di caratteri ognuno dei quali può trovare corrispondenza in un certo punto nel modello. Ad esempio [ab] troverà corrispondenza sia con a che con b .
from re_test_patterns import test_patterns
test_patterns('abbaaabbbbaaaaa',
[ '[ab]', # sia a che b
'a[ab]+', # a seguito da uno o più a oppure b
'a[ab]+?', # a seguito da uno o più a oppure b, non greedy
])
La forma greedy dell'espressione a[ab]+ consuma l'intera stringa visto che la prima lettera è una a ed ogni carattere seguente è una a oppure una b .
$ python re_charset.py 11111 012345678901234 abbaaabbbbaaaaa Corrispondenza con "[ab]" 0 : 0 = "a" 1 : 1 = "b" 2 : 2 = "b" 3 : 3 = "a" 4 : 4 = "a" 5 : 5 = "a" 6 : 6 = "b" 7 : 7 = "b" 8 : 8 = "b" 9 : 9 = "b" 10 : 10 = "a" 11 : 11 = "a" 12 : 12 = "a" 13 : 13 = "a" 14 : 14 = "a" Corrispondenza con "a[ab]+" 0 : 14 = "abbaaabbbbaaaaa" Corrispondenza con "a[ab]+?" 0 : 1 = "ab" 3 : 4 = "aa" 5 : 6 = "ab" 10 : 11 = "aa" 12 : 13 = "aa"
Un insieme di caratteri può anche essere usato per escludere specifici caratteri. Il marcatore speciale ^ significa: 'cerca i caratteri che non sono presenti nell'insieme seguente'.
from re_test_patterns import test_patterns
test_patterns('Questa è una porzione di testo -- con punteggiatura.',
[ '[^-. ]+', # sequenze senza -, ., o spazi
])
Il modello trova tutte le sottostringhe che non contengono i caratteri: - , . , oppure uno spazio
$ python re_charset_exclude.py 1111111111222222222233333333334444444444555 01234567890123456789012345678901234567890123456789012 Questa è una porzione di testo -- con punteggiatura. Corrispondenza con "[^-. ]+" 0 : 5 = "Questa" 7 : 8 = "è" 10 : 12 = "una" 14 : 21 = "porzione" 23 : 24 = "di" 26 : 30 = "testo" 35 : 37 = "con" 39 : 51 = "punteggiatura"
Se l'insieme di caratteri cresce, la digitazione di ciascun carattere che dovrebbe (o no) trovare corrispondenza diventerebbe piuttosto tediosa. Un formato più compatto si ottiene usando una gamma di caratteri ( character ranges ) che consente di definire un insieme di caratteri in modo da includere tutti quelli adiacenti ai limiti di partenza ed arrivo.
from re_test_patterns import test_patterns
test_patterns('Questa porzione di testo -- con punteggiatura.',
[ '[a-z]+', # sequenza di lettere minuscole
'[A-Z]+', # sequenza di lettere maiuscole
'[a-zA-Z]+', # sequenza di lettere maiuscole o minuscole
'[A-Z][a-z]+', # una lettera maiuscola seguita da lettere minuscole
])
Qui la gamma a-z comprende le lettere minuscole ASCII e la gamma A-Z comprende le lettere maiuscole ASCII. Le gamme possono anche essere combinate in un singolo insieme di caratteri
$ python re_charset_ranges.py 111111111122222222223333333333444444 0123456789012345678901234567890123456789012345 Questa porzione di testo -- con punteggiatura. Corrispondenza con "[a-z]+" 1 : 5 = "uesta" 7 : 14 = "porzione" 16 : 17 = "di" 19 : 23 = "testo" 28 : 30 = "con" 32 : 44 = "punteggiatura" Corrispondenza con "[A-Z]+" 0 : 0 = "Q" Corrispondenza con "[a-zA-Z]+" 0 : 5 = "Questa" 7 : 14 = "porzione" 16 : 17 = "di" 19 : 23 = "testo" 28 : 30 = "con" 32 : 44 = "punteggiatura" Corrispondenza con "[A-Z][a-z]+" 0 : 5 = "Questa"
Un caso particolare di insieme di caratteri è il metacarattere punto ( . ), che indica che il modello dovrebbe trovare corrispondenza per qualsiasi singolo carattere in quella posizione.
from re_test_patterns import test_patterns
test_patterns('abbaaabbbbaaaaa',
[ 'a.', # a seguito da qualsiasi carattere
'b.', # b seguito da qualsiasi carattere
'a.*b', # a seguito da qualunque cosa, che finisca per b
'a.*?b', # a seguito da qualunque cosa, che finisca per b
])
Il combinare il punto con la ripetizione potrebbe portare a corrispondenze molto lunghe a meno di non usare la forma non-greedy .
$ python re_charset_dot.py 11111 012345678901234 abbaaabbbbaaaaa Corrispondenza con "a." 0 : 1 = "ab" 3 : 4 = "aa" 5 : 6 = "ab" 10 : 11 = "aa" 12 : 13 = "aa" Corrispondenza con "b." 1 : 2 = "bb" 6 : 7 = "bb" 8 : 9 = "bb" Corrispondenza con "a.*b" 0 : 9 = "abbaaabbbb" Corrispondenza con "a.*?b" 0 : 1 = "ab" 3 : 6 = "aaab"
Una rappresentazione ancora più compatta si ottiene usando sequenze predefinite di codici di escape per parecchi insiemi di caratteri predefiniti. I codici di escape riconosciuti da re sono:
Codice, Descrizione |
---|
\d, una cifra |
\D, una non-cifra |
\s, un whitespace (tabulatori / spazi / interruzioni di riga ecc.) |
\S, non whitespace |
\w, alfanumerio |
\W, non-alfanumerico |
from re_test_patterns import test_patterns
test_patterns('This is a prime #1 example!',
[ r'\d+', # sequenza di cifre
r'\D+', # sequenza di non-cifre
r'\s+', # sequenza di whitespace
r'\S+', # sequenza di non-whitespace
r'\w+', # caratteri alfanumerici
r'\W+', # non-alfanumerici
])
Queste espressioni di esempio combinano sequenze di codici di escape con ripetizioni per trovare corrispondenze di caratteri simili nella stringa di input.
$ python re_escape_codes.py 11111111112222222 012345678901234567890123456 This is a prime #1 example! Corrispondenza con "\d+" 17 : 17 = "1" Corrispondenza con "\D+" 0 : 16 = "This is a prime #" 18 : 26 = " example!" Corrispondenza con "\s+" 4 : 4 = " " 7 : 7 = " " 9 : 9 = " " 15 : 15 = " " 18 : 18 = " " Corrispondenza con "\S+" 0 : 3 = "This" 5 : 6 = "is" 8 : 8 = "a" 10 : 14 = "prime" 16 : 17 = "#1" 19 : 26 = "example!" Corrispondenza con "\w+" 0 : 3 = "This" 5 : 6 = "is" 8 : 8 = "a" 10 : 14 = "prime" 17 : 17 = "1" 19 : 25 = "example" Corrispondenza con "\W+" 4 : 4 = " " 7 : 7 = " " 9 : 9 = " " 15 : 16 = " #" 18 : 18 = " " 26 : 26 = "!"
Per cercare una corrispondenza con dei caratteri che sono parte della sintassi delle espressioni regolari occorre fare precedere gli stessi dalla barra rovesciata
from re_test_patterns import test_patterns
test_patterns(r'\d+ \D+ \s+ \S+ \w+ \W+',
[ r'\\d\+',
r'\\D\+',
r'\\s\+',
r'\\S\+',
r'\\w\+',
r'\\W\+',
])
Questi modelli includono la barra rovesciata ed il segno più, visto che questi sono metacaratteri aventi uno speciale significato in una espressione regolare sono preceduti dalla barra rovesciata.
$ python re_escape_escapes.py 1111111111222 01234567890123456789012 \d+ \D+ \s+ \S+ \w+ \W+ Corrispondenza con "\\d\+" 0 : 2 = "\d+" Corrispondenza con "\\D\+" 4 : 6 = "\D+" Corrispondenza con "\\s\+" 8 : 10 = "\s+" Corrispondenza con "\\S\+" 12 : 14 = "\S+" Corrispondenza con "\\w\+" 16 : 18 = "\w+" Corrispondenza con "\\W\+" 20 : 22 = "\W+"
Oltre a descrivere il contenuto di un modello in cui cercare corrispondenza, si possono anche specificare le posizioni relative del testo in input nelle quali il modello dovrebbe apparire usando istruzioni di ancoraggio
Codice,Significato |
---|
^,inizio della stringa o della riga |
$,fine della stringa o della riga |
\A,inizio della stringa |
\Z,fine della stringa |
\b,stringa vuota all'inizio o alla fine di una parola |
\B,string vuota non all'inizio o alla fine di una parola |
from re_test_patterns import test_patterns
test_patterns('Ecco una porzione di testo -- contiene punteggiatura.',
[ r'^\w+', # parola ad inizio stringa
r'\A\w+', # parola ad inizio stringa
r'\w+\S*$', # parola alla fine della stringa, con punteggiatura opzionale
r'\w+\S*\Z', # parola alla fine della stringa, con punteggiatura opzionale
r'\w*t\w*', # parola che contiene 't'
r'\bt\w+', # 't' ad inizio parola
r'\w+e\b', # 'e' alla fine della parola
r'\Bt\B', # 't', non inizia o finisce in una parola
])
I modelli in questo esempoio per la corrispondenza di parole ad inizio o fine stringa sono diversi in quanto la parola alla fine della stringa è seguita da un simbolo di punteggiatura per concludere la frase. Il modello \w+$ non troverebbe corrispondenza, visto che . non viene considerato un carattere alfanumerico.
$ python re_anchoring.py 1111111111222222222233333333334444444444555 01234567890123456789012345678901234567890123456789012 Ecco una porzione di testo -- contiene punteggiatura. Corrispondenza con "^\w+" 0 : 3 = "Ecco" Corrispondenza con "\A\w+" 0 : 3 = "Ecco" Corrispondenza con "\w+\S*$" 39 : 52 = "punteggiatura." Corrispondenza con "\w+\S*\Z" 39 : 52 = "punteggiatura." Corrispondenza con "\w*t\w*" 21 : 25 = "testo" 30 : 37 = "contiene" 39 : 51 = "punteggiatura" Corrispondenza con "\bt\w+" 21 : 25 = "testo" Corrispondenza con "\w+e\b" 9 : 16 = "porzione" 30 : 37 = "contiene" Corrispondenza con "\Bt\B" 24 : 24 = "t" 33 : 33 = "t" 42 : 42 = "t" 48 : 48 = "t"
In situazioni dove si sa in anticipo che si dovrebbe cercare solo un sottoinsieme dell'intero input, si può ulteriormente restringere la corrispondenza dell'espressione regolare indicando a
re
di limitare il raggio di ricerca. Ad esempio, se il proprio modello di corrispondenza appare all'inizio dell'input, usando
match()
in luogo di
search()
, si ancora la ricerca senza dovere esplicitamente includere un'ancora nel modello di ricerca.
import re
text = 'Questa è una porzione di testo -- con punteggiatura.'
pattern = 'te'
print 'Testo :', text
print 'Modello:', pattern
m = re.match(pattern, text)
print 'Match :', m
s = re.search(pattern, text)
print 'Search :', s
Visto che il testo da cercare non si trova all'inizio del testo in input, esso non viene trovato usando
match()
. La sequenza peraltro appare due volte nel testo, quindi
search()
la trova.
$ python re_match.py Testo : Questa è una porzione di testo -- con punteggiatura. Modello: te Match : None Search : <_sre.SRE_Match object at 0x1d06098>
Il metodo
search()
di una espressione regolare compilata accetta opzionalmente dei parametri posizionali (
start
ed
end
) per limitare la ricerca ad una sottostringa dell'input.
import re
text = 'Questa è una porzione di testo -- con punteggiatura.'
pattern = re.compile(r'\b\w*te\w*\b')
print 'Testo:', text
print
pos = 0
while True:
match = pattern.search(text, pos)
if not match:
break
s = match.start()
e = match.end()
print ' %2d : %2d = "%s"' % \
(s, e-1, text[s:e])
# Spostamento avanti nel testo per la ricerca successiva
pos = e
Questo esempio implementa una forma meno efficiente di
iterall()
. Ogni volta che viene trovata una corrispondenza la posizione finale di quella corrispondenza viene usata per la successiva ricerca.
$ python re_search_substring.py Testo: Questa è una porzione di testo -- con punteggiatura. 26 : 30 = "testo" 39 : 51 = "punteggiatura"
Cercare corrispondenze di modelli è la base delle potenti capacità fornite dalle espressioni regolari. L'aggiunta di gruppi ad un modello consente di isolare parte del testo corrispondente, espandendo queste capacità per creare un parser . I gruppi sono definiti racchiudendo i modelli di corrispondenza tra parentesi tonde.
from re_test_patterns import test_patterns
test_patterns('abbaaabbbbaaaaa',
[ 'a(ab)', # 'a' seguito da 'ab' letterale
'a(a*b*)', # 'a' seguito da 0-n 'a' e 0-n 'b'
'a(ab)*', # 'a' seguito da 0-n 'ab'
'a(ab)+', # 'a' seguito da 1-n 'ab'
])
Qualsiasi espressione regolare può essere convertita in un gruppo ed annidata in una espressione più grande. Tutti i modificatori di ripetizione possono essere applicati ad un gruppo nel suo insieme, richiedendo la ripetizione dell'intero modello di corrispondenza del gruppo.
$ python re_groups.py 11111 012345678901234 abbaaabbbbaaaaa Corrispondenza con "a(ab)" 4 : 6 = "aab" Corrispondenza con "a(a*b*)" 0 : 2 = "abb" 3 : 9 = "aaabbbb" 10 : 14 = "aaaaa" Corrispondenza con "a(ab)*" 0 : 0 = "a" 3 : 3 = "a" 4 : 6 = "aab" 10 : 10 = "a" 11 : 11 = "a" 12 : 12 = "a" 13 : 13 = "a" 14 : 14 = "a" Corrispondenza con "a(ab)+" 4 : 6 = "aab"
Per accedere alle sottostringhe trovate dai singoli gruppi all'interno del modello, si usa il metodo
groups()
dell'oggetto
Match
.
import re
text = 'Questa è una porzione di testo -- con punteggiatura.'
print text
print
for pattern in [ r'^(\w+)', # parola ad inizio stringa
r'(\w+)\S*$', # parola a fine stringa, con punteggiatura opzionale
r'(\bt\w+)\W+(\w+)', # parola che inizia con 't' poi un'altra parola
r'(\w+o)\b', # parola che finisce con 'o'
]:
regex = re.compile(pattern)
match = regex.search(text)
print 'Corrispondenza: "%s"' % pattern
print ' ', match.groups()
print
Match.groups()
ritorna una sequenza di stringhe nell'ordine del gruppo all'interno dell'espressione che trova corrispondenza nella stringa.
$ python re_groups_match.py Questa è una porzione di testo -- con punteggiatura. Corrispondenza: "^(\w+)" ('Questa',) Corrispondenza: "(\w+)\S*$" ('punteggiatura',) Corrispondenza: "(\bt\w+)\W+(\w+)" ('testo', 'con') Corrispondenza: "(\w+o)\b" ('testo',)
Se si sta usando un raggruppamento per trovare parti di una stringa, ma non serve tutto il contenuto delle parti che hanno trovato corrispondenza nei gruppi, si può ottenere di confrontare solo un singolo gruppo con
group()
.
import re
text = 'Questa è una porzione di testo -- con punteggiatura.'
print 'Testo in input :', text
# parola che inizia con 't, poi un'altra parola
regex = re.compile(r'(\bt\w+)\W+(\w+)')
print 'Modello di corrispondenza :', regex.pattern
match = regex.search(text)
print 'Intera corrispondenza :', match.group(0)
print 'Parola inizia con "t" :', match.group(1)
print 'Parola dopo la parola con"t":', match.group(2)
Il gruppo 0 rappresenta la stringa corrispondente all'intera espressione, ed i sotto-gruppi sono numerati a partire da 1 nell'ordine in cui le loro parentesi sinistre appaiono nell'espressione
$ python re_groups_individual.py Testo in input : Questa è una porzione di testo -- con punteggiatura. Modello di corrispondenza : (\bt\w+)\W+(\w+) Intera corrispondenza : testo -- con Parola inizia con "t" : testo Parola dopo la parola con"t": con
Python estende la sintassi base per il raggruppamento aggiungendo gruppi con intestazione . L'uso di nomi per fare riferimento ai gruppi facilita la modifica del modello di corrispondenza nel tempo, senza dovere anche modificare il codice usato per trovare le corrispondenze. Per assegnare un nome ad un gruppo si usa la sintassi (P?<nome>modello) .
import re
text = 'Questa è una porzione di testo -- con punteggiatura.'
print text
print
for pattern in [ r'^(?P<prima_parola>\w+)',
r'(?P<ultima_parola>\w+)\S*$',
r'(?P<parola_t>\bt\w+)\W+(?P<altra_parola>\w+)',
r'(?P<finisce_con_o>\w+o)\b',
]:
regex = re.compile(pattern)
match = regex.search(text)
print 'Corrispondenza "%s"' % pattern
print ' ', match.groups()
print ' ', match.groupdict()
print
Si usa
groupdict()
per recuperare i gruppi con i nomi mappati dal dizionario alle sottostringhe corrispondenti. I modelli con nome sono anche inclusi nella sequenza ordinata restituita da
groups()
$ python re_groups_named.py Questa è una porzione di testo -- con punteggiatura. Corrispondenza "^(?P\w+)" ('Questa',) {'prima_parola': 'Questa'} Corrispondenza "(?P \w+)\S*$" ('punteggiatura',) {'ultima_parola': 'punteggiatura'} Corrispondenza "(?P \bt\w+)\W+(?P \w+)" ('testo', 'con') {'parola_t': 'testo', 'altra_parola': 'con'} Corrispondenza "(?P \w+o)\b" ('testo',) {'finisce_con_o': 'testo'}
Una versione aggiornata di
test_patterns()
che mostra i gruppi con intestazione e quelli numerati che hanno trovato corrispondenza rispetto ad un modello renderà l'esempio che segue molto più facile da seguire.
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import re
def test_patterns(text, patterns=[]):
"""Dato un testo sorgente ed un elenco di modelli
cerca corrispondenze per ogni modello all'interno del testo
e le stampa su stdout.
"""
# Mostra la posizione del carattere ed il testo in input
print
print ''.join(str(i/10 or ' ') for i in range(len(text)))
print ''.join(str(i%10) for i in range(len(text)))
print text
# Cerca corrispondenza nel testo per ogni modello
# e stampa i risultati
for pattern in patterns:
print
print 'Cerco corrispondenze ... "%s"' % pattern
for match in re.finditer(pattern, text):
s = match.start()
e = match.end()
print ' %2d : %2d = "%s"' % \
(s, e-1, text[s:e])
print ' Grouppi:', match.groups()
if match.groupdict():
print ' Gruppi con intestazione:', match.groupdict()
print
return
Visto che un gruppo è esso stesso una espressione regolare completa, i gruppi possono essere annidati all'interno di altri gruppi per espressioni regolari ancor più complicate.
from re_test_patterns_groups import test_patterns
test_patterns('abbaaabbbbaaaaa',
[r'a((a*)(b*))', # 'a' seguito da 0-n 'a' e 0-n 'b'
])
In questo caso, il gruppo
(a*)
corrisponde ad una stringa vuota, quindi il valore restituito da
groups()
comprende quella stringa vuota come valore che ha trovato corrispondenza.
$ python re_groups_nested.py 11111 012345678901234 abbaaabbbbaaaaa Cerco corrispondenze ... "a((a*)(b*))" 0 : 2 = "abb" Grouppi: ('bb', '', 'bb') 3 : 9 = "aaabbbb" Grouppi: ('aabbbb', 'aa', 'bbbb') 10 : 14 = "aaaaa" Grouppi: ('aaaa', 'aaaa', '')
I gruppi sono anche utili per specificare modelli alternativi di corrispondenza. Si usa | per indicare che un modello oppure un altro dovrebbero trovare corrispondenza. Si consideri con cautela il piazzamento di | . La prima espressione in questo esempio corrisponde ad una sequenza di a seguita da una sequenza che consiste interamente di una singola lettera, a oppure b . Il secondo modello corrisponde ad una a seguita da una sequenza che potrebbe includere sia a che b . I modelli sono simili, ma le corrispondenze che ne risultano sono completamente diverse.
from re_test_patterns_groups import test_patterns
test_patterns('abbaaabbbbaaaaa',
[r'a((a+)|(b+))', # 'a' seguito da una sequanza di 'a' o da una sequenza di 'b'
r'a((a|b)+)', # 'a' seguito da una sequenza di 'a' o 'b'
])
Quando un gruppo alternativo non trova corrispondenza, ma vi è corrispondenza con l'intero modello, il valore restituito da
groups()
comprende un valore
None
nel punto nella sequenza laddove il gruppo alternativo dovrebbe apparire.
$ python re_groups_alternative.py 11111 012345678901234 abbaaabbbbaaaaa Cerco corrispondenze ... "a((a+)|(b+))" 0 : 2 = "abb" Grouppi: ('bb', None, 'bb') 3 : 5 = "aaa" Grouppi: ('aa', 'aa', None) 10 : 14 = "aaaaa" Grouppi: ('aaaa', 'aaaa', None) Cerco corrispondenze ... "a((a|b)+)" 0 : 14 = "abbaaabbbbaaaaa" Grouppi: ('bbaaabbbbaaaaa', 'a')
E' anche utile definire un gruppo contenente un sotto modello nei casi in cui la stringa che trova corrispondenza nel sotto modello non è parte di quello che si vuole estrarre dal testo completo. Questi gruppi sono detti non-capturing vale a dire che non vengono trattenuti. Per creare un gruppo non-capturing si usa la sintassi: (?:modello)
from re_test_patterns_groups import test_patterns
test_patterns('abbaaabbbbaaaaa',
[r'a((a+)|(b+))', # forma capturing
r'a((?:a+)|(?:b+))', # forma non-capturing
])
Confronto dei gruppi restituiti dalle forme capturing e non-capturing di un modello con gli stessi risultati di corrispondenza
$ python re_groups_non_capturing.py 11111 012345678901234 abbaaabbbbaaaaa Cerco corrispondenze ... "a((a+)|(b+))" 0 : 2 = "abb" Grouppi: ('bb', None, 'bb') 3 : 5 = "aaa" Grouppi: ('aa', 'aa', None) 10 : 14 = "aaaaa" Grouppi: ('aaaa', 'aaaa', None) Cerco corrispondenze ... "a((?:a+)|(?:b+))" 0 : 2 = "abb" Grouppi: ('bb',) 3 : 5 = "aaa" Grouppi: ('aa',) 10 : 14 = "aaaaa" Grouppi: ('aaaa',)
E' possibile modificare il modo nel quale il motore che cerca le corrispondenze elabora una espressione usando dei flag opzione. Questi flag possono essere combinati usando una operazione bitwise or , quindi passati a compile() , search() , match() , ed altre funzioni che accettano un modello per la ricerca.
IGNORECASE fa sì che i caratteri letterali ed i gruppi di caratteri nel modello trovino corrispondenza sia nei caratteri maiuscoli che quelli minuscoli
import re
text = 'Questo e quello'
pattern = r'\bQ\w+'
with_case = re.compile(pattern)
without_case = re.compile(pattern, re.IGNORECASE)
print 'Testo :', text
print 'Modello :', pattern
print 'Case-sensitive :', with_case.findall(text)
print 'Case-insensitive:', without_case.findall(text)
Visto che il modello comprende la lettera Q, senza impostare IGNORECASE la sola corrispondenza è con la parola Questo . Quando la differenza maiuscolo/minuscolo viene ignorata, viene trovata corrispondenza anche con quello .
$ python re_flags_ignorecase.py Testo : Questo e quello Modello : \bQ\w+ Case-sensitive : ['Questo'] Case-insensitive: ['Questo', 'quello']
Ci sono due flag che influenzano il modo in cui funziona la ricerca su righe multiple. Il flag MULTILINE controlla in che modo il codice di ricerca della corrispondenza elabora le istruzioni di ancoraggio per il testo che contiene caratteri di ritorno a capo (newline). Quando ci si trova in modalità MULTILINE le regole di ancoraggio per ^ e $ si applicano all'inizio ed alla fine di ciascuna riga, in aggiunta all'intera stringa.
import re
text = 'Questa è una porzione di testo -- senza punteggiatura.\nEd una seconda riga'
pattern = r'(^\w+)|(\w+\S*$)'
single_line = re.compile(pattern)
multiline = re.compile(pattern, re.MULTILINE)
print 'Testo :', repr(text)
print 'Modello :', pattern
print 'Singola riga :', single_line.findall(text)
print 'Riga multipla:', multiline.findall(text)
Il modello nell'esempio trova corrispondenza sulla prima ed ultima parola del testo in input. Trova corrispondenza anche con riga anche se non c'è un ritorno a capo.
$ python re_flags_multiline.py Testo : 'Questa é una porzione di testo -- con punteggiatura.\nEd una seconda riga' Modello : (^\w+)|(\w+\S*$) Singola riga : [('Questa', ''), ('', 'riga')] Riga multipla: [('Questa', ''), ('', 'punteggiatura.'), ('Ed', ''), ('', 'riga')]
DOTALL è l'altro flag con attinenza al testo multiriga. Normalmente il carattere punto . trova corrispondenza con qualsiasi cosa sia nel testo in input ad eccezione del carattere di ritorno a capo. Il flag consente al punto di trovare corrispondenza anche con il carattere di ritorno a capo.
import re
text = u'Questa è una porzione di testo -- senza punteggiatura.\nEd una seconda riga'
pattern = r'.+'
no_newlines = re.compile(pattern)
dotall = re.compile(pattern, re.DOTALL)
print 'Testo :', repr(text)
print 'Modello :', pattern
print 'No ritorni a capo :', no_newlines.findall(text)
print 'Dotall :', dotall.findall(text)
Senza il flag DOTALL ogni riga del testo in input trova corrispondenza con il modello separatamente. Aggiungendo il flag viene viceversa consumata l'intera stringa
$ python re_flags_dotall.py Testo : 'Questa è una porzione di testo -- con punteggiatura.\nEd una seconda riga' Modello : .+ No ritorni a capo : ['Questa è una porzione di testo -- con punteggiatura.', 'Ed una seconda riga'] Dotall : ['Questa è una porzione di testo -- con punteggiatura.\nEd una seconda riga']
In Python 2, gli oggetti stringa str usano l'insieme di caratteri ASCII, e l'elaborazione delle espressioni regolari assume che il modello ed il testo in input siano entrambi in ASCII. I codici di escape descritti in precedenza sono predefiniti in termini di ASCII. Questa presunzione significa che il modello \w+ trova corrispondenza con la parola "French" ma non "Français" visto che ç non fa parte dell'insieme dei caratteri ASCII. Per abilitare la corrispondenza con l'insieme di caratteri Unicode in Python 2, occorre aggiungere il flag UNICODE quando si compila il modello.
import re
import codecs
import sys
# Imposta la codifica dello standard output a UTF-8.
sys.stdout = codecs.getwriter('UTF-8')(sys.stdout)
text = u'Français złoty Österreich'
pattern = ur'\w+'
ascii_pattern = re.compile(pattern)
unicode_pattern = re.compile(pattern, re.UNICODE)
print 'Testo :', text
print 'Modello :', pattern
print 'ASCII :', u', '.join(ascii_pattern.findall(text))
print 'Unicode :', u', '.join(unicode_pattern.findall(text))
Le altre sequenze di escape ( \W , \b , \B , \d , \D , \s e \S ) sono elaborate in modo diverso per Unicode. Invece di assumere i membri dell'insieme di caratteri identificati dalla sequenza di escape, il motore dell'espressione regolare consulta il database Unicode per trovare le proprietà per ogni carattere
$ python re_flags_unicode.py Testo : Français złoty Österreich Modello : \w+ ASCII : Fran, ais, z, oty, sterreich Unicode : Français, złoty, Österreich
Il formato compatto della sintassi delle espressioni regolari può essere un impedimento mano a mano che le espressioni diventano più complicate. Quando il numero dei gruppi nella propria espressione inizia ad aumentare, sarà difficile tenere traccia del perchè ciascun elemento è richiesto ed esattamente in quale modo le parti dell'espressione interagiscono. L'uso di gruppi nominati aiuta a mitigare queste difficoltà, ma una migliore soluzione è quella di usare espressionii in modalità verbose (dettagliata), che consente di aggiungere commenti e spaziature supplementari.
Un modello per convalidare indirizzi email illustrerà come la modalità verbose facilita il lavorare con le espressioni regolari. La prima versione riconosce gli indirizzi che finiscono per uno dei tre domini di più alto livello: .com, .org, ed .edu.
import re
address = re.compile('[\w\d.+-]+@([\w\d.]+\.)+(com|org|edu)', re.UNICODE)
candidates = [
u'first.last@example.com',
u'first.last+category@gmail.com',
u'valid-address@mail.example.com',
u'not-valid@example.foo',
]
for candidate in candidates:
print
print 'Candidato:', candidate
match = address.search(candidate)
if match:
print ' Corrispondenza'
else:
print ' Nessuna corrispondenza'
Questa espressione è già complessa. Ci sono diverse classi di caratteri, gruppi, ed espressioni di ripetizione
$ python re_email_compact.py Candidato: first.last@example.com Corrispondenza Candidato: first.last+category@gmail.com Corrispondenza Candidato: valid-address@mail.example.com Corrispondenza Candidato: not-valid@example.foo Nessuna corrispondenza
Convertendo l'espressione in formato più descrittivo faciliterà l'ampliamento della stessa.
import re
address = re.compile(
'''
[\w\d.+-]+ # nome utente
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # dovremmo supportare più domini di livello più alto
''',
re.UNICODE | re.VERBOSE)
candidates = [
u'first.last@example.com',
u'first.last+category@gmail.com',
u'valid-address@mail.example.com',
u'not-valid@example.foo',
]
for candidate in candidates:
print
print 'Candidato:', candidate
match = address.search(candidate)
if match:
print ' Corrispondenza'
else:
print ' Nessuna corrispondenza'
L'espressione trova corrispondenza con lo stesso input, ma in questa forma dettagliata è più leggibile. I commenti aiutano anche ad identificare le diverse parti del modello in modo che possa essere ampliato per trovare corrispondenza con più input.
$ python re_email_verbose.py Candidato: first.last@example.com Corrispondenza Candidato: first.last+category@gmail.com Corrispondenza Candidato: valid-address@mail.example.com Corrispondenza Candidato: not-valid@example.foo Nessuna corrispondenza
La versione dettagliata elabora un input che comprende il nome di una persona e l'indirizzo email, come potrebbe apparire in una intestazione email. Il nome viene per primo e rimane separato, quindi segue l'indirizzo email racchiuso tra parentesi angolari (< e >).
import re
address = re.compile(
'''
# Un nome è composto da lettere e può includere "." per titoli,
# abbreviazoini ed iniziali intermedie
((?P<name>
([\w.,]+\s+)*[\w.,]+)
\s*
# Gli indirizzi email sono racchiusi tra parentesi angolari: < >
# ma noi ne vogliamo solo uno se trovaiamo un nome, quindi
# manteniamo la parentesi di partenza in questo gruppo
<
)? # l'intero nome è opzionale
# L'indirizzo email vero e proprio: username@domain.tld
(?P<email>
[\w\d.+-]+ # nome utente
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # limita i domini di livello più alto consentiti
)
>? # parentesi angolare di chiusura opzionale
''',
re.UNICODE | re.VERBOSE)
candidates = [
u'first.last@example.com',
u'first.last+category@gmail.com',
u'valid-address@mail.example.com',
u'not-valid@example.foo',
u'First Last <first.last@example.com>',
u'No Brackets first.last@example.com',
u'First Last',
u'First Middle Last <first.last@example.com>',
u'First M. Last <first.last@example.com>',
u'<first.last@example.com>',
]
for candidate in candidates:
print
print 'Candidato:', candidate
match = address.search(candidate)
if match:
print ' Nome corrisponde :', match.groupdict()['name']
print ' email corrisponde:', match.groupdict()['email']
else:
print ' Nessuna corrispondenza'
Come per tutti gli altri linguaggi di programmazione, la possibilità di inserire commenti dettagliati all'interno delle espressioni regolari aiuta la loro manutenzione. Questa versione finale comprende delle note di implementazione per futuri curatori di questo codice e spazi per separare i gruppi gli uni dagli altri e per evidenziare i loro livelli di annidamento.
$ python re_email_with_name.py Candidato: first.last@example.com Nome corrisponde : None email corrisponde: first.last@example.com Candidato: first.last+category@gmail.com Nome corrisponde : None email corrisponde: first.last+category@gmail.com Candidato: valid-address@mail.example.com Nome corrisponde : None email corrisponde: valid-address@mail.example.com Candidato: not-valid@example.foo Nessuna corrispondenza Candidato: First LastNome corrisponde : First Last email corrisponde: first.last@example.com Candidato: No Brackets first.last@example.com Nome corrisponde : None email corrisponde: first.last@example.com Candidato: First Last Nessuna corrispondenza Candidato: First Middle Last Nome corrisponde : First Middle Last email corrisponde: first.last@example.com Candidato: First M. Last Nome corrisponde : First M. Last email corrisponde: first.last@example.com Candidato: Nome corrisponde : None email corrisponde: first.last@example.com
In situazioni dove non si possono aggiungere flag durante la compilazione di una espressione, tipo quando si sta passando un modello ad una funzione in una libreria che verrà compilata più tardi, si possono incorporare i flag all'interno della stringa di espressione steesa. Ad esempio, per attivare la modalità case-insensitive si aggiunge (?i) all'inizio dell'espressione.
import re
text = 'Questo e quello.'
pattern = r'(?i)\bQ\w+'
regex = re.compile(pattern)
print 'Testo :', text
print 'Modello :', pattern
print 'Corrispondenza:', regex.findall(text)
Visto che l'opzione controlla il modo nel quale l'intera espressione viene elaborata, il flag dovrebbe sempre essere inserito all'inizio dell'espressione.
$ python re_flags_embedded.py Testo : Questo e quello. Modello : (?i)\bQ\w+ Corrispondenza: ['Questo', 'quello']
Le abbreviazioni per tutti i flag sono:
Flag,Abbreviazione |
---|
IGNORECASE,i |
MULTILINE,m |
DOTALL,s |
UNICODE,u |
VERBOSE,x |
I flag annidati possono essere combinati piazzandoli all'interno dello stesso gruppo. Ad esempio (?imu) attiva la modalità case-insensitive per stringhe Unicode multiriga.
Ci sono molti casi laddove è utile trovare una corrispondenza con solo una parte del modello solamente se vi è corrispondenza con qualche altra parte. Ad esempio in una espressione che analizza un indirizzo email le parentesi angolari sono entrambe contrassegnate come opzionali. In realtà, però, le parentesi dovrebbero esser accoppiate e l'espressione dovrebbe trovare corrispondenza solo se entrambe, o nessuna sono presenti. Questa versione modificata dell'espressione utilizza una asserzione positive look ahead per trovare la coppia. La sintassi per questa asserzione è (?=pattern) .
import re
address = re.compile(
'''
# Un nome è composto da lettere, e può comprendere "." per abbreviazioni
# ed iniziali del secondo nome
((?P<name>
([\w.,]+\s+)*[\w.,]+
)
\s+
) # name non è più opzionale
# LOOKAHEAD
# Gli indirizzi email sono incorporati in parentesi angolari, ma si
# vogliono le parentesi sono se sono presenti entrambe, oppure nessuna.
(?= (<.*>$) # il resto è racchiuso tra parentesi angolari
|
([^<].*[^>]$) # il resto *non* è racchiuso tra parentesi angolari
)
# parentesi angolare aperta opzionale
# L'indirizzo vero e proprio: username@domain.tld
(?P<email>
[\w\d.+-]+ # nome utente
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # limita i domini di livello più alto consentiti
)
>? # parentesi angolare chiusa opzionale
''',
re.UNICODE | re.VERBOSE)
candidates = [
u'Nome Cognome <first.last@example.com>',
u'No Parentesi first.last@example.com',
u'Parentesi aperta <first.last@example.com',
u'Parentesi Chiusa first.last@example.com>',
]
for candidate in candidates:
print
print 'Candidato:', candidate
match = address.search(candidate)
if match:
print ' Corrispondenza con il nome :', match.groupdict()['name']
print ' Corrispondenza con email :', match.groupdict()['email']
else:
print ' Nessuna corrispondenza'
Ci sono diversi importanti cambiamenti in questa versione dell'espressione. Per prima cosa la porzione del nome non è più opzionale. Questo vuole dire che gli indirizzi a se stanti non troveranno corrispondenza, ma in questo modo si previene la corrispondenza con combinazioni di nome/indirizzo formattate impropriamente. La regola di positive look ahead dopo il gruppo "name" asserisce che il resto della stringa deve essere racchiuso tra una coppia di parentesi angolari, o che non ci siano parentesi spaiate, le parentesi o sono presenti a coppie simmetriche oppure non ci sono affatto. positive look ahead viene espressa come gruppo ma la corrispondenza per un gruppo look ahead non consuma alcuna parte del testo in input, in modo che il resto del modello riprende dallo stesso punto dopo che viene trovata corrispondenza per il look ahead
$ python re_look_ahead.py Candidato: Nome CognomeCorrispondenza con il nome : Nome Cognome Corrispondenza con email : first.last@example.com Candidato: No Parentesi first.last@example.com Corrispondenza con il nome : No Parentesi Corrispondenza con email : first.last@example.com Candidato: Parentesi aperta Nessuna corrispondenza
Una asserzione negative look ahead ( (?!pattern) ) dice che il modello non trova corrispondenza con il testo che segue il punto corrente. Ad esempio il modello di riconoscimento di un indirizzo email potrebbe essere modificato in modo da ignorare gli indirizzi noreply comunemente usati dai sistemi automatizzati.
import re
address = re.compile(
'''
^
# Un indirizzo: username@domain.tld
# Ignora gli indirizzi noreply
(?!noreply@.*$)
[\w\d.+-]+ # nome utente
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # limita i domini di livello più alto consentiti
$
''',
re.UNICODE | re.VERBOSE)
candidates = [
u'first.last@example.com',
u'noreply@example.com',
]
for candidate in candidates:
print
print 'Candidato:', candidate
match = address.search(candidate)
if match:
print ' Corrispondenza:', candidate[match.start():match.end()]
else:
print ' Nessuna corrispondenza'
L'indirizzo che comincia con noreply non trova corrispondenza con il modello visto che l'asserzione look ahead fallisce.
$ python re_negative_look_ahead.py Candidato: first.last@example.com Corrispondenza: first.last@example.com Candidato: noreply@example.com Nessuna corrispondenza
Invece che cercare in avanti istanze di noreply nella porzione dell'indirizzo email riservata al nome utente, il modello può anche essere scritto usando una asserzione negativa look behind dopo che è stata trovata corrispondenza con il nome utente usando la sintassi (?<!pattern) .
import re
address = re.compile(
'''
^
# Un indirizzo: username@domain.tld
[\w\d.+-]+ # nome utente
# Ignora gli indirizzi noreply
(?<!noreply)
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # limita i domini di livello più alto consentiti
$
''',
re.UNICODE | re.VERBOSE)
candidates = [
u'first.last@example.com',
u'noreply@example.com',
]
for candidate in candidates:
print
print 'Candidato:', candidate
match = address.search(candidate)
if match:
print ' Corrispondenza:', candidate[match.start():match.end()]
else:
print ' Nessuna corrispondenza'
La ricerca a ritroso funziona in modo leggermente diverso rispetto a quella in avanti look ahead , nel senso che l'espressione deve usare un modello a lunghezza fissa. Le ripetizioni sono consentite, fintanto che le si limita ad un numero fisso (nessun carattere jolly od intervalli)
$ python re_negative_look_behind.py Candidato: first.last@example.com Corrispondenza: first.last@example.com Candidato: noreply@example.com Nessuna corrispondenza
Una asserzione look behind positiva può essere usata per trovare testo seguendo un modello che usa la sintassi (?<=pattern) . Ad esempio, questa espressione trova degli handle di Twitter (un 'twitter handle' è il nome utente che viene scelto da un sottoscrittore - n.d.t. ).
import re
twitter = re.compile(
'''
# Un twitter handle: @username
(?<=@)
([\w\d_]+) # nome utente
''',
re.UNICODE | re.VERBOSE)
text = '''Questo testo include due Twitter handle.
Uno per @ThePSF, ed uno per l'autore, @doughellmann.
'''
print text
for match in twitter.findall(text):
print 'Handle:', match
Il modello trova corrispondenza nelle sequenze di caratteri che possono comporre un handle Twitter, fintanto che sono preceduti da @ .
$ python re_look_behind.py Questo testo include due Twitter handle. Uno per @ThePSF, ed uno per l'autore, @doughellmann. Handle: ThePSF Handle: doughellmann
I valori che corrispondono possono essere usati in parti successive di una espressione. Ad esempio, lo script in questo articolo utilizzato per la ricerca delle email può essere aggiornato in modo che trovi corrispondenza solo per gli indirizzi composti da nome e cognome di una persona, includendo riferimenti a ritroso a questi gruppi. Il modo più facile per conseguire questo risultato è fare riferimento ai gruppi precedentemente estratti tramite un numero identificativo, usando \num .
import re
address = re.compile(
r'''
# Il nome normale
(\w+) # Nome
\s+
(([\w.]+)\s+)? # Secondo nome opzionale o inizilae
(\w+) # Cognome
\s+
<
# Indirizzo: nome.cognome@domain.tld
(?P<email>
\1 # nome
\.
\4 # cognome
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # limita i domini di livello più alto consentiti
)
>
''',
re.UNICODE | re.VERBOSE | re.IGNORECASE)
candidates = [
u'Nome Cognome <nome.cognome@example.com>',
u'Diverso Nome <nome.cognome@example.com>',
u'Nome SecondoNome Cognome <nome.cognome@example.com>',
u'Nome S. Cognome <nome.cognome@example.com>',
]
for candidate in candidates:
print
print 'Candidato:', candidate
match = address.search(candidate)
if match:
print ' Corrispondenza con nome :', match.group(1), match.group(4)
print ' Corrispondenza con email:', match.group(5)
else:
print ' Nessuna corrispondenza :'
Sebbene la sintassi sia semplice, la creazione di riferimenti a ritroso usando un identificativo numerico ha un paio di svantaggi. Dal punto di vista pratico, quando l'espressione viene modificata, occorre contare nuovamente i gruppi e forse aggiornare ogni riferimento. Un altro svantaggio è che è possibile fare in questo modo solo 99 riferimenti, in quanto un numero identificativo di tre cifre verrebbe interpretato come un valore ottale di un carattere invece che un riferimento ad un gruppo. D'altro canto, se si hanno più di 99 gruppi nella propria espressione ci si troveranno difficoltà di mantenimento molto più serie del non essere capaci di fare riferimento ad alcuni gruppi nell'espressione
$ python re_refer_to_group.py Candidato: Nome CognomeCorrispondenza con nome : Nome Cognome Corrispondenza con email: nome.cognome@example.com Candidato: Diverso Nome Nessuna corrispondenza : Candidato: Nome SecondoNome Cognome Corrispondenza con nome : Nome Cognome Corrispondenza con email: nome.cognome@example.com Candidato: Nome S. Cognome Corrispondenza con nome : Nome Cognome Corrispondenza con email: nome.cognome@example.com
L'elaboratore dell'espressione in Python comprende anche una estensione che usa (?P=name) per fare riferimento al nome del gruppo con il quale l'espressione ha trovato precedentemente una corrispondenza.
import re
address = re.compile(
r'''
# Il nome normale
(?P<nome>\w+)
\s+
(([\w.]+)\s+)? # Secondo nome opzionale o inizilae
(?P<cognome>\w+) # Cognome
\s+
<
# Indirizzo: nome.cognome@domain.tld
(?P<email>
(?P=nome)
\.
(?P=cognome)
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # limita i domini di livello più alto consentiti
)
>
''',
re.UNICODE | re.VERBOSE | re.IGNORECASE)
candidates = [
u'Nome Cognome <nome.cognome@example.com>',
u'Diverso Nome <nome.cognome@example.com>',
u'Nome SecondoNome Cognome <nome.cognome@example.com>',
u'Nome S. Cognome <nome.cognome@example.com>',
]
for candidate in candidates:
print
print 'Candidato:', candidate
match = address.search(candidate)
if match:
print ' Corrispondenza con name :', match.groupdict()['nome'], match.groupdict()['cognome']
print ' Corrispondenza con email:', match.groupdict()['email']
else:
print ' Nessuna corrispondenza :'
L'espressione per l'indirizzo è compilata con il flag per ignorare le maiuscole/minuscolo ( IGNORECASE ), visto che i nomi propri in genere hanno la prima lettera maiuscola al contrario degli indirizzi email.
$ python re_refer_to_named_group.py Candidato: Nome CognomeCorrispondenza con name : Nome Cognome Corrispondenza con email: nome.cognome@example.com Candidato: Diverso Nome Nessuna corrispondenza : Candidato: Nome SecondoNome Cognome Corrispondenza con name : Nome Cognome Corrispondenza con email: nome.cognome@example.com Candidato: Nome S. Cognome Corrispondenza con name : Nome Cognome Corrispondenza con email: nome.cognome@example.com
L'altro meccanismo per usare riferimenti a ritroso nelle espressioni consente di seguire un modello diverso in base al fatto che una corrispondenza con un precedente gruppo sia stato trovata o meno. Il modello di email può essere corretto in modo che le parentesi angolari siano richieste se è presente un nome e non se l'indirizzo email è a se stante. La sintassi per verificare se si è verificata corrispondenza con un gruppo è (?(id)yes-expression|no-expression) , dove id è l'identificativo (numerico o nominale, yes-expression è il modello da usare se il gruppo ha un valore e no-expression è il modello da usare altrimenti).
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import re
address = re.compile(
'''
^
# Un nome è composto da lettere, e può comprendere "." per abbreviazioni
# di titoli ed iniziali di secondo nome
(?P<nome>
([\w.]+\s+)*[\w.]+
)?
\s*
# Gli indirizzi email sono racchiusi tra parentesi angolari, ma vogliamo
# le parentesi solo se troviamo un nome
(?(nome)
# visto che abbiamo un nome il resto è racchiuso tra parentesi angolari
(?P<parentesi>(?=(<.*>$)))
|
# non abbiamo un nome ed il resto non deve comprendere parentesi angolari
(?=([^<].*[^>]$))
)
# Cerchiamo le parentesi angolari solo se l'asserzione look ahead le ha
# trovate entrambe
(?(parentesi)<|\s*)
# Indirizzo: nome.cognome@domain.tld
(?P<email>
[\w\d.+-]+ # nome utente
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # limita i domini di livello più alto consentiti
)
# Cerchiamo le parentesi angolari solo se l'asserzione look ahead le ha
# trovate entrambe
(?(parentesi)>|\s*)
$
''',
re.UNICODE | re.VERBOSE)
candidates = [
u'Nome Cognome <nome.cognome@example.com>',
u'Nessuna parentesi nome.cognome@example.com>',
u'Parentesi aperta <nome.cognome@example.com',
u'Parentesi chiusa nome.cognome@example.com>',
u'no.parentesi@example.com',
]
for candidate in candidates:
print
print 'Candidato:', candidate
match = address.search(candidate)
if match:
print ' Corrispondenza con name :', match.groupdict()['nome']
print ' Corrispondenza con email:', match.groupdict()['email']
else:
print ' Nessuna Corrispondenza'
Questa versione del recupero indirizzi email usa due controlli. Se vi è corrispondenza con il gruppo nome l'asserzione look ahead richiede entrambe le parentesi angolari ed imposta il gruppo parentesi . Se nome non trova corrispondenza, l'asserzione richiede che il resto del testo non abbia parentesi angolari attorno. Successivamente, se il gruppo parentesi è impostato, il vero modello di codice per la corrispondenza consuma le parentesi usando modelli letterali, in caso contrario consuma un qualsiasi spazio.
$ python re_id.py Candidato: Nome CognomeCorrispondenza con name : Nome Cognome Corrispondenza con email: nome.cognome@example.com Candidato: Nessuna parentesi nome.cognome@example.com> Nessuna Corrispondenza Candidato: Parentesi aperta Nessuna Corrispondenza Candidato: no.parentesi@example.com Corrispondenza con name : None Corrispondenza con email: no.parentesi@example.com
Oltre alla ricerca attraverso il testo
re
supporta anche la modifica del testo usando espressioni regolari come meccanismo di ricerca, e le modifiche possono fare riferimento a gruppi per i quali è stata trovata corrispondenza nell'espressione regolare come parte del testo del testo da sostituire. Si usa
sub()
per sostituire tutte le occorrenze del modello con un altra stringa.
import re
bold = re.compile(r'\*{2}(.*?)\*{2}', re.UNICODE)
text = 'Rendi questo in **grassetto**. Anche **questo**.'
print 'Testo :', text
print 'Grassetto:', bold.sub(r'<b>\1</b>', text)
I riferimenti al testo cha ha trovato corrispondenza può essere inserito usando la sintassi \num usata per il riferimento all'indietro qui sopra.
$ python re_sub.py Testo : Rendi questo in **grassetto**. Anche **questo**. Grassetto: Rendi questo in grassetto. Anche questo.
Per usare gruppi nominativi nella sostituzione si usa la sintassi \g<nome> .
import re
bold = re.compile(r'\*{2}(?P<bold_text>.*?)\*{2}', re.UNICODE)
text = 'Rendi questo in **grassetto**. Anche **questo**.'
print 'Testo :', text
print 'Grassetto:', bold.sub(r'<b>\g<bold_text></b>', text)
La sintassi \g<nome> funziona anche con riferimenti numerati, ed usandola si elimina ogni ambiguità tra gruppi numerici e le cifre letterali che li circondano
$ python re_sub_named_groups.py Testo : Rendi questo in **grassetto**. Anche **questo**. Grassetto: Rendi questo in grassetto. Anche questo.
Per limitare il numero delle sostituzioni eseguite si passa un valore a count .
import re
bold = re.compile(r'\*{2}(.*?)\*{2}', re.UNICODE)
text = 'Rendi questo in **grassetto**. Anche **questo**.'
print 'Testo :', text
print 'Grassetto:', bold.sub(r'<b>\1</b>', text, count=1)
Viene effettuata solo la prima sostituzione visto che count è uguale a 1 .
$ python re_sub_count.py Testo : Rendi questo in **grassetto**. Anche **questo**. Grassetto: Rendi questo in grassetto. Anche **questo**.
subn()
funziona come
sub()
a parte il fatto che ritorna sia la stringa modificata che il conteggio delle sostituzioni effettuate.
import re
bold = re.compile(r'\*{2}(.*?)\*{2}', re.UNICODE)
text = 'Rendi questo in **grassetto**. Anche **questo**.'
print 'Testo :', text
print 'Grassetto:', bold.subn(r'<b>\1</b>', text)
In questo esempio il modello di ricerca trova due corrispondenze.
$ python re_subn.py Testo : Rendi questo in **grassetto**. Anche **questo**. Grassetto: ('Rendi questo in grassetto. Anche questo.', 2)
str.split()
è uno dei metodi più frequentemente usati per smembrare le stringhe per elaborarle. Come caratteri di separazione supporta solo valori letterali, mentre talvolta è necessaria una espressione regolare se l'input non è foramattato con consistenza. Ad esempio molti linguaggi di
markup
di testo semplice definiscono i separatori di paragrafi come due o più caratteri di nuova riga (newline
\n
). In questo caso
str.split()
non può essere usata causa la parte "o più" della definizione.
Una strategia per identificare paragrafi usando
findall()
sarebbe quella di usare un modello tipo
(.+?)\n{2,}
.
import re
text = 'Paragrafo uno\nsu due righe.\n\nParagrafo due.\n\n\nParagrafo tre.'
for num, para in enumerate(re.findall(r'(.+?)\n{2,}', text, flags=re.DOTALL)):
print num, repr(para)
print
Il modello non riesce a recuperare il paragrafo tre alla fine del testo in input, come dimostrato dal fatto che "Paragrafo tre" non si trova nell'output.
$ python re_paragraphs_findall.py 0 'Paragrafo uno\nsu due righe.' 1 'Paragrafo due.'
Estendendo il modello per indicare che il paragrafo finisce con due o più caratteri di nuova riga, o con la fine dell'input risolve il problema ma rende il modello più complicato. Utilizzando
re.split()
invece che
re.findall()
consente di gestire la condizione delle estremità automaticamente e mantiene il modello semplice.
import re
text = 'Paragrafo uno\nsu due righe.\n\nParagrafo due.\n\n\nParagrafo tre.'
print 'Con findall:'
for num, para in enumerate(re.findall(r'(.+?)(\n{2,}|$)', text, flags=re.DOTALL)):
print num, repr(para)
print
print
print 'Con split:'
for num, para in enumerate(re.split(r'\n{2,}', text)):
print num, repr(para)
print
L'argomento per il modello passato a
split()
esprime la specifica del markup con più precisione: due o più caratteri di nuova riga caratterizzano un punto di separazione di paragrafi nella stringa in input.
$ python re_split.py Con findall: 0 ('Paragrafo uno\nsu due righe.', '\n\n') 1 ('Paragrafo due.', '\n\n\n') 2 ('Paragrafo tre.', '') Con split: 0 'Paragrafo uno\nsu due righe.' 1 'Paragrafo due.' 2 'Paragrafo tre.'
Il racchiudere l'espressione tra parentesi per definire un gruppo fa sì che
split()
funzioni più come
str.partition()
, in modo che restituisca i valori di separazione così come le altre parti della stringa
import re
text = 'Paragrafo uno\nsu due righe.\n\nParagrafo due.\n\n\nParagrafo tre.'
print
print 'Con split:'
for num, para in enumerate(re.split(r'(\n{2,})', text)):
print num, repr(para)
print
L'output ora comprende ciascun paragrafo, assieme alla sequenza dei caratteri di nuova riga che li separano.
$ python re_split_groups.py Con split: 0 'Paragrafo uno\nsu due righe.' 1 '\n\n' 2 'Paragrafo due.' 3 '\n\n\n' 4 'Paragrafo tre.'
Vedere anche: