Per quanto ne so, i gruppi di bilanciamento sono unici per il gusto regex di .NET.
A parte: gruppi ripetuti
Innanzitutto, devi sapere che .NET è (di nuovo, per quanto ne so) l'unico sapore di regex che ti consente di accedere a più acquisizioni di un singolo gruppo di acquisizione (non nei backreferences ma dopo che la corrispondenza è stata completata).
Per illustrare questo con un esempio, considera il modello
(.)+
e la corda "abcd"
.
in tutte le altre versioni di espressioni regolari, la cattura del gruppo 1
produrrà semplicemente un risultato: d
(nota, la corrispondenza completa sarà ovviamente abcd
quella prevista). Questo perché ogni nuovo utilizzo del gruppo di acquisizione sovrascrive l'acquisizione precedente.
.NET invece li ricorda tutti. E lo fa in una pila. Dopo aver abbinato la regex sopra come
Match m = new Regex(@"(.)+").Match("abcd");
lo troverai
m.Groups[1].Captures
È un i CaptureCollection
cui elementi corrispondono alle quattro acquisizioni
0: "a"
1: "b"
2: "c"
3: "d"
dove il numero è l'indice nel file CaptureCollection
. Quindi in pratica ogni volta che il gruppo viene utilizzato di nuovo, una nuova cattura viene inserita nella pila.
Diventa più interessante se usiamo gruppi di cattura con nome. Poiché .NET consente l'uso ripetuto dello stesso nome, potremmo scrivere un'espressione regolare come
(?<word>\w+)\W+(?<word>\w+)
per catturare due parole nello stesso gruppo. Di nuovo, ogni volta che si incontra un gruppo con un certo nome, una cattura viene inserita nel suo stack. Quindi applicare questa regex all'input "foo bar"
e ispezionare
m.Groups["word"].Captures
troviamo due acquisizioni
0: "foo"
1: "bar"
Questo ci consente anche di inserire elementi su un unico stack da parti diverse dell'espressione. Tuttavia, questa è solo la caratteristica di .NET di essere in grado di tenere traccia di più acquisizioni elencate in questo CaptureCollection
. Ma ho detto, questa collezione è una pila . Quindi possiamo estrarre cose da esso?
Immettere: gruppi di bilanciamento
Si scopre che possiamo. Se usiamo un gruppo simile (?<-word>...)
, l'ultima cattura viene estratta dallo stack word
se la sottoespressione ...
corrisponde. Quindi, se cambiamo la nostra espressione precedente in
(?<word>\w+)\W+(?<-word>\w+)
Quindi il secondo gruppo farà apparire la cattura del primo gruppo e CaptureCollection
alla fine riceveremo un vuoto . Ovviamente questo esempio è abbastanza inutile.
Ma c'è un altro dettaglio nella sintassi meno: se lo stack è già vuoto, il gruppo fallisce (indipendentemente dal suo schema secondario). Possiamo sfruttare questo comportamento per contare i livelli di nidificazione - ed è da qui che proviene il nome gruppo di bilanciamento (e dove diventa interessante). Supponiamo di voler abbinare le stringhe correttamente tra parentesi. Inseriamo ogni parentesi di apertura sullo stack e inseriamo un'acquisizione per ogni parentesi di chiusura. Se incontriamo una parentesi di chiusura di troppo, proverà a far apparire uno stack vuoto e farà fallire il pattern:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*$
Quindi abbiamo tre alternative in una ripetizione. La prima alternativa consuma tutto ciò che non è una parentesi. La seconda alternativa corrisponde a (
s mentre le spinge in pila. La terza alternativa corrisponde a )
s mentre estrae elementi dalla pila (se possibile!).
Nota: solo per chiarire, stiamo solo controllando che non ci siano parentesi senza corrispondenza! Ciò significa che non stringa contenente parentesi a tutti sarà fiammifero, perché sono ancora sintatticamente valido (in qualche sintassi in cui è necessario il tuo parentesi a partita). Se vuoi assicurarti almeno una serie di parentesi, aggiungi semplicemente un lookahead (?=.*[(])
subito dopo il ^
.
Questo modello non è perfetto (o del tutto corretto) però.
Finale: modelli condizionali
C'è un altro problema: questo non garantisce che lo stack sia vuoto alla fine della stringa (quindi (foo(bar)
sarebbe valido). .NET (e molte altre versioni) hanno un altro costrutto che ci aiuta qui: i modelli condizionali. La sintassi generale è
(?(condition)truePattern|falsePattern)
dove falsePattern
è facoltativo: se viene omesso, il caso falso corrisponderà sempre. La condizione può essere un modello o il nome di un gruppo di acquisizione. Mi concentrerò su quest'ultimo caso qui. Se è il nome di un gruppo di acquisizione, truePattern
viene utilizzato se e solo se lo stack di acquisizione per quel particolare gruppo non è vuoto. Cioè, un modello condizionale come (?(name)yes|no)
legge "se name
ha trovato qualcosa (che è ancora in pila), usa pattern yes
altrimenti usa pattern no
".
Quindi alla fine del nostro pattern sopra potremmo aggiungere qualcosa del genere (?(Open)failPattern)
che fa fallire l'intero pattern, se lo Open
stack non è vuoto. La cosa più semplice per far fallire incondizionatamente il pattern è (?!)
(un lookahead negativo vuoto). Quindi abbiamo il nostro modello finale:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*(?(Open)(?!))$
Si noti che questa sintassi condizionale non ha nulla a che fare con il bilanciamento dei gruppi, ma è necessario sfruttarne la piena potenza.
Da qui il cielo è il limite. Sono possibili molti usi molto sofisticati e ci sono alcuni trucchi se usati in combinazione con altre funzionalità .NET-Regex come lookbehind a lunghezza variabile ( che ho dovuto imparare a mie spese ). La domanda principale, tuttavia, è sempre: il tuo codice è ancora mantenibile quando si utilizzano queste funzionalità? Devi documentarlo molto bene e assicurarti che anche tutti coloro che ci lavorano siano a conoscenza di queste funzionalità. Altrimenti potresti stare meglio, semplicemente percorrendo la stringa manualmente carattere per carattere e contando i livelli di nidificazione in un numero intero.
Addendum: che cos'è la (?<A-B>...)
sintassi?
I crediti per questa parte vanno a Kobi (vedi la sua risposta sotto per maggiori dettagli).
Ora, con tutto quanto sopra, possiamo confermare che una stringa è correttamente tra parentesi. Ma sarebbe molto più utile, se potessimo effettivamente ottenere acquisizioni (annidate) per tutti i contenuti di quelle parentesi. Ovviamente, potremmo ricordare l'apertura e la chiusura delle parentesi in uno stack di acquisizione separato che non viene svuotato, e quindi eseguire un'estrazione di sottostringa in base alle loro posizioni in un passaggio separato.
Ma .NET fornisce un'altra caratteristica di praticità qui: se usiamo (?<A-B>subPattern)
, non solo un'acquisizione viene estratta dallo stack B
, ma anche tutto ciò che si trova tra quell'acquisizione spuntata di B
e questo gruppo corrente viene inserito nello stack A
. Quindi, se usiamo un gruppo come questo per le parentesi di chiusura, mentre estraiamo i livelli di nidificazione dal nostro stack, possiamo anche spingere il contenuto della coppia su un altro stack:
^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$
Kobi ha fornito questo Live-Demo nella sua risposta
Quindi, prendendo tutte queste cose insieme possiamo:
- Ricorda arbitrariamente molte acquisizioni
- Convalida le strutture nidificate
- Cattura ogni livello di nidificazione
Tutto in un'unica espressione regolare. Se non è eccitante ...;)
Alcune risorse che ho trovato utili quando le ho apprese per la prima volta: