Passo a passo · 6 min de leitura
Expressões regulares: um guia prático de início
Uma expressão regular é um padrão que descreve um conjunto de strings. Ela pode verificar se um texto corresponde, encontrar todas as ocorrências, capturar partes de uma correspondência ou localizar e substituir com referências a essas capturas. A maioria das linguagens de programação e muitas ferramentas de linha de comando implementa a mesma sintaxe básica com pequenas diferenças.
A dificuldade não está em escrever um padrão uma vez, mas em entender por que um padrão corresponde mais ou menos do que você pretendia. Este guia cobre os blocos de construção, o modelo de correspondência que explica resultados surpreendentes e os limites de segurança que vale conhecer antes de rodar um padrão sobre entrada não confiável.
Uma expressão regular é uma descrição, não uma string de busca
Uma busca literal encontra os caracteres exatos que você digitou. Uma expressão regular interpreta alguns caracteres como instruções, então o padrão abc corresponde aos caracteres abc, enquanto a.c corresponde a qualquer string de três caracteres que comece com a e termine com c. Essa interpretação é o que torna o regex poderoso e o que faz um ponto sem escape virar um bug comum.
Expressões regulares costumam ser aplicadas a uma linha ou a uma string inteira dependendo das flags, e esse contexto muda o resultado. O mesmo padrão pode corresponder a uma substring em uma única linha e a algo completamente diferente quando as quebras de linha são incluídas. Confirme sempre se o mecanismo trata a entrada como uma única string ou como linhas separadas antes de depurar o padrão em si.
Os blocos de construção que vale aprender primeiro
Classes de caracteres correspondem a um caractere de um conjunto: [abc] corresponde a a, b ou c; [a-z] corresponde a qualquer letra ASCII minúscula; \d é um dígito; \w é um caractere de palavra; \s é espaço em branco. Classes negadas invertem o significado, então [^0-9] corresponde a qualquer coisa que não seja um dígito. Âncoras não consomem caracteres: ^ corresponde ao início da string ou da linha e $ ao final.
Quantificadores dizem quantas vezes o elemento anterior se repete: * significa zero ou mais, + significa um ou mais, ? significa zero ou um, e {2,5} significa entre dois e cinco. Por padrão são gananciosos e pegam o máximo possível, devolvendo caracteres só quando o resto do padrão precisa deles; adicionar ? os torna preguiçosos e pegam o mínimo possível. Grupos entre parênteses capturam o que correspondeu, e uma barra vertical cria alternativas como cat|dog.
- \d+ corresponde a um ou mais dígitos; ^\d{5}$ corresponde exatamente a cinco dígitos sozinho.
- \bword\b corresponde à palavra inteira, não à palavra dentro de outra palavra.
- ([A-Za-z]+)\s+([A-Za-z]+) captura duas palavras separadas por espaço em branco.
- a.*?b corresponde do primeiro a até o b mais próximo, enquanto a.*b chega até o último b.
- Flags mudam o comportamento: i ignora maiúsculas e minúsculas, g encontra todas as correspondências, m trata ^ e $ como âncoras de linha, s permite que o ponto corresponda a quebras de linha e u ativa a semântica Unicode completa.
Como a correspondência realmente avança
Os mecanismos testam o padrão em posições sucessivas da entrada (a menos que esteja ancorado), e em cada posição exploram alternativas. Quando um quantificador ganancioso consumiu caracteres e o resto do padrão falha, o mecanismo retrocede e tenta uma repetição mais curta. Esse retrocesso é a razão pela qual um padrão pode corresponder corretamente e ainda assim ser lento: o número de caminhos explorados pode crescer exponencialmente com a entrada.
A correspondência global é um laço em torno desse processo. Um padrão com a flag g devolve correspondências sucessivas, e correspondências de comprimento zero avançam a posição de forma especial para evitar um laço infinito. Grupos de captura são numerados pela ordem em que o parêntese de abertura aparece, e grupos nomeados tornam padrões complexos muito mais fáceis de ler e manter.
Um fluxo de trabalho de depuração que funciona
Comece pela menor entrada que deveria corresponder e pela entrada mais próxima que não deveria. Teste as duas e depois adicione complexidade um elemento de cada vez em vez de escrever o padrão completo e ficar olhando o resultado. Quando uma correspondência está errada, faça três perguntas: onde começou, onde terminou e qual alternativa ou repetição consumiu os caracteres do meio.
Escape os caracteres que o mecanismo trata de forma especial. Esta é a causa mais comum de confusão ao testar URLs, caminhos de arquivo, endereços de e-mail e trechos de código. Lembre que literais de string de JSON e de linguagens de programação também processam barras invertidas, então um padrão escrito para um mecanismo regex pode precisar da barra invertida duplicada no código-fonte. Use um testador que mostre tanto o padrão que você digitou quanto o padrão que o mecanismo recebeu.
Segurança: backtracking catastrófico e entrada não confiável
Alguns padrões são corretos mas catastroficamente lentos em entradas específicas. Quantificadores aninhados como (a+)+$ são o exemplo clássico: para uma sequência longa de caracteres seguida de um que não pode corresponder à âncora final, o mecanismo tenta um número exponencial de formas de dividir a sequência. As defesas práticas são simplificar a repetição aninhada, ancorar padrões sempre que possível, preferir classes de caracteres explícitas e aplicar um timeout ou limite de comprimento de entrada ao casar texto não confiável.
Nunca use uma expressão regular como única defesa para análise sensível à segurança. Endereços de e-mail, HTML, URLs e linguagens de programação ultrapassam o que um padrão compacto consegue validar corretamente. Use regex para uma checagem rápida de sanidade e depois analise com uma biblioteca dedicada ou valide estruturalmente. E quando um padrão roda em um servidor, limite o tamanho da entrada e o número de correspondências para uma única requisição não conseguir consumir o processo.