Python Iluminado

Expressões Regulares

Expressões regulares são um mecanismo muito poderoso para manipulação de strings, também conhecido como regex ou regexp, é um campo de estudo muito abrangente na ciência da computação teórica e linguagens formais.

Pode-se dizer que é uma sequência de caracteres que define um padrão de busca, normalmente este padrão é usado por algoritmos de busca de strings, onde é feito uma busca por determinado padrão, seja para encontrá-lo ou até mesmo encontrá-lo e alterá-lo, também é muito usado para validação de input de dados.

O conceito surgiu na década de 1950, quando o matemático americano Stephen Cole Kleene formalizou a descrição de uma linguagem regular. O conceito entrou em uso comum com utilitários de processamento de texto do Unix. Diferentes sintaxes para escrever expressões regulares existem desde a década de 1980, sendo uma o padrão POSIX e outra, amplamente utilizada, a sintaxe Perl.

Expressões regulares são usadas em search engines, search and replace dialogs de processadores de texto e editores de texto, em utilitários de processamento de texto como sed e AWK e em análise lexical. Muitas linguagens de programação fornecem recursos de regex integrados ou por meio de bibliotecas, como é o caso da linguagem Python que nos oferece a biblioteca re.

Introdução

Uma expressão regular, geralmente chamada de padrão, especifica um conjunto de strings necessárias para um propósito específico. Uma maneira simples de especificar um conjunto finito de strings é listar seus elementos ou membros. No entanto, muitas vezes existem maneiras mais concisas, por exemplo, o conjunto contendo as três cadeias de caracteres "Handel", "Händel" e "Haendel" pode ser especificado pelo padrão H(ä|ae?)Ndel, dizemos que esse padrão corresponde a cada uma das três strings. Na maioria dos formalismos, se existe pelo menos uma expressão regular que corresponde a um determinado conjunto, então existe um número infinito de outras expressões regulares que também correspondem a ela - a especificação não é única.

Âncoras: ^ e $

ExemploDescriçãoURL
^PyEncontra qualquer string que comece com Pyhttps://regex101.com/r/cO8lqs/4303
on$Encontra uma string que termina com onhttps://regex101.com/r/cO8lqs/4304
^Python$Encontra a string Pythonhttps://regex101.com/r/cO8lqs/4305
PythonEncontra qualquer string que tenha Python em sihttps://regex101.com/r/cO8lqs/23892

Quantificadores: *, +, ? e {}

ExemploDescrição
abc*Encontra uma string que tenha ab seguido por zero ou mais c
abc+Encontra uma string que tenha ab seguido por um ou mais c
abc?Encontra uma string que tenha ab seguido por zero, ou um c
abc{2}Encontra uma string que tenha ab seguido por dois c
abc{2,}Encontra uma string que tenha ab seguido por dois ou mais c
abc{2,5}Encontra uma string que tenha ab seguido por 2 até no máximo 5 c
a(bc)*Encontra uma string que tenha a seguido por zero ou mais cópias da sequência bc
a(bc){2,5}Encontra uma string que tenha a seguido por 2 até no máximo 5 cópias da sequência bc

Operador OU: | e []

ExemploDescrição
a(b|c)Encontra uma string que tenha a seguido por b ou c
a[bc]Igual o anterior, ambas as notações podem ser usadas e produzem o mesmo resultado

Classes de caracteres: \d, \w, \s e .

ExemploDescrição
\dEncontra um único caracter que seja um dígito
\wEncontra um caracter de palavra (caracter alfanumérico e underline)
\sEncontra caracteres de espaço em branco (incluindo tabs e quebra de linha)
.Encontra qualquer caracter

Negação de classe de caracteres: \D, \W, \S

ExemploDescrição
\DFaz a operação inversa de \d e traz todos os não-dígitos
\WFaz a operação inversa de \w e traz todas as não-palavras
\SFaz a operação inversa de \s e traz todos os não-espaços

Outro ponto importante, é caso queiramos dar escape, por exemplo validar um ., para isso usamos o \.

O mesmo vale caso seja necessário validarmos $, utilizamos $

Experimente mais opções de expressões em Regex101

Expressões Regulares em Python

