Expressões regulares do Python

As expressões regulares são uma linguagem poderosa para corresponder padrões de texto. Esta página fornece uma introdução básica às expressões regulares, o suficiente para nossos exercícios em Python, e mostra como as expressões regulares funcionam em Python. O módulo "re" do Python é compatível com expressões regulares.

Em Python, uma pesquisa por expressão regular geralmente é 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 dentro da string. Se a pesquisa for bem-sucedida, search() retornará um objeto de correspondência ou None, caso contrário. Portanto, a pesquisa geralmente é seguida imediatamente por uma instrução "if" para testar se a pesquisa foi bem-sucedida, como mostrado no exemplo a seguir, que procura pelo padrão "word:" seguido 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". A instrução if testa a correspondência. Se for verdadeira, a pesquisa terá êxito e match.group() for o texto correspondente (por exemplo, "word:cat"). Caso contrário, se a correspondência for "false" (nenhum para ser mais específico), a pesquisa não terá êxito e não haverá texto correspondente.

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

Padrões básicos

O poder das expressões regulares é que elas podem especificar padrões, não apenas caracteres fixos. Estes são os padrões mais básicos que correspondem a caracteres únicos:

  • a, X, 9, < -- caracteres comuns correspondem exatamente a si mesmos. Os metacaracteres que não se correspondem porque têm significados especiais são: . ^ $ * + ? { [ ] \ | ( ) (detalhes abaixo)
  • . (um ponto) -- corresponde a qualquer caractere único, exceto à nova linha "\n"
  • \w -- (w minúsculo) corresponde a um caractere de "palavra": uma letra, um dígito ou uma barra inferior [a-zA-Z0-9_]. Observe que, embora "palavra" seja a expressão mnemônica para isso, ela corresponde a apenas um caractere de palavra, não a uma palavra inteira. \W (W maiúsculo) corresponde a qualquer caractere que não seja uma palavra.
  • \b -- limite entre palavra e não palavra
  • \s -- (s minúsculo) 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.
  • \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
  • \ -- inibem a "especialidade" de um caractere. 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, o programa Python vai ser interrompido com um erro.

Exemplos básicos

Piada: como é que se chama porco com três olhos? piii!

As regras básicas da pesquisa por expressão regular para um padrão dentro de uma string são:

  • A pesquisa segue a string do início ao fim, parando na primeira correspondência encontrada
  • Todos os padrões precisam ter correspondência, mas não todas as strings
  • Se match = re.search(pat, str) for bem-sucedida, a correspondência não será "None" e, em especial, match.group() será 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

Tudo fica mais interessante quando você usa "+" e "*" para especificar repetição no padrão.

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

Maior e mais à esquerda

Primeiro, a pesquisa encontra a correspondência mais à esquerda para o padrão e, depois, tenta usar o máximo possível da string, ou seja, usar + e * ir o mais longe possível (dizem que + e * são "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 macaco roxo". Ele será usado como um exemplo para demonstrar mais atributos de expressão regular. Confira uma tentativa usando 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'

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

Parênteses

Colchetes podem ser usados para indicar um conjunto de caracteres, portanto [abc] corresponde a "a", "b" ou "c". Os códigos \w, \s etc. também funcionam dentro de colchetes, com a única exceção que ponto (.) significa apenas um ponto literal. Para o problema de e-mails, os colchetes são uma maneira fácil de adicionar "." e "-" ao conjunto de caracteres que podem aparecer em torno do @ 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 recursos de colchete) Você também pode 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, insira o traço por último, por exemplo, [abc-]. Um chapéu (^) no início de um colchete o inverte, portanto, [^ab] significa qualquer caractere, exceto "a" ou "b".

Extração de grupo

O recurso "grupo" de uma expressão regular permite que você selecione partes do texto correspondente. Suponha que, para o problema de e-mails, queremos extrair o nome de usuário e o host separadamente. Para isso, adicione parênteses ( ) ao nome do usuário e ao 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 no módulo re. Acima, usamos re.search() para encontrar a primeira correspondência de um padrão. findall() encontra *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, é possível que você tenha o hábito de escrever uma repetição para iterar nas linhas do arquivo e, depois, chamar findall() em cada linha. Em vez disso, deixe findall() fazer a iteração por você - muito melhor! Basta inserir o texto inteiro 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 *tuples*. 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 tamanho 2 contendo o nome de usuário e o host, por exemplo, ("alice", "google.com").

  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 repetir sobre ela para fazer algum cálculo 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 existem agrupamentos de parêntese ( ) no padrão, mas os quais você não deseja extrair. Nesse caso, escreva os parênteses com um ?: no início, por exemplo, (?: ), e esse parêntese esquerdo não contará como um resultado de grupo.)

Fluxo de trabalho e depuração RE

Os padrões de expressão regular contêm muito significado em apenas alguns caracteres , mas eles são tão densos que você pode gastar muito tempo depurando seus padrões. Configure seu tempo de execução para que você possa executar um padrão e imprimir o que corresponde facilmente, por exemplo, executando-o em um texto pequeno de teste e gerando o resultado de findall(). Se o padrão não corresponder a nada, tente enfraquecer o padrão, removendo partes dele para que você receba muitas correspondências. Quando não há correspondência, não é possível progredir, porque não há nada concreto a ser analisado. Quando há muitas correspondências, você pode melhorar cada vez mais para alcançar exatamente o que quer.

Opções

As funções re têm 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 -- ignora as diferenças em maiúsculas/minúsculas para a correspondência, portanto "a" corresponde a "a" e "A".
  • DOTALL -- permite que o ponto (.) corresponda a nova linha. Normalmente, ele corresponde a tudo, exceto a nova linha. Isso pode confundir você. Você acha que .* corresponde a tudo, mas, por padrão, não passa do fim de uma linha. Observe que \s (espaço em branco) inclui novas linhas. Portanto, se você quiser corresponder a uma sequência de espaços em branco que possa incluir uma nova linha, basta usar \s*
  • MULTILINE -- Dentro de uma string formada por muitas linhas, permita que ^ e $ correspondam ao início e ao fim de cada linha. Normalmente, ^/$ corresponde ao início e ao fim de toda a string.

Greedy x não grisalho (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.

Digamos que haja um texto com tags: <b>foo</b> e <i>assim por diante</i>

Suponha que você esteja tentando fazer a correspondência de cada tag com o padrão '(<.*>)'. Qual é a primeira correspondência?

O resultado é um pouco surpreendente, mas o aspecto ganancioso de .* faz com que ele corresponda a "<b>foo</b> inteiro" e <i>assim por diante</i>" como uma grande correspondência. O problema é que o .* vai o mais longe possível, em vez de parar no primeiro > (ou seja, é "ganancioso").

Há uma extensão para a expressão regular onde você adiciona um ? no final, como .*? ou .+?, mudando-os para não gananciosos. Agora, eles param o mais rápido possível. Assim, o padrão "(<.*?>)" receberá apenas "<b>" como a primeira correspondência, e "</b>" como a segunda correspondência, e assim por diante, obtendo cada par <..> por vez. O estilo normalmente é usar um .*? imediatamente seguido por um marcador concreto (>, neste caso) para o qual a execução de .*? é forçada.

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

Uma técnica mais antiga, mas amplamente utilizada, para codificar a ideia de "todos esses caracteres, exceto parar em X", usa o estilo de colchete. Para o exemplo acima, você poderia 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 ^ inicial "inverte" o conjunto de colchetes, para que corresponda 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 do grupo(1), do grupo(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 com 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 de nomes de bebês.