O que são expressões regulares Grupos de Equilíbrio?

Eu estava apenas lendo uma pergunta sobre como obter dados dentro de chaves duplas ( esta questão ) e, em seguida, alguém trouxe grupos de equilíbrio. Ainda não sei bem o que são e como usá-los.

Eu li através da definição de grupo de equilíbrio , mas a explicação é difícil de seguir e ainda estou bastante confuso sobre as questões que mencionei.

Alguém poderia simplesmente explicar o que são grupos balanceadores e como eles são úteis?

Até onde sei, os grupos de balanceamento são exclusivos do sabor de regex do .NET.

Aparte: Grupos repetidos

Primeiro, você precisa saber que o .NET é (novamente, até onde eu sei) o único sabor de regex que permite acessar várias capturas de um único grupo de captura (não em referências anteriores, mas após a conclusão da correspondência).

Para ilustrar isso com um exemplo, considere o padrão

(.)+ 

e a string "abcd" .

em todos os outros sabores regex, capturar o grupo 1 irá simplesmente render um resultado: d (note que a partida completa será, evidentemente, abcd como esperado). Isso ocorre porque cada novo uso do grupo de captura substitui a captura anterior.

O .NET, por outro lado, lembra-se de todos eles. E isso acontece em uma pilha. Depois de combinar o regex acima como

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

Vai descobrir que

 m.Groups[1].Captures 

É um CaptureCollection cujos elementos correspondem às quatro capturas

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

onde o número é o índice no CaptureCollection . Então, basicamente, toda vez que o grupo é usado novamente, uma nova captura é colocada na pilha.

Fica mais interessante se estivermos usando grupos de captura nomeados. Porque o .NET permite o uso repetido do mesmo nome, poderíamos escrever um regex como

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

para capturar duas palavras no mesmo grupo. Novamente, toda vez que um grupo com um determinado nome é encontrado, uma captura é colocada em sua pilha. Então, aplicando este regex à input "foo bar" e inspecionando

 m.Groups["word"].Captures 

encontramos duas capturas

 0: "foo" 1: "bar" 

Isso nos permite até mesmo empurrar coisas para uma única pilha de diferentes partes da expressão. Mas ainda assim, esta é apenas a característica do .NET de poder rastrear múltiplas capturas que estão listadas nesta CaptureCollection . Mas eu disse, essa coleção é uma pilha . Então podemos tirar as coisas disso?

Enter: Grupos de Equilíbrio

Acontece que podemos. Se usarmos um grupo como (?< -word>...) , a última captura será exibida da word stack se a subexpressão ... corresponder. Então, se mudarmos nossa expressão anterior para

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

Em seguida, o segundo grupo fará a captura do primeiro grupo e, no final, receberemos uma CaptureCollection vazia. Claro, esse exemplo é bem inútil.

Mas há mais um detalhe na syntax negativa: se a pilha já estiver vazia, o grupo falhará (independentemente do seu subpadrão). Podemos aproveitar esse comportamento para contar níveis de aninhamento – e é daí que vem o grupo de balanceamento de nomes (e onde fica interessante). Digamos que queremos corresponder cadeias que são corretamente entre parênteses. Empurramos cada parêntese de abertura na pilha e criamos uma captura para cada parêntese de fechamento. Se encontrarmos um parêntese de fechamento demais, ele tentará estourar uma pilha vazia e fazer com que o padrão falhe:

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

Então nós temos três alternativas em uma repetição. A primeira alternativa consome tudo o que não é um parêntese. A segunda alternativa corresponde ( s ao empurrá-los para a pilha. A terceira alternativa corresponde ) s enquanto estala os elementos da pilha (se possível!).

