regex · processamento de texto · segurança
O que é exatamente uma regex?
Uma expressão regular, ou regex, é uma pequena linguagem que descreve a forma do texto em vez do seu conteúdo exato. Em vez de perguntar «esta cadeia é igual a abc?», pergunta «esta cadeia tem a forma de três letras, um hífen e quatro dígitos?».
A ideia é toda esta. O resto é notação.
A notação, numa passagem
Quase tudo o que vai ler constrói-se com um punhado de peças:
.qualquer carácter,\dum dígito,\wum carácter de palavra,\sum espaço*zero ou mais,+um ou mais,?zero ou um,5entre dois e cinco[abc]qualquer um destes,[^abc]tudo menos estes,[a-z]um intervalo^o início da cadeia,$o seu fim(...)um grupo que pode repetir ou capturar,|um ou outro
Assim ^\d3-\d4$ lê-se: desde o início, três dígitos, um hífen, quatro dígitos, depois o fim. Nada mais.
Porque o mesmo padrão se comporta de outra forma em duas linguagens
Não existe uma regex. Existe uma família de dialetos que concordam no básico e divergem para além disso: POSIX, PCRE, a variante integrada no JavaScript, a do Python, a do Go. O lookbehind, os grupos nomeados, os escapes de propriedades Unicode e até o que o \d considera um dígito são pontos onde se separam.
A consequência prática é que um padrão copiado de uma resposta escrita para outra linguagem pode fazer silenciosamente outra coisa na sua. Teste-o onde vai correr, não onde o encontrou. É para isso que serve o testador de regex deste site.
A falha que vale a pena conhecer: backtracking catastrófico
Eis o que transforma um utilitário de correspondência de texto num problema de operação.
A maioria dos motores de regex das linguagens correntes funciona por retrocesso. Quando um padrão pode corresponder de várias maneiras, o motor tenta uma divisão e, se o resto falhar, volta atrás e tenta outra. Para padrões vulgares isso é barato. Para alguns não é.
A forma perigosa é uma repetição dentro de uma repetição, em que ambas podem repartir entre si o mesmo texto de muitas maneiras. O exemplo dos manuais é (a+)+$. Perante uma longa série de a seguida de um carácter que não pode corresponder, o motor tem de tentar todas as formas possíveis de repartir esses a entre a repetição interna e a externa antes de poder concluir que falha. O número de combinações cresce exponencialmente com o comprimento da entrada.
Vinte caracteres podem ser instantâneos. Trinta podem demorar um segundo. Quarenta podem não terminar enquanto ainda lá trabalhar. E como isto só acontece com entradas que quase correspondem, nunca aparece nos testes que escreveu com entradas que correspondem.
Quando essa regex corre sobre entrada fornecida pelo utilizador num servidor, o caso exponencial está ao alcance de qualquer um que consiga enviar um pedido. É a classe de negação de serviço conhecida como ReDoS, catalogada pela OWASP como negação de serviço por expressão regular.
Como ficar de fora
Desconfie dos quantificadores aninhados. Um + ou um * aplicado a um grupo que já contém um é a forma a procurar. Muitas vezes pode ser achatada em algo com uma única maneira inequívoca de corresponder.
Ancore e seja específico. ^, $ e classes de caracteres precisas reduzem o número de divisões que o motor tem de considerar. [^"]* é normalmente melhor do que .*.
Não passe entrada de utilizador por uma regex escrita à mão com ligeireza. Para emails, URL e datas, um analisador ou uma biblioteca testada ganha a um padrão engenhoso, e falha de um modo legível.
Conheça o seu motor. Alguns, como o RE2 em Go, assentam noutro algoritmo e não retrocedem de todo. Abdicam de funcionalidades como as retro-referências em troca de uma correspondência garantida em tempo linear. Se uma regex tiver de correr sobre entrada hostil, essa troca é muitas vezes a certa.
Em resumo
Uma regex descreve a forma do texto, numa notação mais pequena do que parece e menos portável do que aparenta. Aprenda a dúzia de símbolos, verifique o dialeto onde o código vai correr, e trate um quantificador dentro de um quantificador como um defeito até prova em contrário. O padrão que passa todos os seus testes não é o que vai deitar o site abaixo.