Expressões regulares, também chamadas de regex (de regular expression), são meios de descrever padrões que podem ser encontrados dentro de um texto. Os padrões podem ser simples como a busca de um ou dois dígitos especificados, ou padrões complexos que incluem a posição do padrão no texto, o número de repetições, etc.
Regex são usados basicamente para a busca de um padrão, substituição de texto, validação de formatos e filtragem de informações. Praticamente todas as linguagens de programação possuem ferramentas de uso de regex, assim como grande parte dos editores de texto. As diferentes linguagens de programação e aplicativos que incorporam funcionalidade de regex possuem sintaxes ligeiramente diferentes, cada uma, mas há uma embasamento geral que serve a todas. Existem aplicativos de teste de regex em diversas plataformas e aplicativos online para o mesmo fim.
Uma descrição das expressões regulares e seu uso estão no artigo Expressões Regulares (regex) deste site.
Módulo re, expressões regulares
No Python o módulo re, parte da biblioteca padrão, carrega uma “mini-linguagem” com meios de especificar tais padrões e realizar essas buscas e sibstituições.
O módulo possui os métodos que podem ser usados para encontrar padrões, partir texto e compilar padrões:
Método | retorna |
---|---|
re.search(padrao, texto) |
1º texto casado e sua posição, em todas as linhas, |
re.match(padrao, texto) |
1º texto casado e sua posição, na 1ª linha, |
re.findall(padrao, texto) |
retorna uma lista com todos os trechos encontrados, |
re.split(padrao, texto) |
parte o texto na ocorrência do padrão e retorna as partes, |
re.sub(padrao, sub, texto) |
substitue em texto o padrao por sub, |
re.subn(padrao, texto) |
similar à sub mas retorna tupla com nova string e número de substituições |
re.compile(padrao) |
compila e retorna um padrão regex pre-compilado |
Método re.search
resultado = re.search(padrao, texto)
O método search
procura por um padrão dentro de um texto alvo e retorna um objeto re.Match que contém a posição inicial e final do padrão encontrado. Se o padrão não for encontrado o método retorna None.
O objeto re.Match possui o método group() que retorna o trecho encontrado, sendo que apenas a primeira ocorrência é considerada. Os parâmetros são padrao, uma construção regex, e texto, o conjunto de caracteres onde se busca o padrão. Em um texto de muitas linhas search
procura em todas as linhas até encontrar o padrão, diferente do método match
que procura apenas na primeira linha.
Uma letra, dígito ou conjunto de caracteres é casado literalmente. Se não encontrado None é retornado.
» import re » texto = 'Este é um texto de teste para testar o funcionamento das expressões regulares' » # procuramos por 'exp' no texto » padrao = 'exp' » resultado = re.search(padrao, texto) » # o padrão 'exp' é encontado na posição 57 até 60 » print(resultado) ↳ <re.Match object; span=(57, 60), match='exp'> » print(resultado.group()) ↳ exp » # a busca retorna None se o padrão não é encontrado » print(re.search('z', texto)) ↳ None » # apenas o primeira coincidência é casada » print(re.search('ste', texto)) ↳ <re.Match object; span=(1, 4), match='ste'>
Como search() retorna None se não houver um casamento, podemos usar o retorno do método como critério de sucesso da busca. O padrão A{3} significa 3 letras A maiúsculas consecutivas, o que não existe no texto.
» texto = 'American Automobile Association' » busca = re.search('A{3}', texto) » if busca: » print(busca.group()) » else: » print('não encontrado') ↳ não encontrado
Além de caracteres simples e grupos de caracteres os metacaracteres permitem ampliar o poder de busca das regex. Os textos casados são representados como em texto. Na tabela abaixo x representa um padrão qualquer.
padrão | significado | exemplo: | casa ou não com |
---|---|---|---|
a |
caracter comum | a |
casa Afazer aaa |
ab |
grupos de caracteres comuns | ab |
absoluto abraço Abcd trabalho |
. |
casa com qualquer caracteres único | m.to |
mato, mito, m3to |
x* |
0, 1 ou várias ocorrências de x | 13* |
1, 13456, 133, 13333-0987 |
x? |
0, 1 ocorrência de x | 13? |
1, 13456, 133, 13333-0987 |
x+ |
1 ou mais ocorrências de x | 13+ |
1, 13456, 133, 13333-0987 |
» # . = qualquer caracter » print(re.search('p.ata', 'pirata pata prata').group()) ↳ prata » # x* = 0, 1 ou várias repetições de x » print(re.search('jo*e', 'jose joo joe').group()) ↳ joe » print(re.search('jo*e', 'jose joo jooooooe').group()) ↳ jooooooe » # x? = 0 ou 1 ocorrência de x » print(re.search('jo?e', 'jose jooe je').group()) ↳ je » print(re.search('jo?e', 'jose jooe joe').group()) ↳ joe » # x+ = 1 ou mais ocorrências de x » print(re.search('jo+e', 'jose jooe joe').group()) ↳ jooe
As chaves são usadas para quantificar repetições de um padrão.
padrão | significado | exemplo: | casa ou não com |
---|---|---|---|
{n} |
significa exatamente n repetições do padrão | 9{3} | 999, 1999-45, 9-999, 999-00, 9, 99 |
{n,} |
mínimo de n repetições do padrão | 9{2,} | 99, 1999-45, 9-9999, 99999-00, 9, 9-9 |
{n,m} |
mínimo de n, máximo de m repetições do padrão | 9{2,4} | 99, 1999-45, 9-9999, 99999-00, 9, 9-9 |
O objeto re.Match possui diversos métodos:
Método | retorna |
---|---|
match.group() |
a parte do texto casada com o padrão, |
match.start() |
índice do início da parte do texto casada com o padrão, |
match.end() |
índice do fim da parte do texto casada com o padrão, |
match.span() |
os índices do início e do fim da parte do texto casada com o padrão, |
match.re() |
a expressão regular casada (o padrão), |
match.string() |
o texto passado como parâmetro. |
» texto = 'Telefone: 05 (61) 3940-35356 (casa da Dinda), CEP: 123456789' » # 4 digitos, hifen, 5 dígitos » padrao = '\d{4}-\d{5}' » # a variável resultado contém um objeto Match » resultado = re.search(pattern, texto) » if resultado: » print(resultado.group()) » print(resultado.start()) » print(resultado.end()) » print(resultado.span()) » else: » print('Padrão não encontrado!') ↳ 3940-35356 ↳ 18 ↳ 28 ↳ (18, 28) » texto = 'CEP do cliente: 72715-620, DF' » busca = re.search('\d+', texto) » print(busca.start(), busca.end()) ↳ 16 21 » # o primeiro trecho casado é retornado » print(texto[busca.start(): busca.end()], '=', busca.group()) ↳ 72715 = 72715 » busca = re.search('-\d+', texto) » print(busca.group()) ↳ -620
match.group(), que é o mesmo que match.group(0), se refere a todos os grupos encontrados. Se o padrão contém apenas um grupo só uma combinação é encontrada. Podemos construir padrões com mais de um grupos usando os marcadores de grupos, os parênteses ()
.
» texto = 'Telefone: 05 (61) 3940-35356 (casa da Dinda), CEP: 123456789' » # 4 digitos (1º grupo), hifen, 5 dígitos (2º grupo) » padrao = '(\d{4})-(\d{5})' » resultado = re.search(padrao, texto) » # o 1º grupo combina com » print(resultado.group(1)) ↳ 3940 » # o 2º grupo combina com » print(resultado.group(2)) ↳ 35356 » # ambos os grupos » print(resultado.group()) ↳ 3940-35356
Parênteses ()
indicam um grupo, a ser procurado como um bloco. O sinal |
indica uma alternativa onde um ou outro grupo é procurado.
» # procurando por mato ou mito » print(re.search('m(a|i)to', 'moto mato mito').group()) ↳ mato » # só a primeira ocorrência é retornada » print(re.search('m(a|i)to', 'moto muto mito').group()) ↳ mito » print(re.search('q{3}', 'q qq qqq').group()) ↳ qqq » print(re.search('q{,2}', 'qqqqq qq qqq').group()) ↳ qq print(re.search('q{2,}', 'qqqqqqqq qq qqq').group()) ↳ qqqqqqqq
Um colchete []
delimita um conjunto alternativo de caracteres. O sinal |
indica uma alternativa, um ou outro grupo é casado.
» print(re.search('pr[ae]to', 'prato, preto').group()) ↳ prato » print(re.search('pr[ae]to', 'proto, preto').group()) ↳ preto » # [0-9] representa qualquer dígito. '[0-9]{3,}' é grupo com mais de 3 dígitos: » print(re.search('[0-9]{3,}', '6-45-4567-345345').group()) ↳ 4567 » # grupo com até 2 dígitos » print(re.search('[0-9]{,2}', '45-4567-345345').group()) ↳ 45 » # \d é o mesmo que [0-9] » print(re.search('\d{,2}', '45-4567-345345').group()) ↳ 45
No Python uma “raw string” é uma sequência de caracteres que ignoram especiais do texto demarcado com \
. '\ttexto'
é “texto” após um espaçamento de tabulação mas r'\ttexto'
é uma string simples. Na montagem de padrões é comum se usar “raw strings”.
» texto = '(casa): 72715-620, (escritório): 74854-890' » busca = re.search('\(casa\)', texto) » busca.group() ↳ (casa) » # \t é tab » print('\ttexto') ↳ texto » # raw strings ignoram o metacaracter » print(r'\ttexto') ↳ \ttexto
O padrão usado abaixo, padrao = ‘\+\d{2}\(\d{2}\)\d{5}-\\d{4}’ significa um número escrito como um telefone no formato + cód país (cod área) 5 dígitos – 4 dígitos.
» texto = '''Suponha que temos um texto com um número de telefone » Telefone do cliente: +55(21)92374-4782 » mais texto irrelevante''' » padrao = '\+\d{2}\(\d{2}\)\d{5}-\\d{4}' » fone = re.search(padrao, texto).group() » print(fone) ↳ +55(21)92374-4782
Método re.findall
re.findall(padrao, texto)
O método findall
encontra todas as ocorrências de padrao em texto e retorna uma lista com os trechos encontrados.
» import re » texto = 'Hoje 1 estamos 23 procurando 456 por 7890 números' » padrao = '\d' » resultado = re.findall(padrao, texto) » # \d = qualquer um dígito » print(resultado) ↳ ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'] » # \d+ = qualquer um ou mais dígitos » print(re.findall('\d+', texto)) ↳ ['1', '23', '456', '7890'] » # \d{2} = grupos de 2 dígitos » print(re.findall('\d{2}', texto)) ↳ ['23', '45', '78', '90'] » # \d{3} = grupos de 3 dígitos » print(re.findall('\d{3}', texto)) ↳ ['456', '789'] » # \d{3,} = grupos de 3 ou mais dígitos » print(re.findall('\d{3,}', texto)) ↳ ['456', '7890'] » # \D+ = grupos de 1 ou mais não-dígitos » print(re.findall('\D+', texto)) ↳ ['Hoje ', ' estamos ', ' procurando ', ' por ', ' números'] » # caracteres na faixa de a até d (a, b, c, d) » print(re.findall('[a-d]', texto)) ↳ ['a', 'c', 'a', 'd'] » # dígitos na faixa de 1 a 4 (1,2 ,3, 4) » print(re.findall('[1-4]', texto)) ↳ ['1', '2', '3', '4'] » # texto 'oje' ou 'ando' » print(re.findall('oje|ando', texto)) ↳ ['oje', 'ando'] » # texto 'oje' ou 'ando' seguindos de qualquer sequência de caracteres » print(re.findall('oje.*|ando.*', texto)) ↳ ['oje 1 estamos 23 procurando 456 por 7890 números'] » # Obs. em qualquer busca o trecho casado é excluído de buscas posteriores. » # o padrão 'ando.*' é ignorando » # para encontrar no texto um padrão que contém metacaracteres usamos "raw strings" » texto = 'Podemos usar \n para quebra de linha e \t para tabulações.' » print(re.findall(r'[\n\t]', texto)) ↳ ['\n', '\t']
Método re.split
resultado = re.split(padrao, texto, [maxsplit])
O método de split
parte o texto em todas as ocorrências de padrao: e retorna uma lista com os trechos encontrados. Se o padrão não for encontrado uma lista com o texto inteiro é retornada. O parâmetro maxsplit (opcional) especifica o número máximo de cortes devem ser feitos no texto. O default é maxsplit = 0
, signicando que todos os cortes possíveis serão feitos.
» import re » texto = 'Hoje 1 estamos 23 procurando 456 por 7890 números' » padrao = '\d+' » resultado = re.split(padrao, texto) » # texto picado em toda ocorrência de 1 ou mais dígitos » print(resultado) ↳ ['Hoje ', ' estamos ', ' procurando ', ' por ', ' números'] » # texto picado em toda ocorrência de espaços (\s) » print(re.split('\s', texto)) ↳ ['Hoje', '1', 'estamos', '23', 'procurando', '456', 'por', '7890', 'números'] » # padrão não encontrado » print(re.split('w', texto)) ↳ ['Hoje 1 estamos 23 procurando 456 por 7890 números'] » # especificando maxsplit = 2 (fazer apenas 2 cortes no texto) » print(re.split('\d+', texto, 2)) ↳ ['Hoje ', ' estamos ', ' procurando 456 por 7890 números']
Método re.sub
resultado = re.sub(padrao, subst, texto, [quantos])
O método re.sub
procura um padrão e o substitui por um texto. A variável resultado é uma string com padrao substituído por subst. Se o padrão não é encontrado o texto original é retornado. O parâmetro opcional quantos indica quantas substituições devem ser feitas. O default é quantos = 0, o que significa que todas as ocorrências do padrão devem set substituídas.
» # remover todos os espaços em branco » import re » # texto com várias linhas e espaços em branco » texto = 'Nome: Pedro \nSobrenome: Alvarez\nCabral' » # padrão para casar com espaços (troca espaços por '') » padrao = '\s' » sub = '' » resultado = re.sub(padrao, sub, texto) » print(resultado) ↳ Nome:PedroSobrenome:AlvarezCabral » # padrão para substituir 1 ou mais espaços por espaço único » texto = 'É comum ter textos com dois ou mais espaços inseridos onde se deseja apenas um!' » print(texto) ↳ É comum ter textos com dois ou mais espaços inseridos onde se deseja apenas um! » print(re.sub('\s+', ' ', texto) ) ↳ É comum ter textos com dois ou mais espaços inseridos onde se deseja apenas um! » # usando o parâmetro quantos » texto = 'Esse texto possui 4 ocorrências de 3 dígitos repetidos: 012, 123, 234 e 345.' » # Substituindo apenas as 2 primeiras ocorrências de 3 dígitos por ### » print(re.sub('\d{3}', '###', texto, 2)) ↳ Esse texto possui 4 ocorrências de 3 dígitos repetidos: ###, ###, 234 e 345.
Método re.subn
resultado = re.subn(padrao, subst, texto, [quantos])
O método re.subn
é similar à re.sub
mas retorna uma tupla de 2 itens, contendo a string modificada e o número de substituições feitas.
» texto = 'Temos as seguintes permutações de {a, b, c}: abc, acb, bac, bca, cab, cba.' » resulta = re.subn('[abc]{3}', '|||', texto) » print(resulta) » print('Foram feitas {} substituições'.format(resulta[1])) ↳ ('Temos as seguintes permutações de {a, b, c}: |||, |||, |||, |||, |||, |||.', 6) ↳ Foram feitas 6 substituições
O método re.search
recebe dois argumentos: um padrão e o texto a ser modificado. O método procura apenas pela primeira ocorrência do padrão. Se existe um casamento o método retorna um objeto match que contém a posição da coincidência (início e final) e a parte do texto que combina com o padrão. Se não houver nenhum casamento o método retorna None.
Método re.compile
padraoCompilado = re.compile(padrao, flags = 0)
O método re.compile()
é especialmente útil quando o mesmo padrão será usado muitas vezes. Ele prepara um padrão através de uma pré-compilação e as armazena em cache que torna mais rápidas as buscas.
O método retorna um objeto re.Pattern
que representa o padrao compilado sobre efeito dos parâmetros opcionais flags. Um exemplo é flag = re.I que determina que a busca será “insensível ao caso”. O objeto possui métodos que permitem as buscas pelo padrão dentro de um texto, tal como padrao.findall(texto), que retorna uma lista, ou padrao.finditer(texto) que retorna um iterável com os casamentos encontrados.
O padrão patt = ‘(xa|ma){2}’ significa um dos dois grupos, “xa” ou “ma”, repetidos 2 vezes.
» # 2 ocorrências de "xa" ou "ma" » patt = '(xa|ma){2}' » padrao = re.compile(patt) » texto = 'xa, xaxado, ma, mamata, errata' » busca = padrao.findall(texto) » print(busca) ↳ ['xa', 'ma'] » # ocorrência de 5 dígitos juntos » padrao = re.compile('\d{5}') » texto = '12345 543213 858 9658 96521' » busca = padrao.finditer(texto) » for t in busca: » print(t.group()) ↳ 12345 ↳ 54321 ↳ 96521
O objeto retornado, representado pela variável padraoCompilado acima, tem vários atributos, que podem ser vistos com a função dir(). Entre eles temos:
Flags ou sinalizadores
Os métodos do módulo re admitem um parâmetro extra chamado de flag (sinalizador ou marcador). Eles modificam o significado do padrão que se pretende buscar.
Os sinalizadores podem ser qualquer um dos seguintes:
Abreviado | longo | integrado (inline) | significado |
---|---|---|---|
re.I |
re.IGNORECASE |
(?i) |
ignorar maiúsculas e minúsculas. |
re.M |
re.MULTILINE |
(?n) |
força os localizadores ^ $ a considerarem uma linha inteira. |
re.S |
re.DOTALL |
(?s) |
força . a casar com a newline, \n . |
re.U |
re.UNICODE |
(?u) |
força \w, \W, \b, \B} a seguirem regras Unicode. |
re.L |
re.LOCALE |
(?L) |
força \w, \W, \b, \B} a seguirem regras locais. |
re.X |
re.VERBOSE |
(?x) |
permite comentários no regex. |
» txt = 'estado, Estudo, estrume, ESTATUTO' » r1 = re.findall('est[a-z]+', txt) » r2 = re.findall('est[a-z]+', txt, flags=re.IGNORECASE) » print(r1) ↳ ['estado', 'estrume'] » print(r2) ↳ ['estado', 'Estudo', 'estrume', 'ESTATUTO'] » # o mesmo resultado pode ser obtido com a notação inline » re.findall('(?i)est[a-z]+', txt) ↳ ['estado', 'Estudo', 'estrume', 'ESTATUTO'] » re.findall('[a-z]+[dt]o', txt, flags=re.I) ↳ ['estado', 'Estudo', 'ESTATUTO']
Para usar mais de uma flag é possível separá-las com uma barra vertical (ou pipe). Por exemplo para uma busca multiline, insensível ao caso e com comentário:
re.findall(pattern, string, flags=re.I|re.M|re.X)
» texto = """ » Gato é um bicho engraçado. » gato não é como cachoroo. » Gato mia! » """ » # a 1&orf; linha não começa com 'gato' » re.findall("^gato", texto, flags=re.IGNORECASE) ↳ [] » # procurando em todoas as linhas » re.findall("^gato", texto, flags=re.M) ↳ ['gato'] » # procurando em todoas as linhas, insensível ao caso » re.findall("^gato", texto, flags=re.I | re.M) ↳ ['Gato', 'gato', 'Gato'] » # o mesmo resultado pode ser conseguido com flags inline » re.findall("(?i)(?m)^gato", text) ↳ ['Gato', 'gato', 'Gato']
Exemplos
Um exemplo simples de remoção de tags aplicado a um texto HTML pode ser o seguinte: O padrão padrao = '<.*?>|[\n]'
apenas casa com qualquer conteúdo dentro de <>
, não guloso ou um sinal de quebra de linha, [\n]
. Usando o método re.sub
removemos todos os trechos que casam com esse padrão.
» html = ''' » <html> » <body> » <p>Parágrafo um.</p> » <p>Parágrafo dois.</p> » </body> » </html> » ''' » padrao = '<.*?>|[\n]' » textoSemTags = re.sub(padrao, '', html) » print(textoSemTags) ↳ Parágrafo um.Parágrafo dois.
Existem bibliotecas sofisticadas para web scrapping, como Beautiful Soup que permite a busca, modificação e completa navegação de um documento extraído de uma página HTML. Buscas podem ser feitos por elementos de css, ids e classes e tags.
Padrões muito complexos são difíceis de serem lidos e alterados. Para quem programa em Python as buscas regex são geralmente ferramentas auxiliares que podem ser complementadas com manuseios do texto feitos em código.
Suponha que temos um texto no formato *.csv (valores separados por vírgulas) com 5 colunas. Na quarta coluna existe uma data com formato nem sempre consistente, como 26/06/2021 onde o ano pode ter apenas 2 dígitos e o separador pode ser um barra ou hífen. Queremos extrair o valor da quinta coluna quando o ano for posterior a 2015.
» csv = ''' » col1, col2, col3, data, valor » a1 , a2 , a3 , 01/06/01, 1000 » b1 , b2 , b3 , 06/05/2016, 1000 » c1 , c2 , c3 , 4/3/17, 2000 » d1 , d2 , d3 , 14-12-2018, 600 » e1 , e2 , e3 , 19-09-19, 600 » ''' » for t in csv.split('\n'): » data = t.split(',') » if len(data) != 5: continue » dt = data[3].strip() » if not re.match('\d{1,2}[/|-]\d{1,2}[/|-]\d{2,4}', dt): continue » ano = int(re.split('[/|-]',dt)[2]) » ano = ano + 2000 if ano < 100 else ano » if ano > 2015: » print(ano, data[4]) ↳ 2016 1000 ↳ 2017 2000 ↳ 2018 600 ↳ 2019 600
O texto é partido em linhas, cada linha em campos separados por vírgula. Como existem linhas vazias só são aproveitadas aquelas com 5 campos. Formatos de data não admissíveis são excluídos e uma correção para anos com apenas dois dígitos inserida.
Bibliografia
- Hunt, John: Advanced Guide to Python 3 Programming, Springer, Switzerland, 2019.
- Teste de Regex online: Untitled Pattern, acessado em junho de 2021.
- Programiz: Python, programming regex, acessado em junho de 2021.
- PYnative: Compile Regex Pattern using re.compile, acessado em junho de 2021.
- Regular-Expressions.info:Consulte a bibliografia no final do primeiro artigo dessa série.