Otimizando o uso de Expressões Regulares (Regex)
Como descrito em nossos Pilares Técnicos, nosso objetivo como time de Engineering da Axur é construir a tecnologia que permite a nossos produtos tornarem a internet mais segura. Para tal, existem etapas essenciais para prover maior segurança na Web, como o monitoramento e a inspeção de conteúdo na internet. É dessa forma que identificamos possíveis ameaças aos nossos clientes. Este artigo aborda expressões regulares (regex, do inglês Regular Expressions), uma entre tantas ferramentas que utilizamos para o combate a riscos digitais.
De mecanismos simples como comparação de strings até recursos mais complexos como o uso de Machine Learning, existe uma variedade de tecnologias e conhecimentos técnicos aplicados para processar a enorme quantidade de dados que coletamos na internet — tudo isso em tempo hábil e com custo computacional apropriado. Entre essas tecnologias, o uso de expressões regulares destaca-se como um forte aliado para executarmos buscas flexíveis em conteúdos diversos. Todavia, o emprego inapropriado de regex gera gargalos de processamentos, principalmente em um cenário de alto throughput de dados.
Aqui, discutiremos diferentes aspectos da utilização de expressões regulares, com benchmarks, otimizações e comparativos de resultados. Não será abordada a sintaxe das expressões. Este artigo também não tem o objetivo ser um manual exaustivo de todas as otimizações possíveis, e sim, mostrar alguns pontos importantes ao se trabalhar com regex.
Compile uma única vez
Linguagens de programação geralmente possuem suporte built-in ao uso de regex. Esse uso pode ser classificado em duas etapas principais: compilação e matching.
Na compilação, a expressão é transformada em uma sequência de instruções internas que serão usadas por um motor de correspondência (regex/matching engine). Para o matching, um determinado conjunto de caracteres é comparado com esse padrão compilado para verificar equivalência. Em Python, por exemplo, o motor desenvolvido em C interpreta bytecodes gerados.
Aqui temos exemplos em diferentes linguagens de programação do uso da compilação e matching. Neste caso, o código busca a palavra "internet" na frase "a Axur torna a internet mais segura".
Dependendo da linguagem de programação, existem outros meios de verificar a palavra na frase com outras sintaxes, objetos, funções, classes e métodos. Todavia, o ponto é que, devido ao custo computacional da compilação da regex, deve-se evitar a necessidade de recompilá-la toda vez que há uma verificação de matching. Para demonstrar os efeitos da compilação, veja o código a seguir:
Este código gera 10.000 expressões regulares combinando palavras-chave (keywords). Então, são verificados se os padrões das expressões têm correspondência com 1.000 conteúdos randômicos. A ideia deste algoritmo é simular uma ampla gama de conteúdos diferentes testados com uma grande variedade de regexes para matching.
Para o benchmark são usados dois métodos distintos: um que compila a regex em toda verificação de match da expressão com o conteúdo (método 1) e outro que compila uma única vez e salva em memória o padrão compilado para reuso (método 2). Ou seja, o segundo método usa um mecanismo simples de cache. O resultado da execução do código é o seguinte:
Cada execução pode gerar resultados diferentes de acordo com o ambiente utilizado. Entretanto, independentemente disso, existe diferença significativa entre os métodos. Nesse ambiente, com uso de caching, a velocidade de processamento foi 66% maior.
Fique atento! De acordo com a linguagem utilizada, este mecanismo pode estar embutido de alguma forma. Em Python, a operação de compiling usa cache em memória. O trecho a seguir foi retirado da Python Standard Library, mais especificamente do arquivo "re.py":
Código do re.py
É importante lembrar: há um trade-off entre velocidade de processamento e uso de memória , pois a cache irá gastar invariavelmente mais recursos de memória.
Tamanho do conteúdo e complexidade das expressões
A velocidade de processamento de matching de uma regex depende de ao menos dois fatores: complexidade da expressão e tamanho do conteúdo em que é feita a busca do padrão da expressão. Para demonstrar o impacto dessas duas variáveis, temos o seguinte benchmark.
Os resultados da execução do código foram os seguintes:
* os resultados variam a cada execução, mas eles mantêm sempre a notável diferença entre os tempos para as diferentes combinações de regexes e conteúdos.
Pelos resultados, há impacto significativo de ambas as variáveis. Para dois conteúdos idênticos, com uma regex mais complexa entre um conteúdo e outro, o tempo aumentou em mais de 6 vezes. Já para duas regexes idênticas o tempo cresceu em mais de 160 vezes, com o aumento do conteúdo entre uma comparação e outra.
Assim, fica clara a importância de verificar a complexidade da expressão. Ademais, é interessante identificar se o conteúdo pode ser reduzido antes da comparação. Por exemplo, se for desejado buscar algum texto em página Web pode ser que faça sentido aplicar a regex apenas no texto visível e não em todo o HTML da página.
Otimização nas expressões
Um ponto importante é entender como funcionam regexes e suas sintaxes na linguagem de programação escolhida pelo desenvolvedor. O mau uso da sintaxe pode gerar duas expressões regulares equivalentes em matching de padrões, mas com desempenhos completamente distintos. Isso pode ser facilmente verificado no benchmark abaixo:
O resultado, em tempo para processamento, é absurdamente diferente :
A otimização teve êxito: as expressões são equivalentes em matching de conteúdos e o tempo de processamento foi reduzido drasticamente em um dos casos. Com apenas a substituição das sequências de .?
pelo uso de quantificadores {n,m}
, notou-se aumento na velocidade de processamento em dezenas de milhares de vezes.
Por que isso ocorre? Java utiliza autômatos finitos não determinísticos — como pode ser visto aqui — para resolver as expressões. O algoritmo usa o mecanismo de backtracking, testa todas as expansões da expressão regular e aceita a primeira correspondência encontrada. Por conta das sequências de .?
, existe uma explosão de caminhos que devem ser verificados, prejudicando o desempenho.
Em outras implementações algorítmicas esses resultados podem ser bem diferentes. Por este motivo, é importante conhecer a implementação utilizada e realizar validações nas regexes criadas. Para o uso da regex padrão do Java, o impacto da não otimização da regex é enorme e pode gerar gargalos significativos em um sistema. No código equivalente em Python também há diferença nos tempos de processamento entre as expressões, porém o tempo total de execução da aplicação foi muito menor que em Java. Isso não é um problema da linguagem, apenas que neste cenário , o matching engine do Java foi menos eficiente que o motor usado por Python.
Só use se necessário
Sempre verifique se a utilização da expressão regular faz sentido em determinado contexto. Muitas vezes podemos substituí-la por outros recursos da linguagem que melhoram o desempenho do código e o tornam mais legível. Vejamos o exemplo a seguir:
Neste código, procura-se o primeiro nome "João" em uma lista de nomes. O resultado apresenta variações de desempenho entre os conteúdos. Houve melhor desempenho no uso do método startsWith
em vez de regex no teste realizado.
A performance pode ser irrelevante para um cenário simples como esse. Entretanto, o método startsWith
também deixa mais clara a intenção do código que a expressão regular. Isso deve ser considerado na solução, afinal, um código é muito mais lido que escrito.
Conclusão
Neste artigo mostramos testes que comprovam o impacto da compilação e sintaxe das expressões. Também ficam claros os efeitos das otimizações realizadas e a diferença entre abordagens com e sem regex. Demonstramos aqui a importância de testar e validar diferentes expressões, entender o contexto de aplicação da ferramenta e como algumas otimizações ajudam na construção de programas melhores. Em sistemas que processam grandes volumes de dados, esses cuidados podem fazer a diferença para garantir um bom desempenho e evitar gargalos.