Nota: Só para esclarecer, estamos apenas verificando se não há parênteses incomparáveis! Isso significa que a string que não contém parênteses será correspondente, porque ainda é sintaticamente válida (em alguma syntax na qual você precisa que seus parênteses correspondam). Se você quiser garantir pelo menos um conjunto de parênteses, basta adicionar um lookahead (?=.*[(]) Logo após o ^ .

Este padrão não é perfeito (ou inteiramente correto) embora.

Finale: Padrões Condicionais

Há mais uma captura: isso não garante que a pilha esteja vazia no final da string (portanto (foo(bar) seria válido). NET (e muitos outros sabores) tem mais uma construção que nos ajuda aqui padrões condicionais.A syntax geral é

 (?(condition)truePattern|falsePattern) 

onde o falsePattern é opcional – se for omitido, o caso falso sempre será igual. A condição pode ser um padrão ou o nome de um grupo de captura. Vou me concentrar no último caso aqui. Se for o nome de um grupo de captura, truePattern será usado se, e somente se, a pilha de captura para esse grupo específico não estiver vazia. Ou seja, um padrão condicional como (?(name)yes|no) lê “se o name coincidiu e capturou algo (que ainda está na pilha), use o padrão yes caso contrário, use padrão no “.

Então, no final do nosso padrão acima, poderíamos adicionar algo como (?(Open)failPattern) que faz com que todo o padrão falhe, se o Open stack não estiver vazio. A coisa mais simples para fazer com que o padrão falhe incondicionalmente é (?!) (Um lookahead negativo vazio). Então nós temos nosso padrão final:

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

Observe que essa syntax condicional não tem nada a ver com grupos de balanceamento, mas é necessário aproveitar todo o seu poder.

A partir daqui, o céu é o limite. Muitos usos muito sofisticados são possíveis e existem algumas armadilhas quando usados ​​em combinação com outros resources do .NET-Regex, como lookbehinds de comprimento variável ( que eu mesmo tive que aprender da maneira mais difícil ). A questão principal, no entanto, é sempre: o seu código ainda pode ser mantido ao usar esses resources? Você precisa documentá-lo muito bem e ter certeza de que todos que trabalham nele também estão cientes desses resources. Caso contrário, você pode estar melhor, apenas percorrendo a string manualmente caractere por caractere e contando os níveis de aninhamento em um inteiro.

Adendo: O que há com a syntax (?...) ?

Créditos desta parte vão para Kobi (veja a resposta abaixo para mais detalhes).

Agora, com todos os itens acima, podemos validar que uma string está entre parênteses correta. Mas seria muito mais útil se pudéssemos obter capturas (aninhadas) para todo o conteúdo desses parênteses. Naturalmente, poderíamos lembrar de abrir e fechar parênteses em uma pilha de captura separada que não é esvaziada e, em seguida, fazer alguma extração de substring com base em suas posições em uma etapa separada.

Mas o .NET fornece mais um recurso de conveniência aqui: se usarmos (?subPattern) , não apenas uma captura surgirá da pilha B , mas também tudo entre essa captura estourada de B e esse grupo atual será colocado na pilha A Então, se usarmos um grupo como esse para os parênteses de fechamento, enquanto estouramos os níveis de aninhamento de nossa pilha, também podemos empurrar o conteúdo do par para outra pilha:

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

Kobi forneceu esta demonstração ao vivo em sua resposta

Então, tomando todas essas coisas juntas, podemos:

  • Lembre-se arbitrariamente de muitas capturas
  • Validar estruturas aninhadas
  • Capture cada nível de aninhamento

Tudo em uma única expressão regular. Se isso não é excitante …;)

Alguns resources que achei úteis quando aprendi sobre eles:

Apenas uma pequena adição à excelente resposta de M. Buettner:

Qual é o problema com a syntax (?) ?

(?x) é sutilmente diferente de (?< -A>(?x)) . Eles resultam no mesmo stream de controle * , mas capturam diferentemente.
Por exemplo, vamos ver um padrão para chaves balanceadas:

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

No final da partida, temos uma string balanceada, mas isso é tudo o que temos – não sabemos onde estão as chaves porque a pilha B está vazia. O trabalho duro que o motor fez por nós desapareceu.
( exemplo no Regex Storm )

(?x) é a solução para esse problema. Como? Não captura x em $A : captura o conteúdo entre a captura anterior de B e a posição atual.

Vamos usá-lo em nosso padrão:

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

Isso iria capturar em $Content as strings entre as chaves (e suas posições), para cada par ao longo do caminho.
Para a string {1 2 {3} {4 5 {6}} 7} haveria quatro capturas: 3 , 6 , 4 5 {6} e 1 2 {3} {4 5 {6}} 7 – muito melhor que nada ou } } } } .
( exemplo – clique na guia da table e veja ${Content} , capturas )

Na verdade, ele pode ser usado sem balanceamento: (?).(.(?).) Captura os dois primeiros caracteres, embora estejam separados por grupos.
(um lookahead é mais comumente usado aqui, mas nem sempre é escalável: ele pode duplicar sua lógica.)

(?) é uma característica forte – lhe dá controle exato sobre suas capturas. Tenha isso em mente quando você está tentando tirar mais proveito do seu padrão.