Expressões regulares do Python

As expressões regulares são uma linguagem poderosa para a correspondência de padrões de texto. Esta página fornece uma introdução básica às próprias expressões regulares, o suficiente para nossos exercícios de Python, e mostra como as expressões regulares funcionam em Python. O comando "re" do Python oferece suporte a expressões regulares.

Em Python, uma pesquisa de expressão regular normalmente é escrita da seguinte forma:

match = re.search(pat, str)

O método re.search() usa um padrão de expressão regular e uma string e procura esse padrão na string. Se a pesquisa for bem-sucedida, search() retorna um objeto de correspondência ou None, caso contrário. Portanto, a pesquisa é normalmente seguida imediatamente por uma instrução if para testar se a pesquisa foi bem-sucedida, como mostrado no exemplo a seguir, que procura o padrão "word:" seguida por uma palavra de três letras (detalhes abaixo):

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

O código match = re.search(pat, str) armazena o resultado da pesquisa em uma variável chamada "match". Em seguida, a instrução if testa a correspondência. Se for verdadeira, a pesquisa foi bem-sucedida e match.group() é o texto correspondente (por exemplo, "word:cat"). Caso contrário, se a correspondência for falsa (nenhum para ser mais específico), a pesquisa não foi bem-sucedida e não há texto correspondente.

O "r" no início da string de padrão designa um Python "raw" string que passa por barras invertidas sem alteração, o que é muito útil para expressões regulares (Java precisa muito desse recurso!). Recomendamos que você sempre escreva strings de padrão com o 'r' como um hábito.

Padrões básicos

