Desenvolvimento

Regex do zero: âncoras, classes e quantificadores

Expressão regular parece feitiço até você perceber que são só quatro ideias empilhadas: onde a busca começa (âncoras), o que pode aparecer em cada posição (classes), quantas vezes (quantificadores) e como agrupar tudo (grupos e alternância). Depois disso, ler ^\d{4}-\d{2}-\d{2}$ deixa de ser adivinhação. Este guia constrói esse vocabulário do zero, com padrões que você realmente usa, data, telefone, código de ticket, e depois vai fundo no que separa quem escreve regex de brincadeira de quem escreve regex em produção: gulosidade versus preguiça, Unicode de verdade, lookahead e lookbehind, e a armadilha mais cara de todas, o backtracking catastrófico que já tirou a Cloudflare do ar. Todos os exemplos rodam no [construtor de regex](tool:regex-builder), que testa o padrão ao vivo e explica cada símbolo.

J-Kit17 min de leituraIntermediário
  • Regex
  • JavaScript
  • Unicode
  • Segurança
  • Texto

Resumo rápido

  • Âncoras (^ $ \b) não casam caracteres, fixam posições, como início de linha ou borda de palavra.
  • Classes dizem o que pode aparecer; quantificadores dizem quantas vezes. Um ? depois torna o quantificador preguiçoso.
  • Quantificadores aninhados sobre trechos ambíguos causam backtracking catastrófico (ReDoS): o motor pode explorar 2^(n-1) partições de uma entrada de n caracteres.
  • Lookahead, lookbehind e grupos nomeados são padrão no JavaScript desde 2018; grupos atômicos e quantificadores possessivos, não, emule-os com (?=(...))\1 quando precisar travar o backtracking.

Como ler um padrão

Uma regex descreve um texto usando símbolos. O motor lê o padrão da esquerda para a direita e tenta encaixá-lo no texto, posição por posição. Em JavaScript, o padrão vira um objeto RegExp e é aplicado sobre a string, é exatamente isso que o construtor de regex faz por baixo, destacando cada trecho que casou. A leitura fica fácil quando você separa os símbolos por função:

Literais
Caracteres comuns (a, 7, -) casam a si mesmos.
Âncoras
Fixam uma posição sem consumir texto: ^ $ \b.
Classes
Dizem quais caracteres cabem numa posição: [a-z], \d, .
Quantificadores
Repetem o item anterior: * + ? {n,m}.
Grupos e alternância
Juntam pedaços e oferecem opções: (…) e |.
Asserções
Olham ao redor sem consumir: lookahead (?=…) e lookbehind (?<=…).

Âncoras: onde, não o quê

Âncoras são a fonte de confusão número um porque não casam nenhum caractere: elas afirmam uma posição. ^ exige o início do texto; $ exige o fim. \b é uma borda de palavra, o limite entre um caractere de palavra (\w) e um não-palavra, útil para não capturar "gato" dentro de "gatorade". \B é o oposto: uma posição que não é borda de palavra. Como âncoras não consomem texto, elas são o antídoto mais barato contra o backtracking: um $ no fim de um padrão diz ao motor "ou fecha aqui, ou desiste", cortando o passeio pelas combinações que ele faria sem a âncora.

^abc   → casa "abc" apenas no início
abc$   → casa "abc" apenas no fim
\bfoo\b → casa "foo" isolada, não "food" nem "barfoo"
Âncoras afirmam posições; não consomem caracteres.

Classes de caracteres e Unicode

Uma classe entre colchetes casa exatamente um caractere do conjunto. [abc] casa a, b ou c; [a-z] usa um intervalo; [A-Za-z0-9] combina vários. Um ^ logo após o colchete inverte a classe: [^0-9] casa qualquer coisa que não seja dígito. Dentro dos colchetes, quase todos os metacaracteres perdem o poder, um ponto ali é só um ponto. Para os conjuntos mais comuns existem atalhos, e cada um tem sua negação em maiúscula:

