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"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:
| Atalho | Equivale a | Negação |
|---|---|---|
| \d | [0-9] (dígito) | \D (não-dígito) |
| \w | [A-Za-z0-9_] (palavra, só ASCII) | \W (não-palavra) |
| \s | espaç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 acertaPor 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á.
| Forma | Comportamento | No 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 backtrackingA 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.
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"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.
| Sintaxe | Nome | Afirma 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"- 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.
- 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.
- 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.
- 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.
Ver os dados
| x | (a+)+, backtracking, 2^(n-1) | a+, linear, n passos |
|---|---|---|
| 2 | 2 | 2 |
| 4 | 8 | 4 |
| 6 | 32 | 6 |
| 8 | 128 | 8 |
| 10 | 512 | 10 |
| 12 | 2.048 | 12 |
| 14 | 8.192 | 14 |
| 16 | 32.768 | 16 |
| 18 | 131.072 | 18 |
| 20 | 524.288 | 20 |
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 +?
O que é backtracking catastrófico (ReDoS) e como evito em JavaScript?
Posso validar um e-mail com uma regex?
Por que \w não casa "ç" ou "é"?
O JavaScript tem lookbehind e grupos nomeados?
Como faço a regex parar de "comer" texto demais?
A mesma regex funciona em JavaScript e em Python/PHP?
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
- MDN, Regular expressions (guia de referência)
- ECMAScript, RegExp (especificação da linguagem)
- MDN, Unicode character class escape (\p{…})
- OWASP, Regular expression Denial of Service (ReDoS)
- Cloudflare, Details of the outage on July 2, 2019
- Google RE2, engine de regex sem backtracking (tempo linear)
- WHATWG HTML Standard, valid email address (violação intencional da RFC 5322)