2.35. Nozioni di base sui pattern

Un’espressione regolare è un piccolo linguaggio per descrivere le stringhe in base alla forma anziché al loro contenuto esatto. Usala quando una stringa corrisponde se e solo se segue un pattern che puoi descrivere ma non enumerare: «una sequenza di cifre seguita da un’unità», «una riga che inizia con ERROR e termina con un numero», «una qualsiasi di queste estensioni di file, in qualsiasi ordine, con prefissi v opzionali».

Ricorri a re solo quando un semplice metodo delle stringhe non basta.

Ciascuno di questi è più veloce, più facile da leggere e più difficile da sbagliare rispetto alla regex equivalente. Usa le regex quando conta la forma della stringa e non la sottostringa esatta.

2.35.1. Le quattro cose che userai

Il modulo re di MicroPython espone quattro elementi:

  • re.compile() – trasforma una stringa di pattern in un pattern object compilato e riutilizzabile.

  • re.match() – prova il pattern all”inizio di una stringa. Il pattern è ancorato alla posizione 0.

  • re.search() – prova il pattern in qualsiasi punto di una stringa. Restituisce la prima corrispondenza.

  • re.sub() – trova ogni corrispondenza e la sostituisce.

Omissioni di rilievo rispetto a CPython: nessun re.findall, nessun re.finditer, nessun re.split a livello di modulo (i pattern compilati hanno invece un metodo split), nessun re.fullmatch, nessuna costante flag come re.IGNORECASE. Dove su CPython ricorreresti a una di queste, costruisci l’equivalente con re.search() in un ciclo.

2.35.2. Un primo pattern

Il pattern r'\d+' corrisponde a una o più cifre:

>>> import re
>>> m = re.search(r'\d+', 'sensor reading 42 ok')
>>> m.group(0)
'42'

Alcune cose da notare:

  • Il pattern è scritto come raw string (r'...'), così che il backslash in \d arrivi a re invece di essere elaborato come escape di stringa Python. Usa sempre le raw string per i pattern regex.

  • re.search() restituisce un match object in caso di successo e None in caso di fallimento. Controlla sempre prima di chiamare match.group().

  • m.group(0) è il testo completo a cui il pattern ha corrisposto. I gruppi 1, 2, … compaiono più avanti, una volta che il pattern contiene parentesi di cattura.

Lo stesso pattern con re.match() restituisce None perché la stringa non inizia con una cifra:

>>> re.match(r'\d+', 'sensor reading 42 ok') is None
True
>>> re.match(r'\d+', '42 readings')
<match num=1>

2.35.3. Le parti di un pattern

La maggior parte dei pattern utili è costruita a partire da un piccolo insieme di parti. Quelle che funzionano in MicroPython:

Caratteri letterali – qualsiasi carattere non speciale corrisponde a se stesso. hello corrisponde a hello.

Caratteri speciali. ^ $ * + ? { } [ ] \ | ( ) hanno tutti i significati riportati di seguito. Per far corrispondere uno di essi letteralmente, usa l’escape con un backslash: \. corrisponde a un punto letterale.

Classi di caratteri – abbreviazioni per insiemi di caratteri comuni:

  • \d – una cifra qualsiasi 0-9

  • \D – un carattere non numerico qualsiasi

  • \s – un carattere di spaziatura qualsiasi (spazio, tabulazione, newline)

  • \S – un carattere non di spaziatura qualsiasi

  • \w – un carattere di «parola» qualsiasi: lettere, cifre, underscore

  • \W – un carattere non di parola qualsiasi

  • . – un carattere qualsiasi tranne il newline

Quantificatori – quante volte deve corrispondere la parte precedente:

  • * – zero o più (greedy)

  • + – uno o più (greedy)

  • ? – zero o uno

  • {n} – esattamente n

  • {m,n} – tra m e n (estremi inclusi)

Combinando: \d{3}-\d{4} corrisponde a tre cifre, un trattino e quattro cifre. \s+ corrisponde a uno o più caratteri di spaziatura. hello.*world corrisponde a hello, qualsiasi cosa (incluso niente) e poi world.

Nota

Greedy significa che il quantificatore consuma quanto più input possibile pur lasciando che il resto del pattern corrisponda. Contro hello x world y world, il .* in hello.*world corrisponde alla sequenza più lunga che lascia comunque un world alla fine: cattura x world y, non il più breve x. Lo stesso vale per + e per la forma con intervallo {m,n}: il motore prende la corrispondenza più lunga possibile e poi torna indietro solo se il resto del pattern fallisce.

2.35.4. Sostituzione

re.sub() trova ogni corrispondenza e la sostituisce con una stringa. La sostituzione può riferirsi ai gruppi catturati tramite \1, \2, … (trattato più avanti insieme al resto della sintassi dei gruppi). Senza gruppi, re.sub è una semplice operazione di cerca-e-sostituisci su una regex:

>>> re.sub(r'\s+', ' ', 'too    many   spaces')
'too many spaces'

>>> re.sub(r'\d+', 'N', 'log 12, log 345, log 6')
'log N, log N, log N'

Il terzo argomento è la stringa su cui operare; il risultato è una nuova stringa con ogni corrispondenza sostituita.

2.35.5. Suddivisione – solo su un pattern compilato

Non esiste re.split a livello di modulo. Per suddividere su una regex, compila prima il pattern e chiama il suo metodo split

>>> sep = re.compile(r'\s*,\s*')
>>> sep.split('a , b,c ,  d')
['a', 'b', 'c', 'd']

Il secondo argomento opzionale limita il numero di suddivisioni:

>>> sep.split('a, b, c, d', 2)
['a', 'b', 'c, d']

2.35.6. Compilare per il riutilizzo

Se lo stesso pattern viene eseguito molte volte – dentro un ciclo o in una funzione critica per le prestazioni – compilalo una volta e riutilizza l’oggetto compilato:

digit_run = re.compile(r'\d+')

def first_number(line):
    m = digit_run.search(line)
    return int(m.group(0)) if m else None

Chiamare pattern.match() e pattern.search() su un oggetto compilato equivale alle funzioni a livello di modulo, ma evita il costo della ricompilazione a ogni chiamata.

2.35.7. Pattern che non corrispondono a nulla

Tre pattern in particolare colgono di sorpresa gli sviluppatori:

  • .* corrisponde alla stringa vuota. re.search(r'.*', s).group(0) restituisce '' con qualsiasi input.

  • Un pattern con un carattere speciale senza escape è un errore di sintassi. re.compile(r'cost: $5') solleva ValueError perché $ significa «fine della stringa». Usa r'cost: \$5'.

  • Il punto . non corrisponde a un newline. Per corrispondere attraverso i newline, scrivi il pattern in modo da gestirli esplicitamente con [\s\S] oppure passa una riga alla volta.

Con queste parti un pattern può corrispondere a quasi ogni porzione di testo a forma fissa. Estrarre i dati strutturati dalla corrispondenza richiede i gruppi di cattura.