Python já possui incluído o módulo re que utilizaremos para nossas expressões regulares, este módulo fornece operações de correspondência de expressões regulares semelhantes às encontradas em Perl.

Para usá-lo precisamos importá-lo, sem a necessidade de instalação, vamos também inspecionar o módulo para termos uma visão do que ele nos disponibiliza em termos de atributos e métodos.

import re
print(dir(re))
# ['A', 'ASCII', 'DEBUG', 'DOTALL', 'I', 'IGNORECASE', 'L', 'LOCALE', 'M', 'MULTILINE', 'S', 'Scanner', 'T', 'TEMPLATE', 'U', 'UNICODE', 'VERBOSE', 'X', '_MAXCACHE', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '__version__', '_alphanum_bytes', '_alphanum_str', '_cache', '_cache_repl', '_compile', '_compile_repl', '_expand', '_locale', '_pattern_type', '_pickle', '_subx', 'compile', 'copyreg', 'error', 'escape', 'findall', 'finditer', 'fullmatch', 'match', 'purge', 'search', 'split', 'sre_compile', 'sre_parse', 'sub', 'subn', 'sys', 'template']

Veja que temos bastantes opções para trabalharmos, inicialmente vamos ver os métodos mais relevantes através de diversos exemplos, mas primeiramente é importante entendermos o conceito de raw string, pois são elas que vamos utilizar em nossas expressões regulares.

Uma raw string do Python é uma string normal, prefixada com r ou R. Isso trata caracteres como barra invertida (\) como um caractere literal. Isso também significa que esse caractere não será tratado como um caractere de escape, por exemplo:

print('Hello\nWorld')
# Hello
# World

Veja que o output é tratado como esperado, o escape caracter \n é interpretado como uma quebra de linha, já no caso de uma raw string, ele será tratado como um caractere literal:

print(r'Hello\nWorld') # Hello\nWorld

Agora que sabemos como as raw strings funcionam, vamos ver os métodos de expressões regulares.

findall()

O método findall() retorna todas as porções correspondidas como uma lista de strings. Ele conta com os seguintes argumentos:

  • padrão
  • string
  • flags

O primeiro argumento é o padrão que desejamos testar e extrair em relação à string de input, que é o segundo argumento. O último argumento flags é opcional. Vamos então aos exemplos para uma melhor compreensão:

resultados = re.findall(r'ab*c', 'abc ac adc abbbc abbb')
print(resultados) # ['abc', 'ac', 'abbbc']

No exemplo acima usamos o quantificador *, que indica 0 ou mais b's.

resultados = re.findall(r'ab+c', 'abc ac adc abbbc abbb')
print(resultados) # ['abc', 'abbbc']

No exemplo acima usamos o quantificador +, que indica 1 ou mais b's.

Vejamos como podemos definir uma expressão regular para encontrar caracteres de palavras:

import re
x = "Expressões regulares são legais"
r = re.findall(r"\w+", x)
print(r) # ['Expressões', 'regulares', 'são', 'legais']

Como podemos ver, todos os caracteres da sentença foram correspondidos e recebemos uma lista deles.

split()

O método split() divida a string pelas ocorrências do padrão.

import re
e = "Eu sou uma expressao com diversas palavras"
r = re.split(r'\s', e)
print(r) # ['Eu', 'sou', 'uma', 'expressao', 'com', 'diversas', 'palavras']

Como podemos ver, a função split() separou a expressão em uma lista de palavras, usando o separador \s que representa o espaço em branco.

Vamos agora usar um dígito como separador:

string = 'um:1 dois:2 quinze:15 trinta:30'
padrao = r'\d+'
resultado = re.split(padrao, string)
print(resultado) # ['um:', ' dois:', ' quinze:', ' trinta:', '']

Normalmente, usamos o operador in para testar se uma string faz parte de outra string ou não. Para expressões regulares, usamos a função search() cuja lista de argumentos é mostrada abaixo:

  • padrão
  • string
  • flags

O primeiro argumento é o padrão de expressão regular que você deseja testar contra a string de input, que é o segundo argumento, flags é opcional, ajuda a alterar o comportamento padrão dos padrões de expressões regulares.

