Cosa sono i gruppi di bilanciamento delle espressioni regolari?

Stavo solo leggendo una domanda su come ottenere i dati all’interno di doppie parentesi graffe ( questa domanda ), e poi qualcuno ha sollevato gruppi di bilanciamento. Non sono ancora abbastanza sicuro di cosa siano e come usarli.

Ho letto la definizione del gruppo di bilanciamento , ma la spiegazione è difficile da seguire, e sono ancora abbastanza confuso sulle domande che ho menzionato.

Qualcuno potrebbe semplicemente spiegare cosa sono i gruppi di bilanciamento e come sono utili?

Per quanto ne so, i gruppi di bilanciamento sono unici per il gusto regex di .NET.

A parte: gruppi ripetuti

Innanzitutto, è necessario sapere che .NET è (di nuovo, per quanto ne so) l’unico sapore regex che consente di accedere a più acquisizioni di un singolo gruppo di cattura (non in backreferences ma dopo che la corrispondenza è stata completata).

Per illustrare questo con un esempio, considerare il modello

(.)+ 

e la stringa "abcd" .

in tutti gli altri sapori regex, catturare il gruppo 1 produrrà semplicemente un risultato: d (nota, l’intero match sarà ovviamente abcd come previsto). Questo perché ogni nuovo utilizzo del gruppo di acquisizione sovrascrive la cattura precedente.

.NET d’altra parte li ricorda tutti. E lo fa in una pila. Dopo aver abbinato la regex di cui sopra

 Match m = new Regex(@"(.)+").Match("abcd"); 

lo troverai

 m.Groups[1].Captures 

È una CaptureCollection cui elementi corrispondono alle quattro acquisizioni

 0: "a" 1: "b" 2: "c" 3: "d" 

dove il numero è l’indice in CaptureCollection . Quindi praticamente ogni volta che il gruppo viene usato di nuovo, una nuova cattura viene spinta in pila.

Diventa più interessante se stiamo usando gruppi di cattura denominati. Perché .NET consente l’uso ripetuto dello stesso nome potremmo scrivere una regex come

 (?\w+)\W+(?\w+) 

per catturare due parole nello stesso gruppo. Di nuovo, ogni volta che si incontra un gruppo con un certo nome, un’acquisizione viene spinta nella sua pila. Quindi applicando questa regex all’input "foo bar" e ispezionando

 m.Groups["word"].Captures 

troviamo due catture

 0: "foo" 1: "bar" 

Questo ci consente persino di spostare le cose su una singola pila da diverse parti dell’espressione. Ma ancora, questa è solo la caratteristica di .NET di essere in grado di monitorare più acquisizioni che sono elencate in questo CaptureCollection . Ma ho detto, questa collezione è una pila . Quindi possiamo spuntare qualcosa da esso?

Immettere: Gruppi di bilanciamento

Si scopre che possiamo. Se usiamo un gruppo come (?<-word>...) , l’ultima cattura viene estratta dalla word stack se la sottoespressione ... corrisponde. Quindi se cambiamo la nostra espressione precedente a

 (?\w+)\W+(?<-word>\w+) 

Quindi il secondo gruppo farà scoppiare l’acquisizione del primo gruppo e alla fine riceverai una CaptureCollection vuota. Naturalmente, questo esempio è abbastanza inutile.

Ma c’è un ulteriore dettaglio alla syntax meno: se lo stack è già vuoto, il gruppo fallisce (indipendentemente dal suo subpattern). Possiamo sfruttare questo comportamento per contare i livelli di nidificazione – ed è qui che il gruppo di bilanciamento del nome proviene (e dove diventa interessante). Diciamo che vogliamo abbinare le stringhe che sono correttamente tra parentesi. Spingiamo ogni parentesi aperta sullo stack e inseriamo una cattura per ogni parentesi chiusa. Se incontriamo una parentesi di chiusura troppe, cercherà di far apparire una pila vuota e far fallire il modello:

 ^(?:[^()]|(?[(])|(?<-Open>[)]))*$ 

Quindi abbiamo tre alternative in una ripetizione. La prima alternativa consuma tutto ciò che non è una parentesi. La seconda alternativa combina ( se li spinge nello stack.) La terza alternativa corrisponde a s mentre fa scoppiare gli elementi dallo stack (se ansible!).