Atalhos de classe e suas negações.
AtalhoEquivale aNegação
\d[0-9] (dígito)\D (não-dígito)
\w[A-Za-z0-9_] (palavra, só ASCII)\W (não-palavra)
\sespaço, tab, quebra de linha\S (não-espaço)
.qualquer caractere, menos quebra de linha(use a flag s para incluir \n)
\p{L}qualquer letra Unicode (exige a flag u)\P{L} (não-letra)

Repare na segunda e na última linha: \w é só ASCII. Ele equivale a [A-Za-z0-9_] e não inclui acentos, "coração", "José" e "ñ" ficam de fora. Esse é o erro silencioso número um em texto português. A solução moderna é a flag u somada às property escapes: \p{L} casa qualquer letra Unicode, \p{N} qualquer número, \p{P} pontuação. É a mesma razão de o contador de caracteres usar \p{L} em vez de \w para contar palavras em português. As property escapes \p{…} e a flag u são padrão no ECMAScript desde o ES2018.

// Contar "palavras" em português
"São Paulo".match(/\w+/g)     → ["S", "o", "Paulo"]   // \w quebra no "ã"!
"São Paulo".match(/\p{L}+/gu)  → ["São", "Paulo"]      // \p{L} + flag u acerta
Sem a flag u e \p{L}, o acento vira uma fronteira e parte a palavra em pedaços.
Por que \w ignora ç, é e ã

\w foi definido nos anos 1980 como um atalho ASCII: exatamente [A-Za-z0-9_]. Ele nunca soube o que é acento. Você tem duas saídas honestas: listar as letras à mão, como [A-Za-zÀ-ÿ], que cobre o latino-1 mas esquece outros scripts; ou ativar a flag u e usar \p{L}, que delega ao banco de dados Unicode a pergunta "isto é uma letra?". A segunda é a correta para texto de verdade, e é a única que também acerta grego, cirílico e cada emoji-letra que aparecer.

Code point vs grafema: por que /./u não conta emoji direito

Sem a flag u, o ponto casa uma unidade UTF-16, não um caractere. Como quase todo emoji vive acima de U+FFFF, ele é guardado em dois "code units" (um par surrogate), então "😀".length é 2 e /./g acha dois casamentos onde você vê um. Com a flag u, o ponto passa a casar um code point inteiro: /./gu acha um único casamento em "😀". Mas aí mora a próxima pegadinha.

Um grafema, o que o olho chama de "um caractere", pode ser vários code points. A bandeira "🇧🇷" são dois indicadores regionais (dois code points, quatro code units); "👍🏽" é o polegar mais um modificador de tom de pele. Contra "🇧🇷", /./gu retorna 2, não 1, porque conta code points. Para contar grafemas de verdade, a régua que um humano usaria, a ferramenta certa não é regex, é o Intl.Segmenter do próprio JavaScript, que agrupa os code points em clusters de grafema.

A flag v (unicodeSets): o upgrade de 2024

O ECMAScript 2024 acrescentou a flag v, um superconjunto do u. Ela liga a notação de conjunto dentro das classes: interseção com &&, diferença com --, e classes aninhadas. Por exemplo, [\p{Script=Greek}&&\p{Letter}] casa só letras gregas. Regras: u e v são mutuamente exclusivas (usar as duas dá SyntaxError), e a flag v está disponível nos navegadores atuais desde 2023. O construtor de regex desta página expõe u, mas não v, para a maioria dos padrões do dia a dia, u já basta.

Quantificadores: guloso, preguiçoso e possessivo

Um quantificador repete o item imediatamente anterior. Por padrão eles são gulosos: pegam o máximo de texto possível e só devolvem (fazem backtracking) se o resto do padrão não fechar. Acrescentar ? deixa o quantificador preguiçoso, ele pega o mínimo e cresce só se precisar. Existe um terceiro modo, o possessivo, que pega o máximo e nunca devolve: é a defesa mais direta contra backtracking. O problema para quem usa JavaScript é que a linguagem não tem possessivos nem grupos atômicos, mas dá para emulá-los, e a gente chega lá.

