<.+> contra <b>bold</b> and <i>italic</i> não encontra <b>. Encontra a string inteira, do primeiro < ao último >. Nada está quebrado. É exatamente isso que um quantificador greedy deve fazer, e também é o bug mais comum de “o regex está casando demais”. Depois que você entende o que o motor faz enquanto lê o seu padrão, esse bug e a maioria dos parentes dele deixam de ser mistério.

Como um Motor de Regex Lê o Seu Padrão

Um motor de regex não compara o seu padrão com a string como você compara duas strings por igualdade. Os motores que você usa no dia a dia (re do Python, RegExp do JavaScript, PCRE, .NET, Java) são regex-directed: eles percorrem o padrão da esquerda para a direita, token por token, e para cada token tentam consumir caracteres da string na posição atual. Quando um token casa, o motor avança tanto o ponteiro do padrão quanto o da string. Quando um token falha, o motor faz backtracking: volta ao último ponto em que tinha uma escolha, como um quantificador que poderia ter casado com menos caracteres ou um ramo de alternância ainda não tentado, e tenta a outra opção.

É por isso que <.+> engole a linha inteira. .+ é greedy, então primeiro ele agarra todos os caracteres restantes da string, < e > incluídos. O motor ainda precisa de um > literal para terminar, então ele devolve caracteres um por um a partir do final até que o último > da string o satisfaça. O motor retorna a primeira correspondência que encontra nessa ordem greedy-first. Para esse padrão, é a mais longa possível, não a que parece certa à primeira vista.

Tentar primeiro a opção greedy e voltar um passo ao falhar: essa única regra explica tanto o casamento normal do dia a dia quanto as lentidões patológicas mais adiante. É a mesma dinâmica de explorar e desfazer de uma busca recursiva sem memoização, o tipo de explosão que recursão explicada cobre do lado das chamadas de função.

Sintaxe Básica de Regex: Âncoras, Quantificadores, Grupos, Lookarounds

Âncoras e Classes de Caracteres

^ e $ ancoram uma correspondência ao início e ao fim da string (ou da linha, com a flag multiline). \d, \w e \s são abreviações para as classes de caracteres dígito, caractere de palavra e espaço, e [...] permite escrever uma classe diretamente.

1
2
3
4
import re

re.search(r"^\d{3}-\d{4}$", "555-0199")
# <re.Match object; span=(0, 8), match='555-0199'>

Sem as âncoras, \d{3}-\d{4} também casaria dentro de "call 555-0199 now". As âncoras transformam “contém” em “é exatamente”.

As classes de caracteres operam sobre o que o motor considera um caractere, e fora do ASCII puro isso nem sempre é o que você espera. \w casa com uma letra, um dígito ou um underscore de acordo com a noção de caractere do motor, que é a mesma distinção code point versus byte tratada em o que são Unicode e a codificação UTF-8. Execute um padrão sobre bytes UTF-8 brutos em vez de texto decodificado e um caractere multibyte acaba dividido em pedaços que o motor trata como caracteres separados.

Quantificadores Greedy e Lazy

+, * e {m,n} são greedy por padrão: pegam o máximo que conseguem, depois fazem backtracking se isso quebrar o resto do padrão. Coloque um ? depois de qualquer um deles e o quantificador vira lazy. Ele pega o mínimo possível e só se expande quando é obrigado.

1
2
3
s = "<b>bold</b> and <i>italic</i>"
re.findall(r"<.+>", s)    # ['<b>bold</b> and <i>italic</i>']
re.findall(r"<.+?>", s)   # ['<b>', '</b>', '<i>', '</i>']

Um caractere de diferença, um resultado completamente diferente. Quando você quiser a coisa mais curta que satisfaça o resto do padrão, use a forma lazy. Se o que você está casando é markup, leia a última seção primeiro.

Grupos de Captura, Sem Captura e Nomeados

(...) captura o que casa para que você possa extrair depois. (?:...) agrupa por estrutura, por exemplo para aplicar um quantificador a um subpadrão inteiro, sem criar uma captura. (?P<name>...) no Python ((?<name>...) no JavaScript, Java e .NET) nomeia um grupo para que você não precise contar parênteses para encontrá-lo depois.

1
2
3
m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", "2026-10-28")
m.groupdict()
# {'year': '2026', 'month': '10', 'day': '28'}
1
2
3
const m = "2026-10-28".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
m.groups;
// [Object: null prototype] { year: '2026', month: '10', day: '28' }

Você lê o resultado pelo nome, com groupdict() no Python ou .groups no JavaScript (os grupos nomeados chegaram lá no ES2018), em vez de match[1], match[2]. Assim que um padrão tem mais de dois grupos, os caracteres extras se pagam.

Lookahead e Lookbehind

Um lookaround verifica uma posição, não caracteres. Ele checa se algo está (ou não está) presente sem consumi-lo. (?=...) é um lookahead positivo, (?!...) negativo; (?<=...) e (?<!...) são os equivalentes de lookbehind.

1
2
"$40 bill".match(/(?<=\$)\d+/);
// [ '40', index: 1, ... ]   -- encontra "40", não "$40"

