Esta é a segunda parte de uma série de artigos educacionais de regex. Ele mostra como lookaheads e referências aninhadas podem ser usadas para corresponder ao idioma não regular a n b n . As referências aninhadas são introduzidas pela primeira vez em: Como este regex encontra números triangulares?
Uma das linguagens não regulares arquetípicas é:
L = { a
nb
n: n > 0 }
Este é o idioma de todas as strings não vazias, consistindo em algum número de a
's seguido por um número igual de b
' s. Exemplos de cordas nesta língua são ab
, aabb
, aaabbb
.
Essa linguagem pode ser demonstrada como não regular pelo lema do bombeamento . É na verdade uma linguagem livre de contexto arquetípica , que pode ser gerada pela gramática livre de contexto S → aSb | ab
.
No entanto, as implementações de regex modernas reconhecem claramente mais do que apenas linguagens regulares. Ou seja, eles não são "regulares" pela definição formal da teoria da linguagem. PCRE e Perl oferecem suporte a regex recursivo e .NET oferece suporte à definição de grupos de balanceamento. Mesmo recursos menos "sofisticados", por exemplo, correspondência de referência anterior, significa que regex não é regular.
Mas quão poderosos são esses recursos "básicos"? Podemos reconhecer L
com Java regex, por exemplo? Talvez possamos combinar lookarounds e referências aninhadas e ter um padrão que funciona com por exemplo, String.matches
para coincidir com cordas como ab
, aabb
, aaabbb
, etc?
Referências
- perlfaq6: Posso usar expressões regulares Perl para corresponder ao texto balanceado?
- MSDN - Elementos de linguagem de expressão regular - Definições de grupo de equilíbrio
- pcre.org - página de manual do PCRE
- regular-expressions.info - Lookarounds e Grouping and Backreferences
java.util.regex.Pattern
Questões vinculadas
fonte
Respostas:
A resposta é, desnecessário dizer, SIM! Você pode certamente escrever um padrão regex Java para corresponder a n b n . Ele usa um lookahead positivo para asserção e uma referência aninhada para "contagem".
Em vez de fornecer imediatamente o padrão, essa resposta guiará os leitores durante o processo de derivá-lo. Várias dicas são fornecidas conforme a solução é construída lentamente. Nesse aspecto, espero que esta resposta contenha muito mais do que apenas outro padrão regex puro. Esperançosamente, os leitores também aprenderão como "pensar em regex" e como colocar várias construções harmoniosamente juntas, para que possam derivar mais padrões por conta própria no futuro.
A linguagem utilizada para desenvolver a solução será o PHP pela sua concisão. O teste final assim que o padrão for finalizado será feito em Java.
Etapa 1: Antecipar a afirmação
Vamos começar com um problema mais simples: queremos casar
a+
no início de uma string, mas apenas se for seguido imediatamente porb+
. Podemos usar^
para ancorar nossa correspondência e, como queremos apenas corresponder oa+
sem ob+
, podemos usar a asserção antecipada(?=…)
.Aqui está nosso padrão com um equipamento de teste simples:
O resultado é ( conforme visto em ideone.com ):
Esta é exatamente a saída que queremos: correspondemos
a+
, apenas se estiver no início da string e apenas se for imediatamente seguida porb+
.Lição : Você pode usar padrões em lookarounds para fazer afirmações.
Etapa 2: captura à frente (e modo de espaçamento livre)
Agora, digamos que, embora não desejemos que o
b+
seja parte da correspondência, queremos capturá- lo de qualquer maneira no grupo 1. Além disso, como prevemos ter um padrão mais complicado, vamos usar ox
modificador para espaçamento livre para que possamos pode tornar nosso regex mais legível.Com base em nosso snippet PHP anterior, agora temos o seguinte padrão:
A saída é agora ( conforme visto em ideone.com ):
Observe que, por exemplo,
aaa|b
é o resultado dejoin
-ing com o que cada grupo capturou'|'
. Nesse caso, o grupo 0 (ou seja, o que o padrão correspondeu) capturouaaa
e o grupo 1 capturoub
.Lição : você pode capturar dentro de um lookaround. Você pode usar espaçamento livre para melhorar a legibilidade.
Etapa 3: Refatorando o lookahead no "loop"
Antes de introduzirmos nosso mecanismo de contagem, precisamos fazer uma modificação em nosso padrão. Atualmente, o lookahead está fora do
+
"loop" de repetição. Isso está bom até agora porque nós apenas queríamos afirmar que existe umb+
seguinte nossoa+
, mas o que realmente queremos fazer é afirmar que para cada uma
que combinamos dentro do "loop", há um correspondenteb
para acompanhar.Não vamos nos preocupar com o mecanismo de contagem por enquanto e apenas fazer a refatoração da seguinte maneira:
a+
para(?: a )+
(observe que(?:…)
é um grupo de não captura)a*
antes que possamos "ver" ob+
, então modifique o padrão de acordoPortanto, agora temos o seguinte:
A saída é a mesma de antes ( conforme visto em ideone.com ), portanto, não há alteração nesse sentido. O importante é que agora estamos fazendo a afirmação a cada iteração do
+
"loop". Com nosso padrão atual, isso não é necessário, mas a seguir faremos o grupo 1 "contar" para nós usando a auto-referência.Lição : Você pode capturar dentro de um grupo sem captura. Lookarounds podem ser repetidos.
Etapa 4: esta é a etapa em que começamos a contar
Aqui está o que vamos fazer: vamos reescrever o grupo 1 de forma que:
+
, quando a primeiraa
for correspondida, ele deve capturarb
a
é correspondida, ela deve capturarbb
bbb
b
para capturar no grupo 1, a afirmação simplesmente falhaPortanto, o grupo 1, que é agora
(b+)
, terá que ser reescrito para algo como(\1 b)
. Ou seja, tentamos "adicionar" ab
ao que o grupo 1 capturou na iteração anterior.Há um pequeno problema aqui, pois esse padrão não tem o "caso base", ou seja, o caso em que pode corresponder sem a auto-referência. Um caso base é necessário porque o grupo 1 começa "não inicializado"; ele ainda não capturou nada (nem mesmo uma string vazia), portanto, uma tentativa de auto-referência sempre falhará.
Há muitas maneiras de contornar isso, mas por enquanto vamos apenas tornar opcional a correspondência de auto-referência , ou seja
\1?
. Isso pode ou não funcionar perfeitamente, mas vamos ver o que faz e, se houver algum problema, cruzaremos a ponte quando chegarmos a esse ponto. Além disso, adicionaremos mais alguns casos de teste enquanto estamos nisso.A saída é agora ( conforme visto em ideone.com ):
A-ha! Parece que estamos realmente perto da solução agora! Conseguimos fazer o grupo 1 "contar" usando a auto-referência! Mas espere ... algo está errado com o segundo e o último caso de teste !! Não há programas suficientes
b
e, de alguma forma, contou errado! Examinaremos por que isso aconteceu na próxima etapa.Lição : Uma maneira de "inicializar" um grupo de autorreferência é tornar opcional a correspondência de autorreferência.
Etapa 4½: Entender o que deu errado
O problema é que, como tornamos a correspondência de auto-referência opcional, o "contador" pode "zerar" de volta para 0 quando não houver
b
o suficiente . Vamos examinar de perto o que acontece em cada iteração de nosso padrãoaaaaabbb
como entrada.A-ha! Em nossa 4ª iteração, ainda podíamos combinar
\1
, mas não poderíamos\1b
! Como permitimos que a correspondência de auto-referência seja opcional com\1?
, o motor retrocede e escolhe a opção "não, obrigado", que nos permite corresponder e capturar apenasb
!Observe, entretanto, que, exceto na primeira iteração, você sempre poderá corresponder apenas à autorreferência
\1
. Isso é óbvio, é claro, uma vez que é o que acabamos de capturar em nossa iteração anterior, e em nossa configuração podemos sempre combiná-lo novamente (por exemplo, se capturamos dabbb
última vez, temos a garantia de que ainda haverábbb
, mas pode ou pode não serbbbb
desta vez).Lição : cuidado com o retrocesso. O mecanismo de regex fará tanto retrocesso quanto você permitir até que o padrão fornecido corresponda. Isso pode afetar o desempenho (ou seja, retrocesso catastrófico ) e / ou correção.
Etapa 5: Autodomínio para o resgate!
A "correção" agora deve ser óbvia: combine a repetição opcional com o quantificador possessivo . Ou seja, em vez de simplesmente
?
usar?+
(lembre-se de que uma repetição que é quantificada como possessiva não retrocede, mesmo que tal "cooperação" possa resultar em uma correspondência do padrão geral).Em termos muito informais, isto é o que
?+
,?
e??
diz:Em nossa configuração,
\1
não estará lá na primeira vez, mas sempre estará lá a qualquer momento depois disso, e sempre queremos corresponder a isso. Assim,\1?+
realizaríamos exatamente o que desejamos.Agora, a saída é ( conforme visto em ideone.com ):
Voilà !!! Problema resolvido!!! Agora estamos contando corretamente, exatamente da maneira que queremos!
Lição : Aprenda a diferença entre repetição gananciosa, relutante e possessiva. Possessão opcional pode ser uma combinação poderosa.
Etapa 6: toques finais
Portanto, o que temos agora é um padrão que corresponde
a
repetidamente e para cadaa
que foi correspondido, há um correspondenteb
capturado no grupo 1. O+
termina quando não há maisa
, ou se a declaração falhou porque não há um correspondenteb
para uma
.Para terminar o trabalho, simplesmente precisamos anexar ao nosso padrão
\1 $
. Esta é agora uma referência anterior ao que o grupo 1 correspondeu, seguido pelo fim da âncora de linha. A âncora garante que não haja nenhum extrab
na string; em outras palavras, que de fato temos a n b n .Este é o padrão finalizado, com casos de teste adicionais, incluindo um com 10.000 caracteres:
Ele encontra 4 jogos
ab
,aabb
,aaabbb
ea um 5000 b 5000 . Leva apenas 0,06s para ser executado em ideone.com .Etapa 7: o teste de Java
Portanto, o padrão funciona em PHP, mas o objetivo final é escrever um padrão que funcione em Java.
O padrão funciona conforme o esperado ( conforme visto em ideone.com ).
E agora chegamos à conclusão ...
É preciso dizer que o
a*
lookahead e, na verdade, o "+
loop principal ", permitem retrocesso. Os leitores são encorajados a confirmar por que isso não é um problema em termos de correção e por que, ao mesmo tempo, tornar ambos possessivos também funcionaria (embora talvez misturar quantificador possessivo obrigatório e não obrigatório no mesmo padrão possa levar a interpretações errôneas).Também deve ser dito que, embora seja legal, há um padrão regex que corresponderá a n b n , essa nem sempre é a "melhor" solução na prática. Uma solução muito melhor é simplesmente corresponder
^(a+)(b+)$
e, em seguida, comparar o comprimento das strings capturadas pelos grupos 1 e 2 na linguagem de programação de hospedagem.Em PHP, pode ser parecido com isto ( como visto em ideone.com ):
O objetivo deste artigo é NÃO convencer os leitores de que regex pode fazer quase tudo; claramente não pode, e mesmo para as coisas que pode fazer, pelo menos a delegação parcial para a linguagem de hospedagem deve ser considerada se levar a uma solução mais simples.
Conforme mencionado no início, embora este artigo seja necessariamente marcado
[regex]
para stackoverflow, talvez seja mais do que isso. Embora certamente haja valor em aprender sobre asserções, referências aninhadas, quantificador possessivo, etc, talvez a maior lição aqui seja o processo criativo pelo qual alguém pode tentar resolver problemas, a determinação e o trabalho árduo que muitas vezes requer quando você está sujeito a várias restrições, a composição sistemática de várias partes para construir uma solução de trabalho, etc.Material bônus! Padrão recursivo PCRE!
Uma vez que trouxemos o PHP, é preciso dizer que PCRE oferece suporte a padrões recursivos e sub-rotinas. Assim, o seguinte padrão funciona para
preg_match
( como visto em ideone.com ):Atualmente o regex do Java não oferece suporte a padrão recursivo.
Ainda mais material de bônus! Correspondendo a n b n c n !!
Então, vimos como combinar a n b n que não é regular, mas ainda livre de contexto, mas também podemos combinar a n b n c n , que nem mesmo é livre de contexto?
A resposta é, claro, SIM! Os leitores são encorajados a tentar resolver isso por conta própria, mas a solução é fornecida abaixo (com implementação em Java em ideone.com ).
fonte
feature
? .... Não tenho certeza se é uma boa ideia. Eu sei qual é o último símbolo, mas não pode ser lido (além de copiar e colar).preg_match()
é um exemplo de PCRE . As expressões regulares do Java parecem ser baseadas em uma versão mais antiga das expressões regulares do Perl . O que significa que regexes PHP são mais poderosas do que a versão em Java. A partir de 2013-02-21 , pcre.txt afirma que corresponde aproximadamente ao Perl 5.12 . Enquanto Perl está atualmente em 5,16, com 5,18 alguns meses fora. (Na verdade, não foi adicionado muito às regexes naquela época)Dado que nenhuma menção foi feita ao PCRE que suporta padrões recursivos, gostaria apenas de apontar o exemplo mais simples e eficiente de PCRE que descreve a linguagem em questão:
fonte
a^n b^n c^n
, no entanto.a
s eb
s sem capturar (e verificar se há a mesma quantidade com recursão), seguido por um regex de captura que avidamente consome todos os as e, em seguida, aplica o recursivo padrão para consumir e verificar que há o mesmo número deb
s ec
s. O regex é:/^(?=(a(?-1)?b)c)a+(b(?-1)?c)$/x
. Crédito para: nikic.github.io/2012/06/15/…Conforme mencionado na pergunta - com o grupo de balanceamento .NET, os padrões do tipo a n b n c n d n ... z n podem ser combinados facilmente como
Por exemplo: http://www.ideone.com/usuOE
Editar:
Também existe um padrão PCRE para a linguagem generalizada com padrão recursivo, mas é necessária uma verificação à frente. Não acho que seja uma tradução direta do que foi dito acima.
Por exemplo: http://www.ideone.com/9gUwF
fonte
a^n b^n
ao .NET regex?" artigo no futuro, mas você é mais do que bem-vindo para escrevê-lo, se quiser. Não estou escrevendo esses artigos apenas para mim; Eu quero encorajar outros a fazerem isso também para ter um bom conteúdo no site.(?!b)
,(?!c)
etc. após os grupos de captura como: regex101.com/r/sdlRTm/2