Como uma boa prática, é interessante usar raw strings para construir o padrão de expressão regular. Vejamos então alguns exemplos:

import re
sentenca = "esta é uma string"
print('esta' in sentenca) # True
print('este' in sentenca) # False
print(bool(re.search(r'esta',sentenca))) # True
print(bool(re.search(r'este',sentenca))) # False
print(type(re.search(r'esta',sentenca))) # <class 're.Match'>
print(type(re.search(r'este',sentenca))) # <class 'NoneType'>

O valor de retorno da função search() é um objeto re.Match quando há uma correspondência, caso contrário o resultado é NoneType.

Vejamos agora um exemplo usando o argumento flags:

print(bool(re.search(r'Esta',sentenca))) # False
print(bool(re.search(r'Esta',sentenca, flags=re.I))) # True

A flag re.I significa IGNORECASE é uma flag para permitir correspondência sem distinção entre maiúsculas e minúsculas.

Vejamos agora um exemplo com expressões geradoras:

palavras = ['cachorro','macarrão','macaco']
[p for p in palavras if re.search(r'rr',p)] # ['cachorro', 'macarrão']
all(re.search(r'ca',p) for p in palavras) # True
any(re.search(r'xa',p) for p in palavras) # False

Vejamos agora como podemos utilizar um quantificador em nossa padrão:

import re
padrao = "a*b"
r = re.search(padrao, "aaaaaaabcedefg")
print(r) # <_sre.SRE_Match object; span=(0, 8), match='aaaaaaab'>
print(r.group()) # aaaaaaab

Como podemos ver, primeiro nos foi retornado um objeto match, posteriormente acessamos a string encontrada através do método group() que nos traz o resultado, também podemos aplicar métodos como start(), end() e span().

sub()

Para busca e substituição normais, podemos usar o método str.replace(). Para expressões regulares, usamos a função sub(), cuja lista de argumentos é mostrada abaixo:

  • padrão
  • substituição
  • string
  • count
  • flags

O primeiro argumento é o padrão de expressão regular a ser correspondido em relação à string de input, que é o terceiro argumento. O segundo argumento específica a string que irá substituir as porções correspondidas pelo padrão da expressão regular. Os últimos dois argumentos são opcionais. Para entendermos melhor este conceito, vamos ao exemplo:

frase = 'eu gosto de pizza'
print(re.sub(r'z','Z',frase)) # eu gosto de piZZa
print(re.sub(r'z','Z',frase,count=1)) # eu gosto de piZza

Observe que o argumento count limita o número de substituições.

Compilando Expressões Regulares

Expressões Regulares podem ser compiladas usando a função compile(), que nos retorna um objeto re.Pattern.

As funções do módulo re de nível superior estão todas disponíveis como métodos para tais objetos. Compilando uma expressão regular é útil se ela tiver que ser usada em vários lugares ou chamada em vários vezes dentro de um loop (benefício de velocidade).

Vamos começar compilando uma expressão regular:

animal = re.compile(r'gato')
print(type(animal)) # <class 're.Pattern'>

Usaremos agora o o método search() no padrão definido por nós para buscar pelo padrão em uma determinada string:

bool(animal.search('o gato é bonito')) # True
bool(animal.search('o rato é roedor')) # False

Também podemos usar o método sub neste padrão, de forma a executar substituções:

animal.sub('rato','o gato fugiu do cachorro')
# 'o rato fugiu do cachorro'

match()

O método match() funciona da seguinte forma: Se zero ou mais caracteres no início da string corresponderem a expressão regular informada, retorna um objeto de re.Match correspondente. Retorna None se a string não corresponder ao padrão.

Neste exemplo vamos usar um padrão simples para darmos match em um email:

import re
padrao = r"\w+@(\w+\.)+(com|org|net)"
r = re.match(padrao,"teste@teste.org")
print(r) # <re.Match object; span=(0, 15), match='teste@teste.org'>
print(r.group()) # teste@teste.org

Vimos que nesse caso, foi possível dar match no e-mail teste@teste.org, nos mostrando assim a relevância e importância das expressões regulares dentro da computação, um assunto relevante e de compreensão necessária, pois está presente em praticamente todas as linguagens de programação e sua aplicabilidade é muito vasta.