Cosa sono i gruppi di bilanciamento delle espressioni regolari?


91

Stavo solo leggendo una domanda su come ottenere dati all'interno di doppie parentesi graffe ( questa domanda ), e poi qualcuno ha sollevato gruppi di bilanciamento. Non sono ancora del tutto 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?


Mi chiedo su quanti regex engien questo sia effettivamente supportato.
Mike de Klerk

2
@MikedeKlerk È supportato almeno nel motore .NET Regex.
ÈNotALie.

Risposte:


173

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 1produrrà semplicemente un risultato: d(nota, la corrispondenza completa sarà ovviamente abcdquella 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 CaptureCollectioncui 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 wordse 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 CaptureCollectionalla 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, truePatternviene 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 nameha trovato qualcosa (che è ancora in pila), usa pattern yesaltrimenti 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 Openstack 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 Be 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:


6
Questa risposta è stata aggiunta alle domande frequenti sulle espressioni regolari di overflow dello stack , in "Advanced Regex-Fu".
aliteralmind

39

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

Qual è il problema con la (?<A-B>)sintassi?

(?<A-B>x)è leggermente diverso da (?<-A>(?<B>x)). Conseguono lo stesso flusso di controllo * , ma acquisiscono in modo diverso.
Ad esempio, diamo un'occhiata a uno schema per parentesi graffe bilanciate:

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

Alla fine della partita abbiamo una stringa bilanciata, ma è tutto ciò che abbiamo - non sappiamo dove siano le parentesi perché lo Bstack è vuoto. Il duro lavoro svolto dal motore per noi è svanito.
( esempio su Regex Storm )

(?<A-B>x)è la soluzione a quel problema. Come? Essa non cattura xin $A: cattura il contenuto tra l'acquisizione precedente Be la posizione corrente.

Usiamolo nel nostro schema:

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

Questo catturerebbe $Contentle corde tra le parentesi graffe (e le loro posizioni), per ogni coppia lungo il percorso.
Per la stringa {1 2 {3} {4 5 {6}} 7}ci sarebbe quattro acquisizioni: 3, 6, 4 5 {6}, e 1 2 {3} {4 5 {6}} 7- molto meglio di niente o } } } }.
( esempio: fai clic sulla tablescheda e guarda ${Content}, acquisisce )

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

(?<A-B>)è una caratteristica forte: ti dà il controllo esatto sulle tue acquisizioni. Tienilo a mente quando cerchi di ottenere di più dal tuo schema.


@ FYI, continuando la discussione dalla domanda che non ti è piaciuta in una nuova risposta su questa. :)
zx81

Sto cercando di trovare un modo per eseguire il controllo regex delle parentesi graffe bilanciate con la fuga delle parentesi graffe all'interno delle stringhe. Ad esempio, passerà il codice seguente: public class Foo {private const char BAR = '{'; stringa privata _qux = "{{{"; } Qualcuno l'ha fatto?
Mr Anderson

@MrAnderson - Devi solo aggiungere |'[^']*'nel posto giusto: esempio . Se sono necessari anche caratteri di escape, è disponibile un esempio qui: (Regex per la corrispondenza di valori letterali stringa C #) [ stackoverflow.com/a/4953878/7586] .
Kobi
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.