A vantagem das expressões regulares é que elas podem especificar padrões, não apenas caracteres fixos. Aqui estão os padrões mais básicos que correspondem a caracteres únicos:

  • a, X, 9, < -- caracteres comuns apenas correspondem exatamente a si mesmos. Os metacaracteres que não correspondem entre si porque possuem significados especiais são: . ^ $ * + ? { [ ] \ | ( ) (detalhes abaixo)
  • . (um ponto) -- corresponde a qualquer caractere único, exceto a nova linha "\n"
  • \w -- (w minúsculo) corresponde a uma "palavra" caractere: uma letra, dígito ou barra inferior [a-zA-Z0-9_]. Embora a palavra "word" é a mnemônica disso, ela corresponde somente a uma única palavra, não a uma palavra inteira. \W (W maiúsculo) corresponde a qualquer caractere que não seja uma palavra.
  • \b: limite entre palavras e não palavras
  • \s -- (s minúsculos) corresponde a um único caractere de espaço em branco -- espaço, nova linha, retorno, tabulação, formulário [ \n\r\t\f]. \S (S maiúsculo) corresponde a qualquer caractere que não seja um espaço em branco.
  • \t, \n, \r -- tab, nova linha, retornar
  • \d: dígito decimal [0-9] (alguns utilitários de regex mais antigos não são compatíveis com \d, mas todos são compatíveis com \w e \s)
  • ^ = início, $ = fim: corresponde ao início ou ao fim da string
  • \ -- inibir a "especialidade" de um personagem. Por exemplo, use \. para corresponder a um ponto ou \\ para corresponder a uma barra. Se você não tiver certeza se um caractere tem um significado especial, como "@", tente colocar uma barra na frente dele, \@. Se não for uma sequência de escape válida, como \c, seu programa em Python será interrompido com um erro.

Exemplos básicos

Piada: como se chama um porco com três olhos? iiim!

As regras básicas da pesquisa de expressões regulares para um padrão dentro de uma string são:

  • A pesquisa prossegue na string do início ao fim, parando na primeira correspondência encontrada
  • Todos os padrões precisam ser correspondentes, mas não toda a string
  • Se match = re.search(pat, str) for bem-sucedido, a correspondência não será None e, em particular, match.group() é o texto correspondente.
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

Repetição

As coisas ficam mais interessantes quando você usa + e * para especificar a repetição no padrão

  • + -- uma ou mais ocorrências do padrão à esquerda, por exemplo, "i+" = um ou mais "i"
  • * -- 0 ou mais ocorrências do padrão à esquerda
  • ? -- corresponde a 0 ou 1 ocorrência do padrão à esquerda

Mais à esquerda e Maior

Primeiro, a pesquisa encontra a correspondência mais à esquerda para o padrão e, em seguida, tenta usar o máximo possível da string, ou seja, + e * vão o mais longe possível (considerando-se "+" e * "gananciosos").

Exemplos de repetição

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

Exemplo de e-mails

Suponha que você queira encontrar o endereço de e-mail dentro da string "xyz alice-b@google.com rose mackey". Usaremos isso como um exemplo para demonstrar mais recursos de expressões regulares. Aqui está uma tentativa de usar o padrão r'\w+@\w+':

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

A pesquisa não obtém o endereço de e-mail inteiro neste caso porque \w não corresponde a '-' ou "." no endereço. Vamos corrigir isso usando os recursos de expressão regular abaixo.

Colchetes

Colchetes podem ser usados para indicar um conjunto de caracteres, de modo que [abc] corresponde a "a" ou "b" ou "c". Os códigos \w, \s etc. também funcionam dentro de colchetes, com a única exceção de que ponto (.) significa apenas um ponto literal. Para o problema com e-mails, os colchetes são uma maneira fácil de adicionar “.” e '-' ao conjunto de caracteres que pode aparecer ao redor de @ com o padrão r'[\w.-]+@[\w.-]+' para obter o endereço de e-mail completo:

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
(Mais elementos de colchetes) Também é possível usar um traço para indicar um intervalo, de modo que [a-z] corresponda a todas as letras minúsculas. Para usar um traço sem indicar um intervalo, coloque o traço por último. Por exemplo: [abc-]. Um up hat (^) no início de um conjunto de colchetes a inverte, de modo que [^ab] significa qualquer caractere, exceto "a" ou "b".

Extração de grupo

O "grupo" de uma expressão regular permite que você identifique partes do texto correspondente. Suponha que, para o problema de e-mails, queiramos extrair o nome de usuário e o host separadamente. Para isso, adicione parênteses ( ) em torno do nome de usuário e do host no padrão, desta forma: r'([\w.-]+)@([\w.-]+)'. Nesse caso, os parênteses não alteram a correspondência do padrão, mas estabelecem "grupos" lógicos dentro do texto de correspondência. Em uma pesquisa bem-sucedida, match.group(1) é o texto de correspondência correspondente ao primeiro parêntese esquerdo, e match.group(2) é o texto que corresponde ao segundo parêntese esquerdo. O match.group() simples ainda é o texto de correspondência inteiro, como de costume.

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

Um fluxo de trabalho comum com expressões regulares é que você escreve um padrão para o que está procurando, adicionando grupos de parênteses para extrair as partes desejadas.

Findall

findall() provavelmente é a única função mais poderosa do módulo re. Acima, usamos re.search() para encontrar a primeira correspondência de um padrão. findall() localiza *todas* as correspondências e as retorna como uma lista de strings, em que cada string representa uma correspondência.
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

findall Com arquivos

Para arquivos, você pode ter o hábito de escrever uma repetição para iterar nas linhas do arquivo e, em seguida, chamar findall() em cada linha. Em vez disso, permita que findall() faça a iteração para você — muito melhor! Basta inserir todo o texto do arquivo em findall() e deixar que ele retorne uma lista de todas as correspondências em uma única etapa (lembre-se de que f.read() retorna todo o texto de um arquivo em uma única string):

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

findall e Groups

O mecanismo de grupo de parênteses ( ) pode ser combinado com findall(). Se o padrão incluir dois ou mais grupos de parênteses, em vez de retornar uma lista de strings, findall() retornará uma lista de *tuplas*. Cada tupla representa uma correspondência do padrão e, dentro dela, estão os dados group(1), group(2) ... Portanto, se dois grupos de parênteses forem adicionados ao padrão de e-mail, findall() retornará uma lista de tuplas, cada uma contendo o nome de usuário e o host, por exemplo, ("alice", "google.com.br").

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

Depois de ter a lista de tuplas, é possível fazer uma repetição nela para fazer alguns cálculos para cada tupla. Se o padrão não incluir parênteses, findall() retornará uma lista de strings encontradas como nos exemplos anteriores. Se o padrão incluir um único conjunto de parênteses, findall() retornará uma lista de strings correspondentes a esse único grupo. (Recurso opcional obscuro: às vezes, há agrupamentos de parêntese ( ) no padrão, mas que você não quer extrair. Nesse caso, escreva os parênteses com um ?: no início, por exemplo: (?: ) e o parêntese esquerdo não será contabilizado como um resultado de grupo.

Depuração e fluxo de trabalho RE

Os padrões de expressão regular emitem muito significado em apenas alguns caracteres, mas são tão densos que você pode passar muito tempo depurando seus padrões. Configure o tempo de execução para executar um padrão e imprimir o que corresponda facilmente, por exemplo, executando-o em um pequeno texto de teste e exibindo o resultado de findall(). Se o padrão não corresponder a nada, tente enfraquecê-lo e remover partes dele para gerar muitas correspondências. Quando não corresponde a nada, não é possível progredir, já que não há nada concreto para analisar. Quando houver correspondência demais, você poderá apertar de forma incremental para atingir apenas o que deseja.

Opções

As funções re usam opções para modificar o comportamento da correspondência de padrão. A sinalização de opção é adicionada como um argumento extra a search() ou findall() etc., por exemplo, re.search(pat, str, re.IGNORECASE).

  • IGNORECASE -- ignorar diferenças em maiúsculas/minúsculas para correspondência, de modo que "a" corresponde a "a" e "A".
  • DOTALL: permite que o ponto (.) corresponda a "newline" -- normalmente corresponde a qualquer coisa, exceto "newline". Isso pode confundir você, já que acha que .* corresponde a tudo, mas, por padrão, não vai além do fim de uma linha. Observe que \s (espaço em branco) inclui novas linhas. Portanto, se você quiser fazer a correspondência com um espaço em branco que possa incluir uma nova linha, basta usar \s*
  • MULTILINE -- Dentro de uma string composta por muitas linhas, permita que ^ e $ correspondam ao início e ao final de cada linha. Normalmente, ^/$ corresponde ao início e ao fim de toda a string.

Greedy vs. Non-Greedy (opcional)

Esta é uma seção opcional que mostra uma técnica de expressão regular mais avançada, que não é necessária para os exercícios.

Suponha que você tenha um texto com tags: <b>foo</b> e <i>assim por diante</i>

Suponha que você esteja tentando corresponder cada tag ao padrão '(<.*>)' -- o que corresponde primeiro?

O resultado é um pouco surpreendente, mas o aspecto ganancioso do .* faz com que ele corresponda a todo o '<b>foo</b> e <i>assim por diante</i>" como uma grande partida. O problema é que .* vai o máximo possível, em vez de parar no primeiro > (ou seja, é "ganancioso").

Há uma extensão para expressão regular onde você adiciona um ? no final, como .*? ou .+?, mudando para que não seja ganancioso. Agora eles param assim que podem. Então, o padrão '(<.*?>)' receberá apenas "<b>" como primeira correspondência e '</b>' como a segunda correspondência e assim por diante, obter cada <..> um de cada vez. O estilo normalmente é usar um .*? imediatamente seguido por algum marcador concreto (> neste caso) ao qual o .*? é forçada a se estender.

O *? originada em Perl, e as expressões regulares que incluem as extensões de Perl são conhecidas como expressões regulares compatíveis com Perl -- pcre. O Python inclui suporte para PC. Muitos utilitários de linha de comando etc. têm um sinalizador no qual aceitam padrões PC.

Uma técnica mais antiga, mas amplamente utilizada para codificar essa ideia de "todos esses caracteres, exceto a parada em X". usa o estilo colchete. Para o comando acima, você pode escrever o padrão, mas em vez de .* para obter todos os caracteres, use [^>]* que pula todos os caracteres que não são > (o ^ à esquerda "inverte" o conjunto de colchetes, portanto, corresponde a qualquer caractere que não esteja entre colchetes).

Substituição (opcional)

A função re.sub(pat, replace, str) procura todas as instâncias de padrão na string fornecida e as substitui. A string de substituição pode incluir '\1', '\2' que se referem ao texto de group(1), group(2) e assim por diante a partir do texto correspondente original.

Este é um exemplo que pesquisa todos os endereços de e-mail e os altera para manter o usuário (\1), mas ter yo-yo-dyne.com como host.

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

Exercício

Para praticar expressões regulares, consulte o Exercício com nomes de bebês.