<.+> 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.
| |
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.
| |
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.
| |
| |
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.
| |
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.
| |
| |
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:
Elimine a ambiguidade.
(a+)+bea+bcasam 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.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
redo Python suporta os dois desde a 3.11:1 2 3 4 5 6 7pattern = 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 328 0.002 ms 1000 0.001 ms 100000 0.026 msA mesma correspondência falha em 100.000 caracteres leva 26 microssegundos. O
RegExpnativo do JavaScript não tem nenhum dos dois recursos. O pacote npmregexadiciona grupos atômicos e quantificadores possessivos como sintaxe estendida se você precisar deles sem trocar de motor.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.