Regex na prática: âncoras, quantificadores e grupos de captura
Resumo (TL;DR)
Em 2 de julho de 2019, uma única regex dentro do WAF da Cloudflare — um padrão “quase-casado” com o formato .*(?:.*=.*) — derrubou perto de 50% do tráfego global por 27 minutos. O backtracking do PCRE fixou um núcleo de CPU em 100% e toda requisição ficou travada atrás dele. A lição mais honesta que a regex ensina é que ela esconde seu próprio custo. Na maioria dos dias é pequena, rápida e sedutora; uma entrada ruim pode parar um sistema. É por isso que este texto tem um tom levemente cético. A maior parte do trabalho cotidiano com regex se apoia em um punhado de peças: âncoras que amarram um padrão ao início, ao fim ou a uma fronteira de palavra; classes de caracteres que descrevem “qualquer um desses caracteres”; quantificadores que dizem “quantas vezes”; e grupos que permitem capturar, referenciar ou alternar. Acertar isso e os problemas típicos — validar um e-mail aproximado, extrair campos de uma linha de log, normalizar um telefone — ficam curtos e legíveis. Errar e você acaba com padrões que casam demais, de menos, ou que paralisam o motor como no caso Cloudflare. Não parseie HTML, JSON ou XML com regex — linguagens regulares não conseguem descrever aninhamento balanceado. Motores também diferem: regex de ECMAScript, PCRE (Perl/PHP), re do Python, Oniguruma (Ruby e a crate onig do Rust) e RE2 do Go cada um tem diferenças sutis. O incidente da Cloudflare só aconteceu porque o motor era PCRE; sob o RE2 teria terminado em tempo linear.
Contexto e conceitos
Uma expressão regular é uma gramática compacta para casar strings. Vale nomear explicitamente as peças que você mais busca.
Âncoras não consomem caracteres — elas afirmam uma posição. ^ é o início da entrada (ou de uma linha em modo multilinha), $ é o fim, e \b é uma fronteira de palavra (a transição entre \w e não-\w). Sem âncoras, abc casa em qualquer lugar dentro de uma string maior. Com elas, ^abc$ casa apenas o abc literal inteiro, e \babc\b casa abc como palavra isolada.
Classes de caracteres descrevem um único caractere de um conjunto. [abc] é a, b ou c; [a-z] é qualquer letra minúscula; [^0-9] é “qualquer coisa que não seja dígito.” Classes abreviadas cobrem os casos comuns: \d (dígitos), \w (caracteres de palavra — letras, dígitos, sublinhado), \s (espaço em branco). As negações \D, \W, \S são as inversas. . casa qualquer caractere exceto nova linha por padrão; a flag s (dotall) muda isso.
Quantificadores se ligam ao token anterior e dizem quantas vezes ele pode se repetir. * é zero ou mais, + é um ou mais, ? é zero ou um, e {n,m} é “entre n e m” (qualquer um dos limites pode ser omitido). Por padrão, quantificadores são gananciosos: casam o máximo possível e só cedem se o resto do padrão falhar. Acrescente ? para a forma preguiçosa — *?, +?, ?? — que casa o mínimo possível e só se estende quando forçada. Alguns motores adicionam quantificadores possessivos (*+, ++) que casam de forma gananciosa mas recusam ceder em falha, o que pode evitar backtracking catastrófico em entrada patológica — exatamente a ferramenta que faltava no incidente da Cloudflare.
Grupos envolvem subpadrões. (pattern) é um grupo de captura — numerado a partir de 1 pela ordem do parêntese de abertura — cujo casamento pode ser referenciado com \1 dentro do padrão ou com acessores indexados na linguagem host. (?<nome>pattern) adiciona uma captura nomeada. (?:pattern) é um grupo não capturante, usado puramente para alternância ((?:gato|cachorro)) ou agrupamento de quantificador sem registrar o casamento. | dentro de um grupo é alternância: case uma das opções.
Por fim, flags mudam o comportamento do motor: i para case-insensitive, m para multilinha (^ e $ casam fronteiras de linha), s para dotall, u para Unicode. Em JavaScript, a flag u também habilita escapes de propriedade Unicode como \p{L} para “qualquer letra.”
Comparação e dados
| Quantificador | Ganancioso (padrão) | Preguiçoso (sufixo ?) | Possessivo (onde houver suporte) |
|---|---|---|---|
* / *? / *+ | Case o máximo, faça backtrack em falha | Case o mínimo, estenda em falha | Case o máximo, sem backtrack |
+ / +? / ++ | Um ou mais, ganancioso | Um ou mais, preguiçoso | Um ou mais, sem backtrack |
? / ?? / ?+ | Zero ou um, prefere um | Zero ou um, prefere zero | Zero ou um, sem backtrack |
{n,m} / {n,m}? / {n,m}+ | Faixa, gananciosa | Faixa, preguiçosa | Faixa, sem backtrack |
Ganancioso é o padrão certo com frequência suficiente para ser o padrão. Preguiça importa quando “o resto do padrão” é em si permissivo — por exemplo, extrair <b>...</b> de um trecho HTML com <b>(.*?)</b> em vez de um .* ganancioso que poderia engolir várias tags (e ainda assim, não parseie HTML de verdade com regex). Quantificadores possessivos e grupos atômicos (?>...) ajudam quando um padrão de outro modo re-exploraria exponencialmente muitos caminhos de backtrack em quase-casamentos. PCRE, Java e Oniguruma suportam; ECMAScript e o re do Python historicamente não suportavam, embora o Python 3.11 tenha adicionado grupos atômicos ao módulo re.
Cenários reais
Cenário 1 — Um casamento pragmático de e-mail. A gramática completa de e-mail da RFC 5322 admite comentários, partes locais entre aspas e literais IP aninhados; uma regex que cobre tudo isso é notoriamente monstruosa (a tentativa mais famosa tem 6.425 caracteres) e ainda assim não é um parser real. O padrão que efetivamente coloquei em produção é quase sempre ^[^\s@]+@[^\s@]+\.[^\s@]+$ — “não vazio, sem espaço em branco, sem @ perdido, com pelo menos um ponto no domínio” — que rejeita os erros de digitação óbvios sem fingir validar completamente. A única forma de verificar de verdade um endereço é enviar uma mensagem. Use regex para a forma, e-mail para a existência.
Cenário 2 — Telefones em formatos internacionais. +55 11 91234-5678, (11) 91234-5678 e 55-11-91234-5678 descrevem o mesmo número de São Paulo. Uma regex como ^\+?\d{1,3}[-\s().]*\d{1,4}[-\s().]*\d{3,4}[-\s().]*\d{3,4}$ aceita a pontuação comum, e depois uma etapa de normalização tira a pontuação para uma forma canônica só com dígitos. Para qualquer coisa séria — roteamento, armazenamento, discagem — use a libphonenumber do Google em vez de inventar a sua. O resultado mais rápido de reunião que vi nesse tópico foi “não vamos fazer isso com regex”, o que poupou ao time uma semana de bugs de casos de borda. Regex é para a superfície “isso parece vagamente com um telefone?”.
Cenário 3 — Extrair campos de uma linha de log. Uma linha como 2026-04-13T02:11:05Z 192.0.2.42 "GET /search?q=foo HTTP/1.1" 200 1534 pode ser dividida com um único padrão: ^(?<ts>\S+)\s+(?<ip>\S+)\s+"(?<method>\w+)\s+(?<path>\S+)\s+\S+"\s+(?<status>\d+)\s+(?<bytes>\d+)$. Grupos nomeados valem a pena aqui: o objeto de match resultante é como um dicionário e cada campo é endereçável pelo nome. Quando o formato do log muda, o padrão também é a documentação do que você está parseando.
Equívocos comuns
“Uma regex pode validar totalmente um endereço de e-mail.” Ela só pode validar a forma. A RFC 5322 é complexa demais para codificar sensatamente em uma regex — e mesmo que você codificasse, “forma válida” não significa “mailbox existe.” O padrão da indústria é uma regex simples mais um e-mail de verificação.
“Ganancioso é sempre mais lento que preguiçoso.” Não inerentemente. Casamentos gananciosos podem ser mais rápidos quando o subpadrão do quantificador é muito restritivo, porque o motor termina em uma passagem longa pra frente. Preguiçoso vence quando “o resto do padrão” ancora o casamento, como em <b>(.*?)</b>. Benchmark com entradas realistas em vez de ir no reflexo de usar ?.
“Todos os motores de regex são iguais.” Não são. Regex ECMAScript não tem quantificadores possessivos nem grupos atômicos (a flag v nos motores modernos fecha algumas lacunas, mas não essas); o re do Python tem seu próprio conjunto de propriedades Unicode; PCRE suporta back-references e padrões recursivos; Oniguruma — o motor usado por Ruby e pela crate onig do Rust — é ainda outro dialeto. O RE2 do Go abre mão de back-references e lookarounds em troca de tempo de execução garantidamente linear na entrada — o mesmo motor que a Cloudflare avaliou adotar depois do incidente. Um padrão que você copia de um tutorial de Perl pode não funcionar em JavaScript, e vice-versa.
“Regex pode parsear HTML (ou JSON, ou XML).” Não, porque linguagens regulares não conseguem descrever aninhamento balanceado. Regex pode extrair um subpadrão específico e bem formado de texto estruturado — um valor de atributo, por exemplo — mas não pode parsear corretamente a árvore inteira. Para formatos aninhados, use um parser dedicado (DOMParser, JSON.parse, uma biblioteca XML, um leitor de CSV). A saga “regex vs HTML” no Stack Overflow (a resposta de 2009) é um conto de aviso, não um debate.
Checklist
- Qual é a entrada, e quais são os contra-exemplos? Escreva os dois antes de escrever o padrão.
- O dado é aninhado ou recursivo? Se sim, use um parser. Regex é a ferramenta errada.
- Qual motor você está mirando? JavaScript, Python, Go, PCRE diferem em lookaround, back-references, Unicode.
- Você precisa do match em si ou apenas de um sim/não? Prefira
(?:...)não capturante para grupos que só existem para alternância ou quantificação. - O padrão vem do usuário, ou é aplicado a entrada não confiável? Proteja contra backtracking catastrófico com limite de tempo, grupos atômicos ou um motor de tempo linear como o RE2. Caso contrário, você vira a Cloudflare.
- Você faz normalização de texto depois? Não codifique tudo em um padrão enorme; combine uma checagem de forma simples com um pequeno passo de pós-processamento.
- A regex está documentada? Modo multilinha com
x(extended) ou um comentário acima do padrão é seguro barato para a próxima pessoa ler.
Ferramenta relacionada
O testador de regex da Patrache Studio roda padrões contra entrada de exemplo no navegador e mostra capturas de grupo inline, o que é mais rápido do que alternar entre abas. Quando as strings que você está casando são elas mesmas estruturadas — linhas de log com payloads JSON, respostas de API — combine o trabalho de regex com Formatação, validação e JSON Schema na prática para que o trecho extraído seja validado por um parser adequado em vez de uma segunda regex. Um alvo comum de regex é um UUID embutido em uma URL; UUID v1 vs v4 vs v7: escolhendo uma chave primária de BD cobre por que os mesmos 36 caracteres podem significar coisas diferentes dependendo dos bits de versão.
Referências
- MDN, guia “Regular Expressions” — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions
- IETF RFC 5322, “Internet Message Format” (gramática de e-mail) — https://datatracker.ietf.org/doc/html/rfc5322
- regex101, testador interativo com seleção de dialeto — https://regex101.com/
- Google, RE2 — motor de regex em tempo linear — https://github.com/google/re2/wiki/Syntax