Modos de quantificação. As duas últimas linhas NÃO existem no JavaScript padrão.
FormaComportamentoNo JavaScript?
.* .+ (guloso)Pega o máximo; recua se o resto falhar.Sim
.*? .+? (preguiçoso)Pega o mínimo; cresce só se precisar.Sim
.*+ .++ (possessivo)Pega o máximo e nunca recua.Não
(?>…) (grupo atômico)Congela o que casou; descarta os pontos de recuo.Não
(?=(…))\1 (atômico emulado)Truque padrão para simular grupo atômico em JS.Sim (idioma)

O caso clássico que quase todo mundo erra é extrair tags HTML. O ponto casa qualquer coisa, então <.*> guloso engole demais. Compare os três padrões sobre o mesmo texto:

Texto:  <b>negrito</b>

<.*>     (guloso)      → casa "<b>negrito</b>"  , uma ocorrência, engole tudo
<.*?>    (preguiçoso)  → casa "<b>" e "</b>"   , duas ocorrências, para no 1º >
<[^>]*>  (classe certa) → casa "<b>" e "</b>"   , igual, e sem risco de backtracking
Guloso vs. preguiçoso vs. classe específica. A terceira forma é a mais segura: [^>] não pode "passar por cima" de um >.

A lição vale ouro: trocar .* por uma classe negada como [^>] resolve o mesmo problema que o preguiçoso resolveria, mas sem deixar o motor com pontos de recuo para explorar. Sempre que o delimitador é conhecido (um >, uma aspa, uma vírgula), prefira [^delimitador]* a .*?. Você ganha clareza e blinda o padrão contra a explosão de backtracking da última seção.

Teste você mesmo: cole "<b>negrito</b>" no texto e alterne entre <.*> e <.*?> para ver a diferença ao vivo.Abrir a ferramenta em página inteira

Primeiro exemplo trabalhado, agora de extração numérica: \d+(?:[.,]\d+)? aplicado a "Total: R$ 123,45 (parcelado em 3x de 41,15)" com a flag g retorna três resultados, 123,45, 3 e 41,15. O \d+ pega a parte inteira; o grupo opcional (?:[.,]\d+)? captura a parte decimal quando ela existe (por isso o "3", sem casas, também casa). Esse é justamente um dos padrões prontos da biblioteca do construtor de regex.

Grupos, flags, lookahead e lookbehind

Parênteses agrupam um trecho para aplicar um quantificador ou para capturar o valor casado. (\d{4}) cria um grupo de captura, o motor guarda o que casou para você reaproveitar (por exemplo, em uma substituição com $1). Se você só quer agrupar, sem guardar, use um grupo sem captura (?:…), mais leve. Grupos nomeados (?<ano>\d{4}) dão nome ao valor e você o recupera na substituição como $<ano>. A barra | é alternância: gato|cachorro casa uma das duas palavras. Grupos nomeados, como o lookbehind, são padrão no JavaScript desde o ES2018.

// Segundo exemplo trabalhado: reformatar data com grupos nomeados
"05/05/2026".replace(
  /(?<dia>\d{2})\/(?<mes>\d{2})\/(?<ano>\d{4})/,
  "$<ano>-$<mes>-$<dia>"
)
→ "2026-05-05"
Grupos nomeados na captura e $<nome> na substituição transformam dd/mm/aaaa em aaaa-mm-dd.

Lookahead e lookbehind são asserções: olham o que vem antes ou depois da posição atual sem consumir esse texto. São quatro, e a tabela abaixo é a referência que você vai consultar sempre. O uso mais comum é validar sem capturar, por exemplo, exigir que uma senha tenha ao menos um dígito com (?=.*\d), sem que esse pedaço faça parte do casamento.