O re do Python exige que o lookbehind tenha largura fixa: (?<=\$) funciona, (?<=\$+) gera um erro, porque o motor precisa saber exatamente quanto voltar antes de checar. O JavaScript, que adicionou lookbehind no ES2018 junto com os grupos nomeados, não tem essa limitação, então (?<=\$+) funciona lá. No Python, o pacote de terceiros regex no PyPI remove a restrição se você precisar de um lookbehind de largura variável.

Por Que Alguns Regex Travam: Catastrophic Backtracking

Pegue a regra de backtracking do início e dê a ela um caso do qual não consiga se recuperar. Encaixe dois quantificadores de forma que o motor tenha muitas maneiras de dividir a mesma sequência de caracteres entre eles, e então faça a correspondência falhar bem no final. O motor não falha rápido. Ele tenta cada divisão antes de desistir.

1
2
3
4
5
6
7
8
import re, time

pattern = re.compile(r"(a+)+b")
for n in (20, 24, 26, 28):
    s = "a" * n  # sem "b" no final, então nunca casa
    start = time.perf_counter()
    pattern.match(s)
    print(n, f"{(time.perf_counter() - start) * 1000:.1f} ms")
1
2
3
4
20 41.6 ms
24 525.8 ms
26 2083.6 ms
28 8369.3 ms

Cada a extra praticamente dobra o tempo: dois caracteres mais, quatro vezes mais lento. Isso é crescimento exponencial, a curva que a notação Big O coloca do outro lado de O(n^2). (a+)+ pode dividir uma sequência de 28 a em grupos de 2^27 maneiras diferentes, e sem nenhum b na string o motor tenta uma atrás da outra antes de conseguir reportar “sem correspondência”. Siga a mesma curva até 40 caracteres e a espera é de cerca de nove horas.

Quando a entrada vem de um usuário em vez dos seus fixtures de teste, isso é uma vulnerabilidade explorável: ReDoS, regular expression denial of service. O gatilho usual é exatamente essa forma, dois quantificadores empilhados sem nada entre eles que force uma divisão única.

Três maneiras de evitar isso, da mais barata:

  1. Elimine a ambiguidade. (a+)+b e a+b casam com as mesmas strings válidas aqui; o + externo não traz nada. A maioria dos padrões catastróficos tem uma camada redundante assim, uma vez que você procura por ela.

  2. Torne o quantificador interno possessivo, ou envolva-o em um grupo atômico. Ambos dizem ao motor “uma vez que isso casar, não faça backtracking dentro”. Isso elimina a explosão exponencial, ao custo de ocasionalmente rejeitar uma string que a versão com backtracking teria aceitado. O re do Python suporta os dois desde a 3.11:

    1
    2
    3
    4
    5
    6
    7
    
    pattern = re.compile(r"(?>a+)+b")   # grupo atômico
    # ou: re.compile(r"a++b")           # quantificador possessivo, Python 3.11+
    for n in (28, 1_000, 100_000):
        s = "a" * n
        start = time.perf_counter()
        pattern.match(s)
        print(n, f"{(time.perf_counter() - start) * 1000:.3f} ms")
    
    1
    2
    3
    
    28 0.002 ms
    1000 0.001 ms
    100000 0.026 ms
    

    A mesma correspondência falha em 100.000 caracteres leva 26 microssegundos. O RegExp nativo do JavaScript não tem nenhum dos dois recursos. O pacote npm regex adiciona grupos atômicos e quantificadores possessivos como sintaxe estendida se você precisar deles sem trocar de motor.

  3. Limite quantificadores sem limite. {1,50} em vez de + limita o quão ruim o pior caso pode ficar, mesmo sem reescrever a lógica.

Se a string pode vir de fora do seu código, como o corpo de uma requisição ou um valor de configuração escrito por outra pessoa, trate quantificadores encaixados como trataria um loop sem limite sobre entrada externa. Teste o padrão contra uma sequência longa do caractere que dispara a ambiguidade, não só contra os seus fixtures do caminho feliz.

Quando Não Usar Regex

Um regex não tem memória de quão profundo está. Isso é bom para texto plano e de forma fixa, como uma data ou uma linha de log com campos conhecidos. É também por isso que <.+> não consegue pular de forma confiável uma única tag HTML: ele não sabe que esse < abre uma tag que o próximo > correspondente fecha, só sabe “pegar até o último > que encontrar”. Estruturas encaixadas como HTML, JSON, código-fonte e parênteses balanceados precisam de uma gramática, não de um padrão. A falha típica é um regex que passa em todos os casos de teste que você escreveu, e depois encontra um arquivo real com um comentário contendo um > e silenciosamente casa com o trecho errado em vez de gerar um erro.

Use o parser que já existe: um parser HTML para HTML, json.loads para JSON, o módulo AST da sua linguagem para código. Reserve o regex para validar ou extrair texto de forma plana, e troque para um parser no momento em que “balanceado” ou “encaixado” aparecer na descrição do que você está casando.

Artigos relacionados