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.