re - Espressioni Regolari
Scopo: Ricerca all'interno e modifica di testo utilizzando modelli formali
Le espressioni regolari sono modelli per la corrispondenza di testo descritti con una sintassi formale. I modelli sono interpretati come un insieme di istruzioni, le quali sono eseguite con una stringa come input per produrre un sottoinsieme di corrispondenze o una versione modificata dell'originale. Il termine "espressione regolare" è spesso abbreviato in "regex" o "regexp" nel linguaggio di conversazione. Le espressioni possono includere corrispondenze letterali di testo, ripetizioni, composizioni di modello, diramazioni, e altre regole sofisticate. Un grande numero di problemi di elaborazione sono più facili da risolvere con una espressione regolare, invece che creare un analizzatore lessicale e un parser dedicati.
Le espressioni regolari sono tipicamente usate nelle applicazioni che utilizzano molta elaborazione di testo. Ad esempio, sono comunemente usate come modelli di ricerca nei programmi di elaborazione del testo utilizzati dagli sviluppatori compresi vi, emacs e i moderni ambienti integrati di sviluppo (IDE). Sono anche parte integrante delle utilità da riga di comando di Unix come sed, grep ed awk. Molti linguaggi di programmazione includono il supporto per le espressioni regolari nella sintassi del linguaggio (Perl, Ruby, Awk e Tct). Altri linguaggi supportano le espressioni regolari tramite estensioni di libreria.
Esistono diverse implementazioni open source delle espressioni regolari, ognuna di esse con un nocciolo sintattico comune ma con estensioni differenti o modifiche alle caratteristiche avanzate. La sintassi usata nel modulo re di Python è basata sulla sintassi delle espressioni regolari in Perl, con alcuni miglioramenti specifici al linguaggio.
Trovare Modelli nel Testo
L'utilizzo più comune per re è la ricerca di modelli nel testo. La funzione search()
ottiene il modello e il testo da analizzare e ritorna un oggetto Match
quando il modello viene trovato. In caso contrario search()
ritorna None
.
Ogni oggetto Match
contiene informazioni sulla natura della corrispondenza, inclusa la stringa originale in entrata, l'espressione regolare utilizzata e il punto all'interno della stringa originale dove si è verificata la corrispondenza.
# re_simple_match.py
import re
pattern = 'questo'
text = 'questo testo ha corrispondenza nel modello?'
match = re.search(pattern, text)
s = match.start()
e = match.end()
print('Trovato "{}"\nin "{}"\nda {} a {} ("{}")'.format(
match.re.pattern, match.string, s, e, text[s:e]))
I metodi starts()
ed end()
ritornano gli indici all'interno della stringa, mostrando dove si trova il testo che trova corrispondenza nel modello.
$ python3 re_simple_match.py Trovato "questo" in "questo testo ha corrispondenza nel modello?" da 0 a 6 ("questo")
Compilare Espressioni
Sebbene re includa funzioni a livello di modulo per lavorare con le espressioni regolari come stringhe di testo è più efficiente compilare le espressioni che un programma utilizza frequentemente. La funzione compile()
converte una espressione in formato stringa in un oggetto RegExObject
.
# re_simple_compiled.py
import re
# Precompila i modelli
regexes = [
re.compile(p)
for p in ['questo', 'quello']
]
text = 'questo testo ha corrispondenza nel modello?'
print('Testo: {!r}\n'.format(text))
for regex in regexes:
print('Ricerca di "{}" ->'.format(regex.pattern), end=' ')
if regex.search(text):
print('trovato!')
else:
print('non trovato')
Le funzioni a livello di modulo mantengono una cache delle espressioni compilate, ma la dimensione della cache è limitata e l'utilizzo diretto delle espressioni compilate evita il sovraccarico determinato dalla ricerca nella cache. Un altro vantaggio è che la precompilazione di tutte le espressioni avviene quando il modulo viene caricato, e questo lavoro viene spostato nella fase di avvio dell'applicazione invece che avvenire in un punto del programma che potrebbe rispondere a una azione dell'utente.
$ python3 re_simple_compiled.py Testo: 'questo testo ha corrispondenza nel modello?' Ricerca di "questo" -> trovato! Ricerca di "quello" -> non trovato
Corrispondenze multiple
Fino a qui i modelli di esempio hanno usato tutti search()
per cercare singole istanze delle stringhe letterali di testo. La funzione findall()
ritorna tutte le sottostringhe dell'input che corrispondono al modello senza sovrapporsi.
# re_findall.py
import re
text = 'abbaaabbbbaaaaa'
pattern = 'ab'
for match in re.findall(pattern, text):
print('Trovato {!r}'.format(match))
Questa stringa in entrata di esempio comprende due istanze di ab
.
$ python3 re_findall.py Trovato 'ab' Trovato 'ab'
La funzione findfilter()
ritorna un iteratore che produce istanze di Match
invece delle stringhe ritornate da findall()
.
# re_finditer.py
import re
text = 'abbaaabbbbaaaaa'
pattern = 'ab'
for match in re.finditer(pattern, text):
s = match.start()
e = match.end()
print('Trovato {!r} a {:d}:{:d}'.format(
text[s:e], s, e))
In questo esempio si cercano le stesse due occorrenze di ab
, e l'istanza di Match
mostra dove sono state trovate nell'input originale.
$ python3 re_finditer.py Trovato 'ab' a 0:2 Trovato 'ab' a 5:7
Sintassi del Modello
Le espressioni regolari supportano modelli molto più potenti di semplici stringhe letterali di testo. I modelli possono essere ripetuti, ancorati a diverse locazioni logiche all'interno del testo in entrata, e possono essere espressi in formati compatti che non richiedono che ogni carattere letterale sia presente nel modello. Tutte queste caratteristiche sono usate combinando valori di testo letterale con meta-caratteri che sono parte della sintassi del modello delle espressioni regolari implementato da re.
# re_test_patterns.py
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
"""
# Cerca ogni modello nel testo e stampa il risultato
for pattern, desc in patterns:
print("'{}' ({})\n".format(pattern, desc))
print(" '{}'".format(text))
for match in re.finditer(pattern, text):
s = match.start()
e = match.end()
substr = text[s:e]
n_backslashes = text[:s].count('\\')
prefix = '.' * (s + n_backslashes)
print(" {}'{}'".format(prefix, substr))
print()
return
if __name__ == '__main__':
test_patterns('abbaaabbbbaaaaa',
[('ab', "'a' seguito da 'b'"),
])
Gli esempi seguenti utilizzano test_patterns()
per esplorare come le variazioni nei modelli cambiano il modo nel quale trovano corrispondenza con lo stesso testo in entrata. Il risultato mostra il testo in entrata e la sottostringa da ciascuna porzione di testo in entrata che corrisponde al modello.
$ python3 re_test_patterns.py 'ab' ('a' seguito da 'b') 'abbaaabbbbaaaaa' 'ab' .....'ab'
Ripetizioni
Ci sono cinque modi per esprimere una ripetizione in un modello. Un modello seguito dal *
viene ripetuto zero o più volte (consentire a un modello di ripetersi zero volte significa che non deve necessariamente apparire affinchè ci sia corrispondenza). Se *
viene sostituito da +
, il modello deve apparire almeno una volta. Usando ?
si intende che il modello possa apparire zero o una volta. Per un numero specifico di occorrenze si una {m,n}}
dove m
è il numero minimo di ripetizioni ed n
è il massimo. Tralasciando n
({m,}
) si ricerca un valore che appaia almeno m
volte, senza limiti.
# re_repetition.py
from re_test_patterns import test_patterns
test_patterns(
'abbaabbba',
[('ab*', 'a seguito da zero o più b'),
('ab+', 'a seguito da one 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')],
)
Ci sono più corrispondenze per ab*
ed ab?
che per ab+
.
$ python3 re_repetition.py 'ab*' (a seguito da zero o più b) 'abbaabbba' 'abb' ...'a' ....'abbb' ........'a' 'ab+' (a seguito da one o più b) 'abbaabbba' 'abb' ....'abbb' 'ab?' (a seguito da zero o una b) 'abbaabbba' 'ab' ...'a' ....'ab' ........'a' 'ab{3}' (a seguito da tre b) 'abbaabbba' ....'abbb' 'ab{2,3}' (a seguito da due a tre b) 'abbaabbba' 'abb' ....'abbb'
Quando si elabora una istruzione di ripetizione, re in genere consuma tanto testo in entrata quanto possibile mentre cerca corrispondenza con il modello. Questo cosiddetto comportamento greedy (avido - n.d.t.) può risultare in meno corrispondenze individuali, oppure le corrispondenze potrebbero comprendere molto più del testo in entrata rispetto a quanto atteso. Questo comportamento può essere disabilitato facendo seguire l'istruzione di ripetizione da ?
.
# re_repetition_non_greedy.py
from re_test_patterns import test_patterns
test_patterns(
'abbaabbba',
[('ab*?', 'a seguito da zero o più b'),
('ab+?', 'a seguito da one 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 fino a tre b')],
)
Disabilitando il consumo avido del testo in entrata per tutti i modelli dove sono consentite zero occorrenze di b
significa che le sottostringhe di corrispondenza trovate non includono alcun carattere b
.
$ python3 re_repetition_non_greedy.py 'ab*?' (a seguito da zero o più b) 'abbaabbba' 'a' ...'a' ....'a' ........'a' 'ab+?' (a seguito da one o più b) 'abbaabbba' 'ab' ....'ab' 'ab??' (a seguito da zero o una b) 'abbaabbba' 'a' ...'a' ....'a' ........'a' 'ab{3}?' (a seguito da tre b) 'abbaabbba' ....'abbb' 'ab{2,3}?' (a seguito da due fino a tre b) 'abbaabbba' 'abb' ....'abb'
Insiemi di Caratteri
Un insieme di caratteri è un gruppo di caratteri, ciascuno dei quali può trovare corrispondenza in un certo punto del modello. Ad esempio [ab]
troverebbe corrispondenza sia con a
che con b
.
# re_charset.py
from re_test_patterns import test_patterns
test_patterns(
'abbaabbba',
[('[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 "avida" dell'espressione (a[ab]+)
consuma l'intera stringa visto che la prima lettera è a
e tutti i caratteri che seguono sono sia a
che b
.
$ python3 re_charset.py '[ab]' (sia a che b) 'abbaabbba' 'a' .'b' ..'b' ...'a' ....'a' .....'b' ......'b' .......'b' ........'a' 'a[ab]+' (a seguito da uno o più a oppure b) 'abbaabbba' 'abbaabbba' 'a[ab]+?' (a seguito da uno o più a oppure b, non greedy) 'abbaabbba' 'ab' ...'aa'
Un insieme di caratteri può anche essere utilizzato per escludere caratteri specifici. L'accento circonflesso (^
) significa che occorre cercare i caratteri che non sono nell'insieme che segue ^
.
# re_charset_exclude.py
from re_test_patterns import test_patterns
test_patterns(
'Questa è una porzione di testo -- con punteggiatura.',
[('[^-. ]+', 'sequenze senza -, ., o spazio')],
)
Questo modello cerca tutte le sottostringhe che non contengono i caratteri -, .,
oppure uno spazio.
$ python3 re_charset_exclude.py '[^-. ]+' (sequenze senza -, ., o spazio) 'Questa è una porzione di testo -- con punteggiatura.' 'Questa' .......'è' .........'una' .............'porzione' ......................'di' .........................'testo' ..................................'con' ......................................'punteggiatura'
Mano a mano che le dimensioni degli insiemi di caratteri crescono, la digitazione degli stessi potrebbe essere tediosa. Un formato più compatto utilizza intervalli di caratteri che possono essere usati per includere tutti i caratteri contigui tra i punti di inizio e fine specificati.
# re_charset_ranges.py
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 l'intervallo a-z
include le lettere ASCII minuscole, e l'intervallo A-Z
quelle maiuscole. Gli intervalli possono anche essere combinati in un singolo insieme di caratteri.
$ python3 re_charset_ranges.py '[a-z]+' (sequenza di lettere minuscole) 'Questa porzione di testo -- con punteggiatura.' .'uesta' .......'porzione' ................'di' ...................'testo' ............................'con' ................................'punteggiatura' '[A-Z]+' (sequenza di lettere maiuscole) 'Questa porzione di testo -- con punteggiatura.' 'Q' '[a-zA-Z]+' (sequenza di lettere maiuscole o minuscole) 'Questa porzione di testo -- con punteggiatura.' 'Questa' .......'porzione' ................'di' ...................'testo' ............................'con' ................................'punteggiatura' '[A-Z][a-z]+' (una lettera maiuscola seguita da lettere minuscole)
Come caso speciale di un insieme di caratteri, il .
) indica che il modello dovrebbe trovare corrispondenza con qualunque carattere in quella posizione.
# re_charset_dot.py
from re_test_patterns import test_patterns
test_patterns(
'abbaabbba',
[('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')],
)
La combinazione del punto con ripetizioni può risultare in corrispondenze molto lunghe, a meno di usare la forma non avida.
$ python3 re_charset_dot.py 'a.' (a seguito da qualsiasi carattere) 'abbaabbba' 'ab' ...'aa' 'b.' (b seguito da qualsiasi carattere) 'abbaabbba' .'bb' .....'bb' .......'ba' 'a.*b' (a seguito da qualunque cosa che finisca per b) 'abbaabbba' 'abbaabbb' 'a.*?b' (a seguito da qualunque cosa, che finisca per b) 'abbaabbba' 'ab' ...'aab'
Codici di Escape
Una rappresentazione ancor più compatta utilizza codici di escape per parecchi insiemi di caratteri predefiniti. I codici di escape riconosciuti da re sono elencati nella tabella seguente.
CODICE | SIGNIFICATO |
---|---|
\d | una cifra |
\D | una non cifra |
\s | spazio, tabulazione, ritorni a capo |
\S | non \s |
\w | alfanumerici |
\W | non alfanumerici |
r
elimina il problema e mantiene leggibilità.# re_escape_codes.py
from re_test_patterns import test_patterns
test_patterns(
'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 codici di escape con ripetizioni per trovare sequenze di caratteri simili nella stringa in entrata.
$ python3 re_escape_codes.py '\d+' (sequenza di cifre) 'A prime #1 example!' .........'1' '\D+' (sequenza di non cifre) 'A prime #1 example!' 'A prime #' ..........' example!' '\s+' (sequenza di whitespace) 'A prime #1 example!' .' ' .......' ' ..........' ' '\S+' (sequenza di non whitespace) 'A prime #1 example!' 'A' ..'prime' ........'#1' ...........'example!' '\w+' (caratteri alfanumerici) 'A prime #1 example!' 'A' ..'prime' .........'1' ...........'example' '\W+' (non alfanumerici) 'A prime #1 example!' .' ' .......' #' ..........' ' ..................'!'
Per trovare i caratteri che sono parte della sintassi dell'espressione regolare, si fanno precedere dal codice di escape.
# re_escape_escapes.py
from re_test_patterns import test_patterns
test_patterns(
r'\d+ \D+ \s+',
[(r'\\.\+', 'codice di escape')],
)
Il modello in questo esempio utilizza il codice di escape per la barra rovesciata e il segno più, visto che entrambi sono meta-caratteri ed hanno un significato speciale in una espressione regolare.
$ python3 re_escape_escapes.py '\\.\+' (codice di escape) '\d+ \D+ \s+' '\d+' .....'\D+' ..........'\s+'
Ancoraggio
Oltre alla descrizione del contenuto di un modello con il quale trovare corrispondenza, le locazioni relative nel testo in entrata dove dovrebbe apparire il modello possono essere specificata utilizzando istruzioni di ancoraggio. La tabella qui sotto elenca codici di ancoraggio validi.
CODICE | SIGNIFICATO |
---|---|
^ | inizio della stringa o riga |
$ | fine della stringa o riga |
\A | inizio della stringa |
\Z | fine della stringa |
\b | stringa vuota all'inizio o alla fine di una parola |
\B | stringa vuota non all'inizio o alla fine di una parola |
# re_anchoring.py
from re_test_patterns import test_patterns
test_patterns(
'Trova in parte di testo -- con punteggiatura.',
[(r'^\w+', 'parola ad inizio stringa'),
(r'\A\w+', 'parola ad inizio stringa'),
(r'\w+\S*$', 'parola verso la fine della stringa, senza punteggiatura'),
(r'\w+\S*\Z', 'parola verso la fine della stringa, senza punteggiatura'),
(r'\w*t\w*', 'parola che contiene t'),
(r'\bt\w+', 't ad inizio della parola'),
(r'\w+t\b', 't alla fine della parola'),
(r'\Bt\B', 't, non all\'inizio o fine della parola')],
)
I modelli nell'esempio per trovare corrispondenza con parole all'inizio e alla fine della stringa sono diversi poichè la parola alla fine della stringa è seguita da un segno di punteggiatura per concludere la frase. Il modello \w+$
non troverebbe corrispondenza, visto che .
non è considerato un carattere alfanumerico.
$ python3 re_anchoring.py '^\w+' (parola a inizio stringa) 'Trova in parte di testo -- con punteggiatura.' 'Trova' '\A\w+' (parola a inizio stringa) 'Trova in parte di testo -- con punteggiatura.' 'Trova' '\w+\S*$' (parola verso la fine della stringa, senza punteggiatura) 'Trova in parte di testo -- con punteggiatura.' ...............................'punteggiatura.' '\w+\S*\Z' (parola verso la fine della stringa, senza punteggiatura) 'Trova in parte di testo -- con punteggiatura.' ...............................'punteggiatura.' '\w*t\w*' (parola che contiene t) 'Trova in parte di testo -- con punteggiatura.' .........'parte' ..................'testo' ...............................'punteggiatura' '\bt\w+' (t a inizio della parola) 'Trova in parte di testo -- con punteggiatura.' ..................'testo' '\w+t\b' (t alla fine della parola) 'Trova in parte di testo -- con punteggiatura.' '\Bt\B' (t, non all'inizio o fine della parola) 'Trova in parte di testo -- con punteggiatura.' ............'t' .....................'t' ..................................'t' ........................................'t'
Costrizioni nella Ricerca
In situazioni nelle quali è noto in anticipo che solo un sottoinsieme dell'intero testo in entrata sarà oggetto di ricerca, le regole di corrispondenza dell'espressione regolare possono essere ulteriormente ristrette dicendo a re di limitare l'intervallo di ricerca. Ad esempio, se il modello deve trovarsi all'inizio del testo in entrata, allora usando match()
invece che search()
si ancorerà la ricerca senza dovere esplicitamente includere un ancoraggio nel modello di ricerca.
# re_match.py
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 letterale te
non appare all'inizio del testo in entrata, non viene trovato usando match()
. La sequenza compare altre due volte nel testo, quindi search()
la trova.
$ python3 re_match.py Testo : Questa è una porzione di testo -- con punteggiatura. Modello : te Match : None Search : <_sre.SRE_Match object; span=(25, 27), match='te'>
Il metodo fullmatch()
richiede che l'intera stringa in entrata sia usata per la corrispondenza con il modello.
# re_fullmatch.py
import re
text = 'Questa è una porzione di testo -- con punteggiatura.'
pattern = 'te'
print('Testo :', text)
print('Modello :', pattern)
m = re.search(pattern, text)
print('Search :', m)
s = re.fullmatch(pattern, text)
print('Full match :', s)
Qui search()
mostra che il modello compare nel testo in entrata, ma non consuma tutto il testo stesso, quindi fullmatch()
non rileva la corrispondenza.
$ python3 re_fullmatch.py Testo : Questa è una porzione di testo -- con punteggiatura. Modello : te Search : <_sre.SRE_Match object; span=(25, 27), match='te'> Full match : None
Il metodo search()
di una stringa compilata di espressione regolare accetta parametri opzionali di posizioni (start
ed end
) per limitare la ricerca a un sottoinsieme della stringa da cercare.
# re_search_substring.py
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} = "{}"'.format(
s, e - 1, text[s:e]))
# Si sposta in avanti nel testo per la ricerca successiva
pos = e
Questo esempio implementa un forma meno efficiente di iterall()
. Ogni volta che viene trovata una corrispondenza, la posizione di coda di quella corrispondenza viene usata per la ricerca successiva.
$ python3 re_search_substring.py Testo: Questa è una porzione di testo -- con punteggiatura. 25 : 29 = "testo" 38 : 50 = "punteggiatura"
Sezionare le Corrispondenze con i Gruppi
La ricerca di corrispondenze di modelli è la base delle potenti capacità fornite dalle espressioni regolari. L'aggiunta di gruppi a un modello isola parti del testo corrisposto, espandendo queste caratteristiche per creare un parser. I gruppi sono definiti racchiudendo i loro modelli tra parentesi.
# re_groups.py
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')],
)
Qualunque completa espressione regolare può essere convertita in un gruppo e annidata all'interno di una espressione più grande. Tutti i modificatori di ripetizione possono essere applicati a un gruppo nell'insieme, richiedendo a un modello di gruppo la ripetizione.
$ python3 re_groups.py 'a(ab)' (a seguito da ab letterale) 'abbaaabbbbaaaaa' ....'aab' 'a(a*b*)' (a seguito da 0-n a e 0-n b) 'abbaaabbbbaaaaa' 'abb' ...'aaabbbb' ..........'aaaaa' 'a(ab)*' (a seguito da 0-n ab) 'abbaaabbbbaaaaa' 'a' ...'a' ....'aab' ..........'a' ...........'a' ............'a' .............'a' ..............'a' 'a(ab)+' (a seguito da 1-n ab) 'abbaaabbbbaaaaa' ....'aab'
Per accedere alle sottostringhe corrisposte da gruppi individuali all'interno di un modello, si usi il metodo groups()
dell'oggetto Match
.
# re_groups_match.py
import re
text = 'Questa è una porzione di testo -- con punteggiatura.'
print(text)
print()
patterns = [
(r'^(\w+)', 'parola ad inizio stringa'),
(r'(\w+)\S*$', 'parola alla fine, with 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'),
]
for pattern, desc in patterns:
regex = re.compile(pattern)
match = regex.search(text)
print("'{}' ({})\n".format(pattern, desc))
print(' ', match.groups())
print()
Match.groups()
ritorna una sequenza di stringhe nell'ordine dei gruppi all'interno dell'espressione corrisposta nella stringa.
$ python3 re_groups_match.py Questa è una porzione di testo -- con punteggiatura. '^(\w+)' (parola a inizio stringa) ('Questa',) '(\w+)\S*$' (parola alla fine, with punteggiatura opzionale) ('punteggiatura',) '(\bt\w+)\W+(\w+)' (parola che inizia con t, poi un'altra parola) ('testo', 'con') '(\w+o)\b' (parola che finisce con o) ('testo',)
Per ottenere corrispondenza di un singolo gruppo, si usi il metodo group()
. Questo è utile quando il raggruppamento è usato per trovare parti di stringa, ma alcune di queste parti sono corrisposte da gruppi non necessari nei risultati.
# re_groups_individual.py
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 corrisposta dall'intera espressione, e i sottogruppi sono numerati a partire da 1
nell'ordine nel quale le loro parentesi aperte appaiono nell'espressione.
$ python3 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 del raggruppamento aggiungendo gruppi nominativi. L'uso di nomi per fare riferimento a gruppi facilita la modifica del modello nel tempo, senza dovere anche modificare il codice che usa il risultato della corrispondenza. Per impostare il nome di un gruppo, si usi la sintassi (?P<nome>modello)
.
# re_groups_named.py
import re
text = 'Questa è una porzione di testo -- con punteggiatura.'
print(text)
print()
patterns = [
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',
]
for pattern in patterns:
regex = re.compile(pattern)
match = regex.search(text)
print("'{}'".format(pattern))
print(' ', match.groups())
print(' ', match.groupdict())
print()
Si usi groupdict()
per ottenere un dizionario che mappa i nomi dei gruppi alle sottostringhe ottenute dalla ricerca. I modelli nominativi sono inclusi anche nella sequenza ordinata ritornata da groups()
.
$ python3 re_groups_named.py Questa è una porzione di testo -- con punteggiatura. '^(?P<prima_parola>\w+)' ('Questa',) {'prima_parola': 'Questa'} '(?P<ultima_parola>\w+)\S*$' ('punteggiatura',) {'ultima_parola': 'punteggiatura'} '(?P<parola_t>\bt\w+)\W+(?P<altra_parola>\w+)' ('testo', 'con') {'altra_parola': 'con', 'parola_t': 'testo'} '(?P<finisce_con_o>\w+o)\b' ('testo',) {'finisce_con_o': 'testo'}
Una versione aggiornata di test_patterns()
che mostra i gruppo numerati e nominativi corrisposti da un modello renderà gli esempi successivi più semplici da seguire.
# re_test_patterns_groups.py
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.
"""
# Cerca ciascun modello nel testo e stampa i risultati
for pattern, desc in patterns:
print('{!r} ({})\n'.format(pattern, desc))
print(' {!r}'.format(text))
for match in re.finditer(pattern, text):
s = match.start()
e = match.end()
prefix = ' ' * (s)
print(
' {}{!r}{} '.format(prefix,
text[s:e],
' ' * (len(text) - e)),
end=' ',
)
print(match.groups())
if match.groupdict():
print('{}{}'.format(
' ' * (len(text) - s),
match.groupdict()),
)
print()
return
Visto che un gruppo è a sua volta una espressione regolare completa, i gruppi possono essere annidati all'interno di altri gruppi per costruire espressioni ancora più complicate.
# re_groups_nested.py
from re_test_patterns_groups import test_patterns
test_patterns(
'abbaabbba',
[(r'a((a*)(b*))', 'a seguita da 0-n a e 0-n b')],
)
In questo caso, il gruppo (a*)
trova corrispondenza con una stringa vuota, quindi il valore ritornato da groups()
include la stringa vuota come valore corrisposto.
$ python3 re_groups_nested.py 'a((a*)(b*))' (a seguita da 0-n a e 0-n b) 'abbaabbba' 'abb' ('bb', '', 'bb') 'aabbb' ('abbb', 'a', 'bbb') 'a' ('', '', '')
I gruppi sono anche utili per specificare modelli alternativi. Si usi simbolo pipe (|
) per separare due modelli e indicare che entrambi i modelli potrebbero trovare corrispondenza. Si consideri tuttavia il piazzamento del pipe con molta cura. La prima espressione nell'esempio corrisponde con una sequenza di una a
seguita da una sequenza che consiste interamente di una singola lettera, a
o b
. Il secondo modello cerca corrispondenza con una sequenza che potrebbe includere sia una a
che una b
. I modelli sono simili, ma le corrispondenze risultanti sono completamente diverse.
# re_groups_alternative.py
from re_test_patterns_groups import test_patterns
test_patterns(
'abbaabbba',
[(r'a((a+)|(b+))', 'a poi sequenza di a o sequenza di b'),
(r'a((a|b)+)', 'a poi sequenza di [ab]')],
)
Quando un gruppo alternativo non trova corrispondenza, ma esiste corrispondenza con l'intero modello, il valore ritornato da groups()
include un valore None
al punto nella sequenza dove dovrebbe comparire il gruppo alternativo.
$ python3 re_groups_alternative.py 'a((a+)|(b+))' (a poi sequenza di a o sequenza di b) 'abbaabbba' 'abb' ('bb', None, 'bb') 'aa' ('a', 'a', None) 'a((a|b)+)' (a poi sequenza di [ab]) 'abbaabbba' 'abbaabbba' ('bbaabbba', 'a')
Definire un gruppo che contiene un sotto modello è anche utile in casi dove la stringa che corrisponde al sotto modello non è parte di ciò che dovrebbe essere estratto dall'intero testo. Questi tipi di gruppi sono chiamati non catturanti. Essi possono essere usati per descrivere ripetizioni di modelli o alternative, senza isolare la porzione corrisposta della stringa nel valore ritornato. Per creare un gruppo non catturante, si usi la sintassi (?:modello)
.
# re_groups_noncapturing.py
from re_test_patterns_groups import test_patterns
test_patterns(
'abbaabbba',
[(r'a((a+)|(b+))', 'forma catturante'),
(r'a((?:a+)|(?:b+))', 'non catturante')],
)
Nell'esempio seguente, si confrontino i gruppi ritornati dalle forme catturanti e non di un modello che trova gli stessi risultati di corrispondenza.
$ python3 re_groups_noncapturing.py 'a((a+)|(b+))' (forma catturante) 'abbaabbba' 'abb' ('bb', None, 'bb') 'aa' ('a', 'a', None) 'a((?:a+)|(?:b+))' (non catturante) 'abbaabbba' 'abb' ('bb',) 'aa' ('a',)
Opzioni di Ricerca
I flag di opzione sono usati per modificare il modo nel quale il motore di corrispondenza elabora una espressione. I flag possono essere combinati usando una operazione bitwise OR, poi sono passati a compile()
, search()
, match()
e altre funzioni che accettano un modello per la ricerca.
Corrispondenza Indipendente da Maiuscolo-Minuscolo
IGNORECASE
fa sì che i caratteri letterali e gli intervalli di caratteri nel modello trovino corrispondenza sia con i caratteri maiuscoli che quelli minuscoli.
# re_flags_ignorecase.py
import re
text = 'Test su di una una porzione di testo -- con punteggiatura.'
pattern = r'\bT\w+'
with_case = re.compile(pattern)
without_case = re.compile(pattern, re.IGNORECASE)
print('Testo:\n {!r}'.format(text))
print('Modello:\n {}'.format(pattern))
print('Case-sensitive:')
for match in with_case.findall(text):
print(' {!r}'.format(match))
print('Case-insensitive:')
for match in without_case.findall(text):
print(' {!r}'.format(match))
Visto che il modello include la lettera T
, se non viene impostato IGNORECASE
, l'unica corrispondenza è con la parola Test
. Quando viene ignorata la differenza tra maiuscolo e minuscolo si trova corrispondenza anche con testo
.
$ python3 re_flags_ignorecase.py Testo: 'Test su di una una porzione di testo -- con punteggiatura.' Modello: \bT\w+ Case-sensitive: 'Test' Case-insensitive: 'Test' 'testo'
Testo in Entrata su Righe Multiple
La ricerca su testo in entrata su righe multiple viene influenzata da due flag: MULTILANE
e DOTALL
. MULTILINE
controlla il modo nel quale il codice di corrispondenza del modello elabora le istruzioni di ancoraggio per testo che contiene caratteri di ritorno a capo. Quando la modalità multilinea viene attivata, le regole di ancoraggio per ^
e $
si applicano all'inizio e alla fine di ogni riga, oltre che sull'intera stringa.
# re_flags_multiline.py
import re
text = 'Test su di una porzione di testo -- con punteggiatura.\nUn\'altra riga.'
pattern = r'(^\w+)|(\w+\S*$)'
single_line = re.compile(pattern)
multiline = re.compile(pattern, re.MULTILINE)
print('Testo:\n {!r}'.format(text))
print('Modello:\n {}'.format(pattern))
print('Riga Singola :')
for match in single_line.findall(text):
print(' {!r}'.format(match))
print('Multiriga :')
for match in multiline.findall(text):
print(' {!r}'.format(match))
Il modello nell'esempio trova corrispondenza con la prima e ultima parola del testo in entrata. Trova riga.
alla fine della stringa, anche se non ci sono ritorni a capo.
$ python3 re_flags_multiline.py Testo: "Test su di una porzione di testo -- con punteggiatura.\nUn'altra riga." Modello: (^\w+)|(\w+\S*$) Riga Singola : ('Test', '') ('', 'riga.') Multiriga : ('Test', '') ('', 'punteggiatura.') ('Un', '') ('', 'riga.')
DOTALL
è l'altro flag legato al testo multilinea. Normalmente il carattere "punto" (.
) trova corrispondenza con qualunque testo in entrata tranne il carattere di ritorno a capo. Il flag consente al punto di trovare corrispondenza anche con i ritorni a capo.
# re_flags_dotall.py
import re
text = 'Test su di una porzione di testo -- con punteggiatura.\nUn\'altra riga.'
pattern = r'.+'
no_newlines = re.compile(pattern)
dotall = re.compile(pattern, re.DOTALL)
print('Testo:\n {!r}'.format(text))
print('Modello:\n {}'.format(pattern))
print('No ritorni a capo :')
for match in no_newlines.findall(text):
print(' {!r}'.format(match))
print('Dotall :')
for match in dotall.findall(text):
print(' {!r}'.format(match))
Senza il flag, ciascuna riga del testo in entrata corrisponde al modello separatamente. Con il flag attivato viene viceversa consumata l'intera stringa.
$ python3 re_flags_dotall.py Testo: "Test su di una porzione di testo -- con punteggiatura.\nUn'altra riga." Modello: .+ No ritorni a capo : 'Test su di una porzione di testo -- con punteggiatura.' "Un'altra riga." Dotall : "Test su di una porzione di testo -- con punteggiatura.\nUn'altra riga."
Unicode
In Python 3.x, gli oggetti str
usano l'intero insieme di caratteri Unicode e le espressioni regolari che elaborano un oggetto str
assumono che il testo in entrata e il modello siano entrambi Unicode. I codici di escape precedentemente descritti sono definiti in termini di Unicode nella modalità predefinita. Queste assunzioni implicano che il modello \w+
troverà corrispondenza sia con "Franch" che con "Français". Per restringere i codici di escape al solo insieme di caratteri ASCII, così come è predefinito in Python 2.x, si usi il flag ASCII
quando si compila il modello oppure quando si chiamano le funzioni a livello di modulo search()
e match()
.
# re_flags_ascii.py
import re
text = u'François złoty Österreich'
pattern = r'\w+'
ascii_pattern = re.compile(pattern, re.ASCII)
unicode_pattern = re.compile(pattern)
print('Testo :', text)
print('Modello :', pattern)
print('ASCII :', list(ascii_pattern.findall(text)))
print('Unicode :', list(unicode_pattern.findall(text)))
Anche le altre sequenze di escape (\W
, \b
, \B
, \d
, \D
, \s
ed \S
) sono elaborate in modo diverso per il testo ASCII. Invece di consultare il database Unicode per trovare le proprietà di ogni carattere, re usa la definizione dell'insieme di caratteri ASCII.
$ python3 re_flags_ascii.py Testo : François złoty Österreich Modello : \w+ ASCII : ['Fran', 'ois', 'z', 'oty', 'sterreich'] Unicode : ['François', 'złoty', 'Österreich']
Sintassi Verbosa dell'Espressione
Il formato compatto della sintassi dell'espressione regolare può diventare un ostacolo mano a mano che le espressioni diventano più complicate. Quando il numero dei gruppi in una espressione aumenta, ci sarà molto più lavoro per tenere traccia del perchè ciascun elemento è necessario e come interagiscono esattamente le parti dell'espressione. L'utilizzo di gruppi nominativi allevia questi disagi, ma una soluzione migliore è quella di usare la modalità verbosa nelle espressioni, la quale consente di incorporare commenti e spazi supplementari nel modello.
Un modello per validare indirizzi email illustrerà come la modalità verbosa facilita il lavorare con le espressioni regolari. La prima versione riconosce gli indirizzi che terminano in uno dei primi 3 livelli principali dei domini: .com
, .org
od .edu
.
# re_email_compact.py
import re
address = re.compile('[\w\d.+-]+@([\w\d.]+\.)+(com|org|edu)')
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:
match = address.search(candidate)
print('{:<30} {}'.format(
candidate, 'Corrispondenza' if match else 'Nessuna Corrispondenza')
)
Questa espressione è già complessa. Ci sono parecchie classi di caratteri, gruppi ed espressioni di ripetizione.
$ python3 re_email_compact.py first.last@example.com Corrispondenza first.last+category@gmail.com Corrispondenza valid-address@mail.example.com Corrispondenza not-valid@example.foo Nessuna Corrispondenza
Convertire l'espressione in un formato più verboso ne faciliterà l'estensione.
# re_email_verbose.py
import re
address = re.compile(
'''
[\w\d.+-]+ # nome utente
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # TODO: supportare altri livelli principali di dominio
''',
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:
match = address.search(candidate)
print('{:<30} {}'.format(
candidate, 'Corrispondenza' if match else 'Nessuna Corrispondenza'),
)
L'espressione trova corrispondenza con lo stesso testo in entrata, ma in questo formato esteso è più facile da leggere. I commenti aiutano anche a identificare le parti differenti del modello in modo che possano essere espanse per trovare corrispondenza con ulteriore testo in entrata.
$ python3 re_email_verbose.py first.last@example.com Corrispondenza first.last+category@gmail.com Corrispondenza valid-address@mail.example.com Corrispondenza not-valid@example.foo Nessuna Corrispondenza
Questa versione estesa elabora testo in entrata che comprende il nome della persona e l'indirizzo email, così come potrebbe apparire in una intestazione email. Il nome viene prima e rimane a se stante, poi segue l'indirizzo email, racchiuso tra parentesi angolari (<
e >
).
# re_email_with_name.py
import re
address = re.compile(
'''
# Un nome è composto da lettere e può includere "." per titoli,
# abbreviazini ed iniziali intermedie
((?P<name>
([\w.,]+\s+)*[\w.,]+)
\s*
# Gli indirizzi email sono racchiusi tra parentesi angolari: < >
# solo se troviamo un nome, quindi
# manteniamo la parentesi di partenza in questo gruppo
<
)? # # l'intero nome è opzionalre
# 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.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('Candidato:', candidate)
match = address.search(candidate)
if match:
print(' Nome :', match.groupdict()['name'])
print(' Email:', match.groupdict()['email'])
else:
print(' Nessuna corrispondenza')
Così come per altri linguaggi di programmazione, la capacità di inserire commenti all'interno delle espressioni regolari ne aiuta la mantenibilità. La versione finale include note di implementazione per i futuri manutentori e spazi che separano ciascun gruppo e ne evidenziano il livello di indentazione.
$ python3 re_email_with_name.py Candidato: first.last@example.com Nome : None Email: first.last@example.com Candidato: first.last+category@gmail.com Nome : None Email: first.last+category@gmail.com Candidato: valid-address@mail.example.com Nome : None Email: valid-address@mail.example.com Candidato: not-valid@example.foo Nessuna corrispondenza Candidato: First Last <first.last@example.com> Nome : First Last Email: first.last@example.com Candidato: No Brackets first.last@example.com Nome : None Email: first.last@example.com Candidato: First Last Nessuna corrispondenza Candidato: First Middle Last <first.last@example.com> Nome : First Middle Last Email: first.last@example.com Candidato: First M. Last <first.last@example.com> Nome : First M. Last Email: first.last@example.com Candidato: <first.last@example.com> Nome : None Email: first.last@example.com
Incorporare Flag nei Modelli
In situazioni dove i flag non possono essere aggiunti quando si compila una espressione, come quando un modello viene passato come argomento a una funzione di libreria che verrà compilata successivamente, i flag possono essere incorporati all'interno della stringa stessa dell'espressione. Ad esempio, per attivare la corrispondenza indipendente da maiuscolo-minuscolo, si aggiunga (?i)
all'inizio dell'espressione.
# re_flags_embedded.py
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 le opzioni controllano il modo nel quale l'intera espressione viene valutata o elaborata, esse dovrebbero comparire sempre all'inizio dell'espressione.
$ python3 re_flags_embedded.py Testo : Questo e quello. Modello : (?i)\bQ\w+ Corrispondenza: ['Questo', 'quello']
Le abbreviazioni per tutti i flag sono elencati qui sotto:
FLAG | ABBREVIAZIONE |
---|---|
ASCII | a |
IGNORECASE | i |
MULTILINE | m |
DOTALL | s |
VERBOSE | x |
I flag incorporati possono essere combinati piazzandoli all'interno dello stesso gruppo. Ad esempio (?im)
attiva la corrispondenza indipendente dal maiuscolo-minuscolo per le stringhe multiriga.
Guardare Avanti od Indietro
In molti casi, è utile cercare corrispondenza con una parte del modello solo se ne corrisponde anche un'altra parte. Ad esempio nell'espressione che elabora l'email, le parentesi angolari sono marcate come opzionali. Realisticamente, esse dovrebbero essere accoppiate, e l'espressione dovrebbe trovare corrispondenza solo se entrambe sono presenti, oppure nessuna di esse. Questa versione modificata dell'espressione usa una asserzione positive look ahead (guardare avanti positiva - n.d.t.) per trovare la corrispondenza con la coppia. La sintassi di questa asserzione è (?=modello)
.
# re_look_ahead.py
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+
) # il nome non è più opzionale
# RICERCA IN AVANTI
# Gli indirizzi email sono incorporati in parentesi angolari, solo se
# sono presenti entrambe, oppure nessuna.
(?= (<.*>$) # il resto è racchiuso tra parentesi angolari
|
([^<].*[^>]$) # il resto *non* è racchiuso tra parentesi angolari
)
<? # parentesi angolare di apertura opzionale
# L'indirizzo stesso: username@domain.tld
(?P<email>
[\w\d.+-]+ # nome utente
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # limita ai domini di livello più alto consentiti
)
>? # parentesi angolare di chiusura opzionale
''',
re.VERBOSE)
candidates = [
u'Nome Cognome <first.last@example.com>',
u'Nessuna parentesi first.last@example.com',
u'Parentesi di apertura <first.last@example.com',
u'Parentesi di chiusura first.last@example.com>',
]
for candidate in candidates:
print('Candidato:', candidate)
match = address.search(candidate)
if match:
print(' Nome :', match.groupdict()['name'])
print(' Email:', match.groupdict()['email'])
else:
print(' Nessuna corrispondenza')
Ci sono diverse importanti modifiche in questa versione dell'espressione. Per prima cosa la porzione del nome non è più opzionale. Il che vuol dire che indirizzi da soli non trovano corrispondenza, ma viene anche evitata la corrispondenza di combinazioni nome/indirizzo formattate impropriamente. La regola positive look ahead dopo il gruppo "name" asserisce che il resto della stringa è racchiusa tra parentesi angolari oppure non ci sono parentesi disaccoppiate; entrambe o nessuna parentesi sono presenti. L'asserzione è espressa come gruppo, ma la ricerca di corrispondenza con un gruppo look ahead non consuma alcuna parte del testo in input, quindi il resto del modello riprende dallo stesso punto dopo che si è trovata la corrispondenza
$ python3 re_look_ahead.py Candidato: Nome Cognome <first.last@example.com> Nome : Nome Cognome Email: first.last@example.com Candidato: Nessuna parentesi first.last@example.com Nome : Nessuna parentesi Email: first.last@example.com Candidato: Parentesi di apertura <first.last@example.com Nessuna corrispondenza Candidato: Parentesi di chiusura first.last@example.com> Nessuna corrispondenza
Una asserzione negative look ahead (guardare avanti negativa - n.d.t) ((?!modello)
) dice che il modello non trova corrispondenza con il testo che segue il punto corrente. Ad esempio il modello di riconoscimento dell'email può essere modificato affichè ignori gli indirizzi noreply
che sono usati comunemente dai sistemi automatici.
# re_negative_look_ahead.py
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.VERBOSE)
candidates = [
u'first.last@example.com',
u'noreply@example.com',
]
for candidate in candidates:
print('Candidati:', candidate)
match = address.search(candidate)
if match:
print(' Corrispondenza:', candidate[match.start():match.end()])
else:
print(' Nessuna corrispondenza')
L'indirizzo che inizia con noreply
non trova corrispondenza nel modello, visto che l'asserzione look ahead fallisce.
$ python3 re_negative_look_ahead.py Candidati: first.last@example.com Corrispondenza: first.last@example.com Candidati: noreply@example.com Nessuna corrispondenza
Invece che cercare in avanti per noreply
nella porzione del nome utente nell'indirizzo email, il modello può essere scritto in alternativa con l'asserzione negative look behind (guardare indietro negativa - n.d.t.) dopo che il nome utente viene trovato usando la sintassi (?<!modello)
.
# re_negative_look_behind.py
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.VERBOSE)
candidates = [
u'first.last@example.com',
u'noreply@example.com',
]
for candidate in candidates:
print('Candidati:', 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 da quella in avanti, nel senso che l'espressione deve usare un modella a lunghezza fissa. Le ripetizioni sono concesse, fintanto che ce ne sono un numero prefissato (nessun carattere jolly o intervalli).
$ python3 re_negative_look_behind.py Candidati: first.last@example.com Corrispondenza: first.last@example.com Candidati: noreply@example.com Nessuna corrispondenza
Una asserzione positive look behind (guardare indietro positiva - n.d.t.), può essere usata per trovare testo che segue un modello usando la sintassi (?<=modello)
. Nell'esempio seguente l'espressione trova gli
# re_look_behind.py
import re
twitter = re.compile(
'''
# Un handle di Twitter: @username
(?<=@)
([\w\d_]+) # username
''',
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 con sequenze di caratteri che possono comporre un handle di Twitter, fintanto che sono preceduti da @
.
$ python3 re_look_behind.py Questo testo include due Twitter handle. Uno per @ThePSF, e uno per l'autore, @doughellmann. Handle: ThePSF Handle: doughellmann
Espressioni Autoreferenzianti
I valori corrisposti trovati possono essere usati in successive parti di una espressione. L'esempio email può essere aggiornato per fare sì che vengano corrisposti solo i nomi e cognomi di una persona, includendo riferimenti all'indietro per questi gruppi. Il modo più facile per ottenere questo risultato è di fare riferimento al gruppo corrisposto in precedenza tramite il suo ID, usando \num
.
# re_refer_to_group.py
import re
address = re.compile(
r'''
# Il nome normale
(\w+) # nome
\s+
(([\w.]+)\s+)? # Secondo nome opzionale od iniziale
(\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.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('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 all'indietro per ID numerici ha alcuni svantaggi. Da un punto di vista pratico, se l'espressione cambia, i gruppi devono essere contati nuovamente e ciascun riferimento deve essere aggiornato. Un altro svantaggio è che si possono usare fino a 99 riferimenti, visto che un numero di ID a tre cifre sarebbe interpretato come un valore di carattere ottale invece che un riferimento a un gruppo. Naturalmente se ci fossero più di 99 gruppi in una espressione ci sarebbero problemi di mantenimento ben maggiori del non essere in grado di creare riferimenti a ciascuno di essi.
$ python3 re_refer_to_group.py Candidato: Nome Cognome <nome.cognome@example.com> Corrispondenza con nome : Nome Cognome Corrispondenza con email: nome.cognome@example.com Candidato: Diverso Nome <nome.cognome@example.com> Nessuna corrispondenza Candidato: Nome SecondoNome Cognome <nome.cognome@example.com> Corrispondenza con nome : Nome Cognome Corrispondenza con email: nome.cognome@example.com Candidato: Nome S. Cognome <nome.cognome@example.com> Corrispondenza con nome : Nome Cognome Corrispondenza con email: nome.cognome@example.com
L'elaboratore di espressioni di Python include una estensione che usa (?P=nome)
per fare riferimento al valore di un gruppo nominativo che è stato corrisposto precedentemente nell'espressione.
# re_refer_to_named_group.py
import re
address = re.compile(
'''
# Il nome normale
(?P<first_name>\w+)
\s+
(([\w.]+)\s+)? # secondo nome opzionale od iniziali
(?P<last_name>\w+)
\s+
<
# Indirizzo: nome.cognome@domain.tld
(?P<email>
(?P=first_name)
\.
(?P=last_name)
@
([\w\d.]+\.)+ # prefisso del nome di dominio
(com|org|edu) # limita i domini di livello più alto consentiti
)
>
''',
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('Candidate:', candidate)
match = address.search(candidate)
if match:
print(' Corrispondenza con nome :', match.groupdict()['first_name'],
end=' ')
print(match.groupdict()['last_name'])
print(' Corrispondenza con email:', match.groupdict()['email'])
else:
print(' Nessuna corrispondenza')
L'espressione dell'indirizzo è compilata con attivato il flag IGNORECASE
, visto che i nomi propri in genere hanno la prima lettera maiuscola ma gli indirizzi email no.
$ python3 re_refer_to_named_group.py Candidate: Nome Cognome <nome.cognome@example.com> Corrispondenza con nome : Nome Cognome Corrispondenza con email: nome.cognome@example.com Candidate: Diverso Nome <nome.cognome@example.com> Nessuna corrispondenza Candidate: Nome SecondoNome Cognome <nome.cognome@example.com> Corrispondenza con nome : Nome Cognome Corrispondenza con email: nome.cognome@example.com Candidate: Nome S. Cognome <nome.cognome@example.com> Corrispondenza con nome : Nome Cognome Corrispondenza con email: nome.cognome@example.com
L'altro meccanismo per utilizzare riferimenti all'indietro nelle espressioni sceglie un modello diverso in base all'eventuale corrispondenza con un gruppo precedente. Il modello email può essere corretto in modo che le parentesi angolari siano richieste se è presente un nome, e non richieste se l'indirizzo email è a se stante. La sintassi per verificare se un gruppo ha trovato corrispondenza è (?(id)espressione-sì|espressione-no)
, dove id
è il nome o il numero del gruppo, espressione-si
è il modello da usare se il gruppo ha un valore ed espressione-no
è il modello da usare altrimenti.
# re_id.py
import re
address = re.compile(
'''
^
# Un nome è composto da lettere, e può comprendere "." per abbreviazioni
# di titoli ed iniziali di secondo nome
(?P<name>
([\w.]+\s+)*[\w.]+
)?
\s*
# Gli indirizzi email sono racchiusi tra parentesi angolari, ma vogliamo
# le parentesi solo se troviamo un nome
(?(name)
# visto che abbiamo un nome il resto è racchiuso tra parentesi angolari
(?P<brackets>(?=(<.*>$)))
|
# 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
(?(brackets)<|\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
(?(brackets)>|\s*)
$
''',
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('Candidate:', candidate)
match = address.search(candidate)
if match:
print(' Corrispondenza con name :', match.groupdict()['name'])
print(' Corrispondenza con email:', match.groupdict()['email'])
else:
print(' Nessuna corrispondenza')
Questa versione dell'elaboratore di stringhe email usa due test. Se il gruppo del nome ha corrispondenza, allora l'asserzione look ahead richiede entrambe le parentesi angolari e imposta il gruppo brackets
, se il nome non è corrisposto l'asserzione richiede che il resto del testo non sia racchiuso tra parentesi angolari. Successivamente, se il gruppo brackets
è impostato, il reale codice di corrispondenza del modello consuma le parentesi nella stringa in entrata usando modelli letterali, altrimenti consuma qualsiasi spazio bianco.
$ python3 re_id.py Candidate: Nome Cognome <nome.cognome@example.com> Corrispondenza con name : Nome Cognome Corrispondenza con email: nome.cognome@example.com Candidate: Nessuna parentesi nome.cognome@example.com> Nessuna corrispondenza Candidate: Parentesi aperta <nome.cognome@example.com Nessuna corrispondenza Candidate: Parentesi chiusa nome.cognome@example.com> Nessuna corrispondenza Candidate: no.parentesi@example.com Corrispondenza con name : None Corrispondenza con email: no.parentesi@example.com
Modificare Stringhe con i Modelli
Altre alla ricerca nel testo, re ne supporta anche la modifica usando le espressioni regolari come meccanismo di ricerca, e la sostituzione può fare riferimento ai gruppi che hanno trovato corrispondenza nel modello come parte del testo in sostituzione. Si usi sub()
per sostituire tutte le occorrenze di un modello con un'altra stringa.
# re_sub.py
import re
bold = re.compile(r'\*{2}(.*?)\*{2}')
text = 'Rendi questo in **grassetto**. Anche **questo**.'
print('Testo:', text)
print('Grassetto:', bold.sub(r'<b>\1</b>', text))
I riferimenti al testo corrisposto dal modello possono essere inseriti usando la sintassi \num
usata per i riferimenti all'indietro.
$ python3 re_sub.py Testo: Rendi questo in **grassetto**. Anche **questo**. Grassetto: Rendi questo in <b>grassetto</b>. Anche <b>questo</b>.
Per usare gruppi nominativi nelle sostituzioni, si usi la sintassi \g<nome>
.
# re_sub_named_groups.py
import re
bold = re.compile(r'\*{2}(?P<bold_text>.*?)\*{2}')
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, e usandola si eliminano le ambiguità tra gruppi numerati ed cifre letterali circostanti.
$ python3 re_sub_named_groups.py Testo: Rendi questo in **grassetto**. Anche **questo**. Grassetto: Rendi questo in <b>grassetto</b>. Anche <b>questo</b>.
Si passi un valore a count
per limitare il numero di sostituzioni eseguite.
# re_sub_count.py
import re
bold = re.compile(r'\*{2}(.*?)\*{2}')
text = 'Rendi questo in **grassetto**. Anche **questo**.'
print('Testo:', text)
print('Grassetto:', bold.sub(r'<b>\1</b>', text, count=1))
Viene eseguita solo la prima sostituzione in quanto count
equivale ad 1.
$ python3 re_sub_count.py Testo: Rendi questo in **grassetto**. Anche **questo**. Grassetto: Rendi questo in <b>grassetto</b>. Anche **questo**.
subn()
funziona come sub()
eccetto che ritorna sia la stringa modificata che il numero di sostituzioni effettuate.
# re_subn.py
import re
bold = re.compile(r'\*{2}(.*?)\*{2}')
text = 'Rendi questo in **grassetto**. Anche **questo**.'
print('Testo:', text)
print('Grassetto:', bold.subn(r'<b>\1</b>', text))
Il modello di ricerca nell'esempio trova due corrispondenze.
$ python3 re_subn.py Testo: Rendi questo in **grassetto**. Anche **questo**. Grassetto: ('Rendi questo in <b>grassetto</b>. Anche <b>questo</b>.', 2)
Dividere con i Modelli
str.split()
è uno dei metodi più comunemente usati per separare stringhe per elaborarle. Tuttavia supporta solo l'uso di valori letterali come separatori e talvolta è necessaria una espressione regolare se il testo in entrata non è formattato consistentemente. Ad esempio molti linguaggi di marcatura di testo semplice definiscono i separatori di paragrafi come due o più caratteri di ritorno a capo (\n
). In questo caso str.split()
non può essere usato data la parte "o più" della definizione.
Una strategia per identificare paragrafi è l'utilizzo di findall()
con un modello come (.+?)\n{2,}
.
# re_paragraphs_findall.py
import re
text = '''Paragrafo uno
su due righe.
Paragrafo due.
Paragrafo tre.'''
for num, para in enumerate(re.findall(r'(.+?)\n{2,}',
text,
flags=re.DOTALL)
):
print(num, repr(para))
print()
Questo modello non funziona per paragrafi alla fine del testo in entrata, come dimostrato dal fatto che "Paragrafo tre" non è parte del risultato.
$ python3 re_paragraphs_findall.py 0 'Paragrafo uno\nsu due righe.' 1 'Paragrafo due.'
Utilizzando re.split()
invece che re.findall()
si ottiene la gestione automatica della condizione limite e mantiene il modello semplice.
# re_split.py
import re
text = '''Paragrafo uno
su due righe.
Paragrafo due.
Paragrafo 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 del modello per split()
esprime con più precisione la specifica di marcatura. Due o più ritorni a capo marcano un punto di separazione tra paragrafi nelle stringa in entrata.
$ python3 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.'
Racchiudendo l'espressione tra parentesi per definire un gruppo fa sì che split()
funzioni più come str.partition()
, quindi ritorna i valori del separatore così come le altre parti della stringa.
# re_split_groups.py
import re
text = '''Paragrafo uno
su due righe.
Paragrafo due.
Paragrafo tre.'''
print('Con split:')
for num, para in enumerate(re.split(r'(\n{2,})', text)):
print(num, repr(para))
print()
Il risultato ora ritorna tutti i paragrafi, e anche la sequenza di ritorni a capo che li separa.
$ python3 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:
- re
- La documentazione della libreria standard per questo modulo
- Regular Expression HOWTO
- Introduzione alle espressioni regolari per gli sviluppatori di Python di Andrew Kuchling
- Kodos
- Uno strumento di verifica interattivo delle espressioni regolari di Phil Schwartz.
- pythex
- Uno strumento web per verificare le espressioni regolari di Gabriel Rodriguez. Ispirato da Rubular.
- Espressione Regolare
- La voce di Wikipedia sull'argomento
- locale
- Il modulo locale consente di impostare la configurazione della lingua quando si lavora con testo Unicode.
- unicodedata
- Accesso programmatico al database delle proprietà di Unicode.