Testador de regex
Teste um padrão em texto real, com correspondências destacadas e parada rígida em backtracking descontrolado.
Porque é que um testador de regex precisa de um worker
Quase toda a maquinaria desta página existe por causa de um único modo de falha. Considere /(a+)+b/ aplicado a trinta a e nenhum b. O motor tem de decidir como dividir esses trinta caracteres entre o quantificador interior e o exterior, e há 2³⁰ maneiras de o fazer. Todas são tentadas antes de a correspondência poder ser declarada impossível.
O ponto importante é que uma expressão regular em execução não pode ser interrompida. O JavaScript é de fio único, portanto uma guarda com setTimeout não dispara enquanto a expressão não regressar — o que, para aquela entrada, é algures depois da morte térmica do universo. O separador está perdido.
Por isso o padrão corre num Web Worker, no seu próprio fio, e a página principal segura um temporizador. Quando o temporizador ganha, o worker.terminate() mata o fio sem cerimónias. É o único mecanismo que realmente funciona, e é por isso que este testador lhe diz que o padrão é perigoso em vez de se tornar um separador pendurado.
Vale a pena interiorizar isto, porque o mesmo padrão em código de produção é uma negação de serviço: entrada fornecida pelo utilizador comparada com uma expressão vulnerável, e um único pedido prende um núcleo do processador por tempo indeterminado. A esta classe de defeitos chama-se ReDoS.
As formas a evitar
| Perigoso | Porquê | Mais seguro |
|---|---|---|
(a+)+ | Quantificadores aninhados — divisões exponenciais | a+ |
(\w|\s)* | Alternância dentro de um quantificador, com ramos sobrepostos | [\w\s]* |
(\d+)*$ | Grupo quantificado com uma âncora que obriga à exploração completa | \d*$ |
.*.*= | Dois caracteres universais sem limite a disputar o mesmo texto | [^=]*= |
O denominador comum é a ambiguidade: haver mais do que uma maneira de o padrão consumir os mesmos caracteres. Uma classe de caracteres faz o trabalho de uma alternância sem a ambiguidade, e é por isso que a coluna do «mais seguro» é, no essencial, substituir | por [].
Ganancioso, preguiçoso, e o defeito que toda a gente escreve uma vez
Os quantificadores são gananciosos por omissão: .+ agarra tudo o que consegue e só devolve caracteres a contragosto. Contra <b>bold</b>:
| Padrão | Corresponde a |
|---|---|
<.+> | <b>bold</b> — a coisa toda |
<.+?> | <b> — preguiçoso, pára no primeiro > |
<[^>]+> | <b> — e mais depressa, porque não há nada para recuar |
É à terceira forma que se deve deitar a mão. Uma classe de caracteres negada não consegue passar do ponto, portanto não há retrocesso a desfazer — é ao mesmo tempo mais clara quanto à intenção e mais barata a correr do que a versão preguiçosa.
lastIndex, e porque desaparece uma correspondência sim, outra não
Um RegExp com a bandeira g guarda estado. Mantém um lastIndex que avança depois de cada exec ou test, portanto reutilizar o mesmo objecto em chamadas separadas retoma onde tiver parado:
const re = /\d+/g;
re.test('123'); // true, lastIndex is now 3
re.test('123'); // false! it resumed from index 3Crie a expressão dentro do ciclo, reponha lastIndex = 0, ou use matchAll, que trata disso por si. O testador acima cria um objecto novo em cada execução, portanto o que vê aqui é o comportamento da primeira chamada.
Onde o regex de JavaScript difere
Os resultados aqui coincidem exactamente com o Node e com código de navegador, porque é o mesmo motor. Face a outras linguagens, as diferenças que apanham as pessoas são estas:
- Sem grupos atómicos nem quantificadores possessivos. O PCRE tem
(?>...)ea++para impedir o retrocesso de todo; o JavaScript não tem nenhum dos dois, e é por isso que aqui é mais fácil escrever um ReDoS. - Sem recursão. Não é possível casar parênteses equilibrados com regex de JavaScript. Se o requisito é esse, precisa de um analisador sintáctico.
- O lookbehind chegou tarde. O
(?<=...)é suportado nos navegadores actuais mas não no Safari mais antigo, portanto verifique os seus alvos. - O RE2 do Go é outra máquina. Tempo linear garantido, sem retro-referências, sem lookaround. Se alguma vez se interrogou sobre porque é que o Go deixou funcionalidades de fora, esta página é a razão.
Relacionado
Se o texto contra o qual está a comparar for JSON, formatá-lo primeiro costuma tornar o padrão desnecessário — sobre dados estruturados, um analisador ganha sempre a uma expressão regular. Se se trata de agendar em vez de comparar, a ferramenta vizinha é o analisador de cron.
Perguntas sobre expressões regulares
Porque é que o meu padrão foi interrompido ao fim de dois segundos?
Porque estava a fazer retrocesso catastrófico. Certas formas — quantificadores aninhados como (a+)+ ou (\w|\s)* — levam o motor a tentar um número exponencial de maneiras de corresponder, ao ponto de uma cadeia de quarenta caracteres demorar mais do que a idade do universo. Não há forma de interromper uma expressão regular já em execução a partir de JavaScript, e é por isso que aqui o padrão corre num Web Worker: o worker pode ser terminado e a página continua a responder. Um tempo-limite na linha principal só dispararia depois de a expressão acabar, o que para esses padrões é nunca.
Isto usa o mesmo motor de expressões regulares que o meu código?
Usa o do seu navegador, ou seja o motor de JavaScript — por isso os resultados coincidem exactamente com o Node e com código de navegador. Nem sempre coincidirão com PCRE, Python, Go ou Java. O JavaScript não tem grupos atómicos, nem quantificadores possessivos, nem recursão; o lookbehind existe nos navegadores actuais, mas chegou tarde. O RE2 do Go é um desenho completamente diferente e não tem retro-referências de propósito, que é justamente como evita o problema de retrocesso acima.
Porque é que a minha expressão global só encontra uma correspondência sim, outra não?
Um objecto RegExp com a bandeira g guarda um lastIndex que avança a cada chamada, por isso reutilizar o mesmo objecto em chamadas separadas a exec ou test retoma a partir de onde ficou. É uma das partes mais fiavelmente confusas desta API. Ou cria a expressão de novo de cada vez, ou repõe lastIndex a 0 antes de usar, ou usa matchAll, que trata disso por si.
Qual é a diferença entre um quantificador ganancioso e um preguiçoso?
O ganancioso agarra tudo o que pode e só devolve caracteres quando é obrigado; o preguiçoso (escrito com um ? no fim) agarra o mínimo possível e só se estende quando não há alternativa. Contra <b>bold</b>, o padrão <.+> corresponde à cadeia inteira, porque .+ engole tudo e recua apenas o suficiente para encontrar um > final. Escrever <.+?> corresponde só a <b>. É a razão mais comum para um padrão apanhar muito mais do que se pretendia.
É seguro colar dados reais aqui?
É. O padrão e o texto são entregues a um worker dentro desta página e avaliados aí. Nada é enviado para um servidor — e é isso mesmo que torna possível cancelar um padrão descontrolado, já que não há pedido nenhum à espera de resposta.
Última revisão . Encontrou algo desactualizado? Diga-nos.