As quatro asserções de lookaround. Todas padrão no JavaScript (lookbehind desde o ES2018).
SintaxeNomeAfirma que a posição está…
(?=…)Lookahead positivo…seguida por …
(?!…)Lookahead negativo…não seguida por …
(?<=…)Lookbehind positivo…precedida por …
(?<!…)Lookbehind negativo…não precedida por …
// Extrair só o número depois de "R$", sem levar o "R$" junto
"R$ 123,45".match(/(?<=R\$\s?)\d+(?:[.,]\d+)?/)  → "123,45"

// Casar "gol" só quando NÃO for seguido de "eiro" (evita "goleiro")
/gol(?!eiro)/  → casa "gol" em "gol de placa", não em "goleiro"

// Separador de milhar: um dígito seguido de grupos de três até a borda
"1234567".replace(/\d(?=(\d{3})+\b)/g, "$&.")  → "1.234.567"
Lookbehind pega o que vem depois de um prefixo; lookahead filtra pelo que vem à frente, sem consumir nenhum dos dois.
  1. ES2015Flag u (modo Unicode)

    O ECMAScript 2015 (ES6) traz o modo Unicode: o ponto e as classes passam a raciocinar por code point, e o caminho para \p{…} fica aberto.

  2. ES2018Lookbehind, grupos nomeados, \p{…}, dotAll

    A grande safra: lookbehind (?<=) e (?<!), grupos nomeados (?<nome>…), property escapes \p{…} e a flag s (dotAll) entram todos de uma vez.

  3. ES2022Flag d (índices de casamento)

    A flag d (hasIndices) passa a expor as posições de início e fim de cada grupo casado, útil para realçar trechos no texto.

  4. ES2024Flag v (unicodeSets)

    Classes de conjunto com interseção (&&), diferença (--) e aninhamento, além de \p{…} para propriedades de string.

Flags mudam o comportamento do motor inteiro e vão depois do padrão. O construtor de regex expõe seis: g (global, todas as ocorrências), i (ignora maiúsculas e minúsculas), m (multiline, ^ e $ por linha), s (dotAll, o ponto passa a casar quebras de linha), u (modo Unicode, necessário para \p{…}) e y (sticky, casa a partir de uma posição fixa). O JavaScript moderno tem mais duas que a ferramenta não usa: d (desde o ES2022, expõe os índices de cada grupo) e v (desde o ES2024, o superconjunto do u). No localizar e substituir texto, marcar "diferenciar maiúsculas" simplesmente liga ou desliga a flag i.

Armadilhas: escape, backtracking e ReDoS