Nota: giusto per chiarire, stiamo solo controllando che non ci siano parentesi non corrispondenti! Ciò significa che la stringa che non contiene parentesi corrisponderà, perché sono ancora sintatticamente valide (in alcune syntax in cui è necessario che le parentesi corrispondano). Se si desidera garantire almeno un set di parentesi, aggiungere semplicemente un lookahead (?=.*[(]) Subito dopo ^ .

Questo modello non è perfetto (o del tutto corretto).

Finale: modelli condizionali

C’è un’altra cattura: ciò non garantisce che lo stack sia vuoto alla fine della stringa (quindi (foo(bar) sarebbe valido). NET (e molti altri sapori) hanno un altro costrutto che ci aiuta : schemi condizionali La syntax generale è

 (?(condition)truePattern|falsePattern) 

dove il falsePattern è facoltativo – se viene omesso, la falsa falsePattern corrisponde sempre. La condizione può essere uno schema o il nome di un gruppo che cattura. Mi concentrerò su quest’ultimo caso qui. Se si tratta del nome di un gruppo di acquisizione, viene utilizzato truePattern se e solo se lo stack di acquisizione per quel particolare gruppo non è vuoto. Cioè, un modello condizionale come (?(name)yes|no) legge “se il name ha trovato e catturato qualcosa (che è ancora in pila), usa lo schema yes altrimenti usa il pattern no “.

Quindi alla fine del nostro pattern sopra possiamo aggiungere qualcosa come (?(Open)failPattern) che causa il fallimento dell’intero pattern, se Open -stack non è vuoto. La cosa più semplice per rendere il modello incondizionatamente fallito è (?!) (Un lookahead negativo vuoto). Quindi abbiamo il nostro modello finale:

 ^(?:[^()]|(?[(])|(?<-Open>[)]))*(?(Open)(?!))$ 

Si noti che questa syntax condizionale non ha nulla a che vedere con i gruppi di bilanciamento, ma è necessario sfruttarne appieno il potere.

Da qui, il cielo è il limite. Molti usi molto sofisticati sono possibili e ci sono alcuni trucchi quando usati in combinazione con altre funzionalità di .NET-Regex come le guardie di lunghezza variabile ( che ho dovuto imparare da solo ). La domanda principale però è sempre: il tuo codice è ancora mantenibile quando si utilizzano queste funzionalità? È necessario documentarlo molto bene e accertarsi che tutti coloro che ci lavorano siano a conoscenza di queste funzionalità. Altrimenti potresti stare meglio, basta camminare la stringa manualmente carattere per carattere e contare i livelli di nidificazione in un numero intero.

Addendum: che cos’è la syntax (?...) ?

I crediti per questa parte vanno a Kobi (vedi la sua risposta sotto per maggiori dettagli).

Ora con tutto quanto sopra, possiamo verificare che una stringa sia correttamente tra parentesi. Ma sarebbe molto più utile se potessimo effettivamente catturare (nidificati) per tutti quei contenuti delle parentesi. Ovviamente, potremmo ricordare di aprire e chiudere le parentesi in uno stack di acquisizione separato che non è stato svuotato, e quindi eseguire un’estrazione di sottostringa in base alle loro posizioni in un passaggio separato.

Ma .NET offre qui un’altra caratteristica di comodità: se usiamo (?subPattern) , non solo viene catturata una cattura dallo stack B , ma anche tutto ciò che intercorre tra l’acquisizione di B catturata e questo gruppo corrente viene inserito nello stack A Quindi, se usiamo un gruppo come questo per le parentesi di chiusura, mentre facciamo scoppiare i livelli di nidificazione dal nostro stack, possiamo anche spingere il contenuto della coppia su un altro stack:

 ^(?:[^()]|(?[(])|(?[)]))*(?(Open)(?!))$ 

Kobi ha fornito questa Live-Demo nella sua risposta

Quindi, prendendo tutte queste cose insieme possiamo:

  • Ricorda arbitrariamente molte catture
  • Convalidare strutture annidate
  • Cattura ogni livello di nidificazione

Tutto in una singola espressione regolare. Se non è eccitante …;)

Alcune risorse che ho trovato utili quando ho saputo prima di loro:

Solo una piccola aggiunta all’eccellente risposta di M. Buettner:

Qual è l’accordo con la syntax (?) ?

(?x) è sottilmente diverso da (?<-A>(?x)) . Risultano nello stesso stream di controllo * , ma si catturano in modo diverso.
Ad esempio, esaminiamo un modello per parentesi graffe bilanciate:

 (?:[^{}]|(?{)|(?<-B>}))+(?(B)(?!)) 

Alla fine della partita abbiamo una stringa bilanciata, ma questo è tutto ciò che abbiamo – non sappiamo dove siano le parentesi perché lo stack B è vuoto. Il duro lavoro che il motore ha fatto per noi è finito.
( esempio su Regex Storm )

(?x) è la soluzione per questo problema. Come? Non cattura x in $A : cattura il contenuto tra l’acquisizione precedente di B e la posizione corrente.

Usiamolo nel nostro modello:

 (?:[^{}]|(?{)|(?}))+(?(Open)(?!)) 

Questo catturerebbe in $Content le stringhe tra le parentesi graffe (e le loro posizioni), per ogni coppia lungo la strada.
Per la stringa {1 2 {3} {4 5 {6}} 7} ci saranno quattro catture: 3 , 6 , 4 5 {6} e 1 2 {3} {4 5 {6}} 7 – molto meglio di niente o } } } } .
( esempio: fai clic sulla scheda table e guarda ${Content} , acquisisce )

Infatti, può essere utilizzato senza bilanciamento: (?).(.(?).) Acquisisce i primi due caratteri, anche se sono separati da gruppi.
(un lookahead è più comunemente usato qui, ma non sempre scala: potrebbe duplicare la tua logica).

(?) è una caratteristica forte: ti dà il controllo esatto sulle tue catture. Tienilo a mente quando stai cercando di ottenere di più dai tuoi schemi.