2.36. Gruppi e ancore#

Un pattern può fare di più che dire «questa stringa corrisponde»: può scomporre le parti trovate e passarne ognuna all’applicazione tramite un nome. Le parentesi attorno a una porzione di un pattern la rendono un gruppo di cattura; il match object espone quindi ogni gruppo come una sottostringa separata.

2.36.1. Gruppi di cattura#

Racchiudi una qualsiasi porzione di un pattern tra (...) per catturare ciò che ha trovato:

>>> import re
>>> m = re.search(r'temp (\d+) at (\d+)s', 'temp 42 at 137s ok')
>>> m.group(0)
'temp 42 at 137s'
>>> m.group(1)
'42'
>>> m.group(2)
'137'
  • Il gruppo 0 è sempre l’intera corrispondenza.

  • I gruppi 1, 2, … sono le sottostringhe catturate, numerate da sinistra a destra in base alla loro parentesi di apertura.

  • Chiamare match.group() con un indice oltre l’ultimo gruppo solleva IndexError.

Un pattern comune è «trova una struttura nota, cattura le parti variabili come interi»:

def parse_temp(line):
    m = re.search(r'temp (\d+) at (\d+)s', line)
    if not m:
        return None
    return int(m.group(1)), int(m.group(2))

2.36.2. Gruppi senza cattura#

Le parentesi raggruppano anche una sotto-espressione, in modo che un quantificatore possa applicarsi all’intero gruppo. È questo l’unico scopo del raggruppamento in r'(ab)+': «uno o più ab«. Il fatto che ab compaia come gruppo 1 è un effetto collaterale.

Per raggruppare senza catturare, usa (?:...)

>>> re.search(r'(?:ab)+', 'xababy').group(0)
'abab'

I gruppi senza cattura mantengono ordinata la numerazione dei gruppi quando un pattern usa il raggruppamento per la struttura ma non ha bisogno di estrarre ogni singola parte.

2.36.3. Ancore#

Le ancore non corrispondono a un carattere: corrispondono a una posizione.

  • ^ – inizio della stringa.

  • $ – fine della stringa.

Le ancore sono ciò che fa comportare diversamente re.match() e re.search(). re.match(p, s) equivale a re.search('^' + p, s): forza il pattern a iniziare alla posizione 0. Aggiungere $ alla fine di un pattern fa quindi sì che il pattern corrisponda all”intera stringa e a nient’altro:

>>> re.search(r'^\d+$', '12345')
<match num=1>
>>> re.search(r'^\d+$', '12345 ok') is None
True

In MicroPython re, ^ e $ indicano sempre l’inizio e la fine dell”intera stringa passata a re.search(). Non esiste un flag re.MULTILINE per farli corrispondere a ogni newline interno, e $ non corrisponde nemmeno alla posizione prima di un \n finale: deve essere la fine assoluta dell’input. Per ottenere un comportamento per riga, dividi prima l’input sui newline ed esegui il pattern su ogni riga.

2.36.4. Insiemi di caratteri#

Le parentesi quadre definiscono un insieme esplicito di caratteri. La corrispondenza consuma esattamente un carattere dell’insieme.

  • [abc] – uno tra a, b, c.

  • [a-z] – un carattere nell’intervallo a-z (estremi inclusi).

  • [a-zA-Z0-9] – lettere o cifre. Tre intervalli combinati.

  • [^abc]non uno tra a, b, c. Il ^ nega solo quando è il primo carattere all’interno delle parentesi.

Esempi:

>>> re.search(r'[A-F0-9]{6}', 'colour #1a2b3c rest').group(0)
'1A2B3C'
>>> re.search(r'[A-F0-9]{6}', 'colour #1a2b3c rest') is None
True

In pratica la prima chiamata restituisce None perché il testo letterale è in minuscolo. MicroPython re non ha alcun flag re.IGNORECASE: per una corrispondenza che ignori le maiuscole/minuscole, inserisci entrambe le forme nell’insieme:

>>> re.search(r'[A-Fa-f0-9]{6}', 'colour #1a2b3c rest').group(0)
'1a2b3c'

Le scorciatoie di classe (\d, \s, \w e le loro forme negate) possono essere usate anche dentro [...]: [\w-] significa «caratteri di parola o un trattino letterale».

2.36.5. Quantificatori greedy e lazy#

I quantificatori *, +, ? e {m,n} sono greedy per impostazione predefinita: corrispondono a quanti più caratteri possibile pur lasciando che il resto del pattern continui a corrispondere. Spesso è esattamente ciò che si vuole; a volte no:

>>> re.search(r'<(.+)>', 'a <b> <c> d').group(1)
'b> <c'

Il .+ greedy ha catturato fino all’ultimo >. Aggiungere ? rende il quantificatore lazy: corrisponde a quanto meno possibile:

>>> re.search(r'<(.+?)>', 'a <b> <c> d').group(1)
'b'

La forma lazy si ferma al primo >. I quantificatori lazy ricorrono di continuo quando si estraggono delimitatori bilanciati da una stringa.

2.36.6. Backreference nella sostituzione#

re.sub() può riferirsi ai gruppi catturati nella stringa di sostituzione tramite \1, \2, … La sostituzione riscrive ogni corrispondenza usando le parti catturate:

>>> re.sub(r'(\d+)\.(\d+)', r'\2.\1', 'swap 12.34 and 5.6')
'swap 34.12 and 6.5'

Ogni corrispondenza cattura due numeri e la sostituzione li scambia. \g<1> è una sintassi alternativa per la stessa cosa, utile quando il carattere successivo nella sostituzione è una cifra (r'\g<1>0' per aggiungere uno zero letterale al gruppo 1 invece di leggere «gruppo 10»).

2.36.7. Cosa non è disponibile#

Un promemoria di ciò che MicroPython re non supporta, nel caso un pattern proveniente da CPython finisca qui e ti sorprenda:

  • Lookahead (?=...) e lookbehind (?<=...) – non implementati.

  • Gruppi con nome (?P<name>...) e backreference con nome (?P=name) – non implementati.

  • Costanti flag come re.IGNORECASE, re.MULTILINE, re.DOTALL – non rispettate. Costruisci tu stesso l’insieme insensibile alle maiuscole o pre-suddividi l’input.

  • I metodi match.groups(), match.span(), match.start() e match.end() sono vincolati a un livello ROM che nessuna scheda OpenMV in commercio abilita. Il codice che vi fa affidamento non funzionerà sulla cam.

Con pattern, gruppi e ancore, il set di strumenti per le regex sulla cam è abbastanza piccolo da imparare in una sola seduta e abbastanza ricco da fare tutto, tranne il parsing sensibile al contesto.