Para casar um metacaractere literalmente, escape-o com barra invertida: \. casa um ponto, \? casa uma interrogação, \( casa um parêntese. Esquecer isso é o erro mais comum, 3.14 com o padrão 3.14 casa "3x14" também, porque o ponto é curinga. Um detalhe de linguagem: dentro de uma string de código JavaScript você dobra a barra ("\\d"), mas no campo do construtor de regex você digita o padrão direto (\d), sem a barra extra. Resolvido o escape, sobra a armadilha que custa caro de verdade.

Backtracking catastrófico acontece quando o padrão deixa o motor com muitas maneiras de dividir o mesmo texto. Pegue (a+)+ contra uma entrada de "a"s que termina num caractere que faz o padrão falhar, como "aaaaaaaaaaX". O grupo interno a+ pode ficar com 1, 2, 3… "a"s; o grupo externo repete isso; e há 2^(n-1) maneiras de particionar uma sequência de n "a"s em blocos. Como o X no fim nunca casa, o motor testa todas essas partições antes de desistir. Para n = 30, são mais de meio bilhão de tentativas, o navegador congela. O motor de regex do JavaScript é baseado em backtracking, então ele é vulnerável; o construtor de regex desta página limita a 1000 casamentos por execução justamente para não travar a aba.

0131.072262.144393.216524.28821120Comprimento da entrada (n)Partições exploradas (teórico)(a+)+, backtracking, 2^(n-1)a+, linear, n passos
Crescimento TEÓRICO: número de partições que um motor de backtracking pode explorar num trecho de n caracteres idênticos. A curva "(a+)+" é 2^(n-1) (contagem combinatória, não milissegundos medidos); a curva "a+" é linear. Repare como a linear parece colada ao eixo, é exatamente esse o problema.
Ver os dados
x(a+)+, backtracking, 2^(n-1)a+, linear, n passos
222
484
6326
81288
1051210
122.04812
148.19214
1632.76816
18131.07218
20524.28820
2^(n-1)partições que o motor pode explorar num trecho ambíguo de n caracteres
~27 minqueda global da Cloudflare em 2 de julho de 2019 por backtracking numa regra de WAF
0grupos atômicos ou quantificadores possessivos no JavaScript padrão

Isso não é teoria de laboratório. Em 2 de julho de 2019, a Cloudflare ficou fora do ar por cerca de 27 minutos, e derrubou boa parte da internet junto, por causa de uma única regra de WAF. O trecho culpado era .*(?:.*=.*): dois .* gulosos disputando o mesmo texto. Contra requisições legítimas, o motor entrava em backtracking e saturava a CPU de toda a rede global. No relatório oficial, a Cloudflare foi direta: "o motor de expressões regulares em uso não tinha garantias de complexidade". A correção incluiu migrar para motores com tempo de execução garantido, como o RE2 e o regex do Rust.

Como blindar um padrão contra ReDoS

Evite quantificador dentro de quantificador sobre o mesmo alfabeto, (a+)+, (.*)* e (a|a)* são os arquétipos. Troque .* aberto por uma classe específica que exclua o delimitador, como [^>]* ou [^"]*. Ancore o padrão: um ^ no começo e um $ no fim tiram do motor as posições de recuo. Prefira quantificadores exatos ({4}) a intervalos frouxos quando souber o tamanho. E teste sempre com uma entrada longa e propositalmente falha antes de confiar no padrão.

Como o JavaScript não tem grupos atômicos nem possessivos, o truque padrão para "travar" um trecho é embrulhá-lo num lookahead com captura e referenciar a captura logo em seguida: (?=(a+))\1. O lookahead casa a+ da forma gulosa, a captura guarda o resultado e a backreference \1 o consome sem deixar pontos de recuo, o efeito prático de um grupo atômico. Fora isso, valide o tamanho da entrada no código antes de rodar a regex: nenhuma expressão precisa processar 10 MB de uma vez.

RE2 e as engines lineares: por que abrem mão de lookaround e backreference

RE2 (do Google) e o regex do Rust não fazem backtracking. Eles compilam o padrão num autômato e varrem a entrada uma vez, com tempo garantidamente linear no tamanho do texto, imunes a ReDoS. O preço é que eles se recusam a implementar recursos para os quais só existe solução por backtracking: backreferences e lookaround (lookahead/lookbehind) ficam de fora. Nas palavras da documentação do RE2, ele "não suporta construções para as quais só se conhecem soluções por backtracking". É uma troca consciente: você perde dois recursos e ganha a garantia de que nenhuma entrada maliciosa derruba o serviço, foi por isso que a Cloudflare migrou.

PCRE, Python e ECMAScript: mesmos ossos, sabores diferentes

O JavaScript (padrão ECMAScript) não é idêntico ao PCRE usado em PHP, Perl ou grep -P, nem ao módulo re do Python. As bases, âncoras, classes, quantificadores, alternância, são iguais, mas os detalhes divergem: JS não tem grupos atômicos (?>…) nem possessivos (a++); as âncoras \A e \z do PCRE não valem em JS (use ^ e $); e a sintaxe de propriedades e a nomeação de grupos variam. Antes de copiar um padrão da internet, confirme para qual motor ele foi escrito e teste no de destino. Se o padrão é só um passo de um pipeline, vale combinar com o guia de conversão entre JSON e YAML para extrair e depois estruturar o resultado.

Perguntas frequentes

Qual a diferença entre * e +?
* casa zero ou mais repetições, o item pode até não aparecer. + casa uma ou mais, exige pelo menos uma ocorrência. Por isso \d* casa uma string vazia, mas \d+ só casa se houver ao menos um dígito.
O que é backtracking catastrófico (ReDoS) e como evito em JavaScript?
É quando um padrão com quantificadores aninhados sobre trechos ambíguos, como (a+)+ ou .*.*, leva o motor a testar um número exponencial de partições da entrada antes de falhar, saturando a CPU. Como o JavaScript não tem grupos atômicos nem quantificadores possessivos, evite: troque .* por classes específicas como [^>]*, ancore o padrão com ^ e $, não aninhe quantificadores e, se precisar travar um trecho, emule o grupo atômico com (?=(…))\1. Foi um padrão desse tipo que tirou a Cloudflare do ar em 2019.
Posso validar um e-mail com uma regex?
Você pode checar o formato, não a existência. A RFC 5322 é tão permissiva que uma regex "completa" fica gigante e insegura; por isso o padrão HTML do WHATWG usa de propósito uma versão simples e assumidamente "não conforme". Na prática, cheque só o básico (um @, um domínio com ponto) e confirme o endereço enviando um e-mail com link ou código de verificação.
Por que \w não casa "ç" ou "é"?
Porque \w é um atalho ASCII: equivale a [A-Za-z0-9_] e foi definido antes do Unicode dominar. Para casar letras acentuadas e de outros alfabetos, ligue a flag u e use a property escape \p{L} (qualquer letra Unicode). É por isso que ferramentas que contam palavras em português usam \p{L}, não \w.
O JavaScript tem lookbehind e grupos nomeados?
Sim, os dois são padrão desde o ECMAScript 2018. Lookbehind vem em duas formas, (?<=…) positivo e (?<!…) negativo, e o JavaScript ainda permite lookbehind de tamanho variável. Grupos nomeados usam (?<nome>…) na captura e $<nome> na substituição. O que o JS não tem são grupos atômicos e quantificadores possessivos.
Como faço a regex parar de "comer" texto demais?
O comportamento guloso é o padrão. Torne o quantificador preguiçoso com ? (por exemplo .+? no lugar de .+) ou, melhor ainda, troque o ponto por uma classe específica que não inclua o delimitador, como [^>]+ para casar até o próximo ">". A classe específica é mais segura porque não deixa pontos de backtracking.
A mesma regex funciona em JavaScript e em Python/PHP?
As bases (âncoras, classes, quantificadores) são iguais, mas há diferenças: grupos atômicos e possessivos existem no PCRE e não no JS; as âncoras \A e \z são do PCRE; e nomes de grupos e sintaxe Unicode variam entre ECMAScript, PCRE e o módulo re do Python. Teste sempre no motor de destino antes de confiar no padrão.

Leia toda regex por função: âncoras fixam a posição, classes dizem o que cabe, quantificadores dizem quantas vezes e grupos organizam o resto. Domine gulosidade versus preguiça, use \p{L} com a flag u para não perder acentos, e conheça lookahead, lookbehind e grupos nomeados, padrão no JavaScript desde 2018. Acima de tudo, desconfie de quantificadores aninhados: eles causam o backtracking catastrófico que já tirou a Cloudflare do ar. Prefira classes específicas a .*, ancore os padrões e teste tudo ao vivo no construtor de regex.

Fontes e referências

  1. MDN, Regular expressions (guia de referência)
  2. ECMAScript, RegExp (especificação da linguagem)
  3. MDN, Unicode character class escape (\p{…})
  4. OWASP, Regular expression Denial of Service (ReDoS)
  5. Cloudflare, Details of the outage on July 2, 2019
  6. Google RE2, engine de regex sem backtracking (tempo linear)
  7. WHATWG HTML Standard, valid email address (violação intencional da RFC 5322)