Expressões regulares, parte I

Introdução

Imagine que você recebeu um banco de dados cujo um dos campos contém strings, e strings digitadas por usuários. Essa opção já deve provocar tremores em qualquer um que já lidou com esse tipo de dado. Linguagens de programação compartilham uma característica com os corretores da dissertação do ENEM: elas irão puxar o seu pé pelo menor erro de digitação possível. Exemplo desses pesadelos são campos de nomes, endereços, etc. Alguns exemplos são:

  • "Rafael da Silva" será diferente de "rafael da silva" ou "RAFAEL DA SILVA";
  • "Curitiba, Paraná" será diferente de "Curitiba, Parana";
  • "Av. Paulista, São Paulo - SP" será sempre diferente de "Avenida Paulista, Sao Paulo - SP";

Assim consulta sobre esses dados sempre serão prejudicadas por essas diferenças de caracteres que, a olho nu, parecem bobas, mas para a máquina fazem toda a diferença.

Nos cursos introdutórios de Python, já vimos como se edita uma string. Então, para padronizar a frase "Av. Paulista, São Paulo - SP", poderíamos simplesmente usar

 

texto = "Av. Paulista, São Paulo - SP"
texto.lower().replace('ã','a')

O resultado será "av. paulista, sao paulo - sp", que pode ser comparado com outras tantas variações que o usuário pode digitar. Porém, fizemos essa alteração para uma única string em particular. Um database pode ser composto de milhões, bilhões ou trilhões de outras strings completamente diferente dessas.

Mas com um padrão de busca ou mudança em mente (tal qual passar tudo para caixa baixa e retirar os acentos), seria interessante aplicar as mesmas operações à todas as strings que obedecem a esse padrão. Nasce, portanto, o conceito de expressões regulares ou regex (do inglês, regular expressions): de forma simples, uma maneira de realizar consultas e procurar padrões em textos especificando uma regra que todas as strings desejadas obedeçam. Mesmo que usemos o conceito dentro da linguagem de programação Python, expressões regulares possuem sua lógica própria, tal como o SQL. Por isso, as regexs parecem intimidadoras, mas conforme o uso e o aprendizado da lógica por trás, vai ficando mais fácil.

Conceitos Básicos

Quando passamos um texto para um código Python, ele pode interpretar esse texto de várias maneiras, entre elas como uma string pura ou como uma string regular. Exemplo, suponhamos que estejamos lendo um texto de um arquivo. Esse texto virá acompanhado de vários caracteres especiais que, embora não os vemos, eles são salvos junto com o texto, por exemplo a quebra de linha "\n". Toda vez que você pressiona Enter para criar uma nova linha no seu texto, você adiciona no final da parte anterior um "\n":

print("O dia está ensolarado.\nVamos ao parque!")

Cujo resultado será:
O dia está ensolarado.
Vamos ao parque!

Assim, o Python está interpretando esse "\n" com o papel que ele desempenha no texto. Mas ele poderia interpretá-lo como um carácter qualquer:

print(r"O dia está ensolarado.\nVamos ao parque!")

Cuja resposta é
O dia está ensolarado.\nVamos ao parque!

Uma string regular é um texto cujos caracteres serão interpretados para fornecer o formato de texto que esperemos: quebras de linha, indentação de parágrafos, negrito, sublinhado, etc. Uma string pura é uma string que o Python não interpretará sua função no texto, simplesmente a tratará como outro carácter qualquer.

Outro conceito muito importante para lidar com regex é o de padrão: é uma string contendo uma expressão abstrata que irá representar um padrão na linguagem escrita. Por exemplo, podemos usar o método .replace() das strings do Python para substituir uma vogal acentuada em um texto pela mesma vogal sem o acento; mas esse processo é manual, pois necessitará da vogal e do acento em particular. A ideia do padrão é, portanto, produzir uma expressão que irá identificar qualquer vogal com qualquer acento em qualquer lugar do texto.

Dessa forma, podemos estar interessados em identificar também a formatação do texto (i.e., caracteres tais como "\n","\t", etc). Dessa forma, é importante que o padrão esteja como string pura.

Identificadores

São expressões que irão identificar a presença ou a ausência de um determinado carácter no texto. Exemplo, uma forma de procurar um dígito dentro de uma string usando regex é com o identificador "\d", que irá buscar qualquer dígito no texto. Considere o seguinte texto: "José da Silva, portador do CPF nº 999.999.999-99". Suponha que estejamos interessados apenas nos números. Então montamos um padrão de busca com o "\d" que voltara as combinações de dígitos no texto: Para isso, usaremos a função findall() do módulo re. Ela irá retornar uma lista contendo todas os achados do padrão procurado.

texto = r"José da Silva, portador do CPF nº 999.999.999-99"
padrao = r"\d"
re.findall(padrao, texto)

Cujo resultado será:
['9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9']
Temos, portanto, uma lista com todos os caracteres do CPF de José da Silva. Uma ótima opção para padronizarmos o input desse dado, já que um usuário poderia digitá-lo de outra forma:

texto = r"José da Silva, portador do CPF nº 99999999999"
padrao = r"\d"
re.findall(padrao, texto)

Cujo resultado será a mesma lista acima. Abaixo segue o uma lista com os principais identificadores:

Identificador Uso
\d Identificar qualquer dígito no texto.
\D Identificar qualquer carácter no texto que não seja um dígito.
\s Identificar qualquer espaço (espaço, tab, quebra de linha, etc).
\S Identificar qualquer carácter no texto que não seja espaço (espaço, tab, quebra de linha “\n”, etc).
\w Identifica qualquer carácter alfanumérico, incluindo “_”.
\W Identifica qualquer carácter não alfanumérico, excluindo “_”.
. Identifica qualquer carácter que não seja a quebra de linha “\n”.
\A Identifica os caracteres no início de uma string.
\Z Identifica os caracteres no final de uma string.
\b Identifica os caracteres no final ou início de uma palavra.
\B Identifica os caracteres na palavra que não estejam no final ou início dessa palavra.

 

Modificadores

A ação dos modificadores é complementar a ação dos identificadores, expressando quantidade, intervalo de caracteres, posição ou até a operação de lógica ou.

Modificador Uso
+ Identifica um ou mais caracteres.
* Identifica zero ou mais caracteres.
? Identifica um ou nenhum caracter.
^ Identifica no início da string.
$ Identifica no final da string.
| Bitwise or. O padrão x|y Identifica o padrão x ou o padrão y.

Alguns modificadores são mais complicados. Eles irão expressar variância, intervalo e quantidades específicas de caracteres a serem identificados; são eles:

  • {}:
    • pode ser usador para especificar uma quantidade de caracteres a serem identificados; e.g. "\d{3}" irá identificar qualquer sucessão de 3 dígitos presentes no texto.
    • pode ser usado para especificar um intervalo de quantidades de caracteres a serem idenficados; e.g.: "\d{3,5}" irá identificar qualquer sequência de dígitos de comprimento 3, 4 ou 5 presentes no texto.
  • []:
    • Pode ser usado para identificar um grupo de caracteres em particular; e.g.: "[abc]" irá identificar a, b ou c.
    • Pode ser usado para identificar um intervalo de caracteres; e.g.: "[A-N]" irá identificar qualquer carácter de A até N (apenas as maiúsculas). [0-9] irá identificar qualquer carácter de 0 até 9.
    • Se o primeiro carácter de [] for ^, então esse modificador irá especificar qualquer carácter que não está nesse intervalo especificado; e.g.: [^A-Z] irá identificar qualquer carácter que não esteja no intervalo de A até Z(maiúsculo).
    • Outros caracteres especiais, com a exceção de ^ no inicio, perdem o significado dentro de []. E.g.: [+*()^] irá identificar qualquer carácter entre +, *, (, ) ou ^.

No exemplo abaixo, vamos recuperar do texto todos os valores que representam dinheiro. Assim, deveremos procurar todos os pedaços desse texto que se iniciam com R$; como $é um modificador, para usá-lo no regex usamos o \$ para diferenciá-los. Após, queremos identificar uma sequência de um ou mais dígitos para, que será a parte inteira do nosso valor, trabalho que será feito com o identificador de dígitos \d seguido do modificador +. Por último, a parte fracionária que, no caso do padrão brasileiro está separado por vírgula da parte inteira. Assim, chegamos no padrão r"R\$\d+,\d+"

texto = "José da Silva ganha, atualmente, R$1000,010 por mês, mas tem que pagar R$300,26 de aluguel cujo valor sobe 0,1% ao anos."
padrao =  r"R\$\d+,\d*"
re.findall(padrao, texto)

O resultado é a seguinte lista:
['R$1000,010', 'R$300,26']

Outro exemplo, suponha que você montou um dicionário com dados de gênero a partir dos nomes. Seu dicionário contem o gênero do Felipe, da Aline e do Rafael. Mas e as variações desses nomes: Phelipe, Philip, Alinne, Alyne, Raphael, etc? Usaremos aqui o símbolo o identificador . que irá procurar o padrão com qualquer carácter exceto a quebra de linha "\n". Para o caso do nome Rafael, vemos que as partes Ra e ael são comuns a todos; o que muda de um pra outro é o f, ff e ph, que é uma sequência de um ou dois caracteres. Assim, chegamos ao padrão r"Ra.{1,2}ael":

texto = "Raphael Silva, 40 anos, casado, morador da Avenida Rebousas, nº 1500. Raffael possui 3 laranjas e 4 pêras em seu nome. Porém, Rafael não deseja vender nenhuma delas."
padrao = r'Ra.{1,2}ael'
re.findall(padrao,texto)

Cujo retorno é
['Raphael', 'Raffael', 'Rafael']

Outro exemplo interessante é o do CPF, que pode ser escrito das seguintes formas:

  • XXX.XXX.XXX-XX
  • XXXXXXXXX-XX
  • XXXXXXXXXXX

Assim, construiremos um padrão para cada um deles e após usaremos o modificador | para encontrar qualquer ocorrência dentro dessas três. Para o primeiro formato, temos 3 grupos de 3 dígitos separados por vírgula e seguidos por um hífen e por um grupo de 2 dígitos. Assim, o padrão para esse caso fica r"\d{3}\.\d{3}\.\d{3}-\d{2}". Repare que o ponto da string é representado por \., já que . é um identificador. Para os outros, a lógica é similar: r"\d{9}-\d{2}" e \d{11}. Dessa forma, o padrão completo fica r"\d{3}\.\d{3}\.\d{3}-\d{2}|\d{9}-\d{2}|\d{11}"
Assim, o código

texto = "José da Silva, portador do CPF nº 999.999.999-99. Ana Souza portadora do CPF nº 888888888-88. Jõao Silva, portador do CPF nº 77777777777"
padrao = r"\d{3}\.\d{3}\.\d{3}-\d{2}|\d{7}-\d{2}|\d{11}"
re.findall(padrao, texto)

produz a seguinte lista:
['999.999.999-99', '8888888-88', '77777777777']

Assim como o Python, o regex diferencia as caixas do carácter. Dessa forma, uma busca pode ser atrapalhada por essas diferenças. Considere o seguinte caso: queremos todos os nomes próprios do texto. Assim, sabemos que a palavra irá começar com uma letra maiúscula, usaremos o padrão "[A-Z]", seguida de uma ou mais letras minusculas, combinaremos o anterior com "[a-z]+".

texto = "José da Silva, nascido em 23/05/1950 em São Paulo - SP, solteiro, residente na Av. Paulista nº 1500 e portador do CPF nº 999.999.999-99. O telefone para contato é (11) 9999-9999"
padrao = r"[A-Z][a-z]+"
re.findall(padrao, texto)

O código acima resultará em

['Jos', 'Silva', 'Paulo', 'Av', 'Paulista']
Eliminando o caracter acentuado do nome José e tirando a palavra São da lista, já que usamos o modificador +. Para isso, também devemos dizer para o regex que queremos caracteres acentuados:

texto = "José da Silva, nascido em 23/05/1950 em São Paulo - SP, solteiro, residente na Av. Paulista nº 1500 e portador do CPF nº 999.999.999-99. O telefone para contato é (11) 9999-9999"
padrao = r"[A-ZÁ-Ý][a-zá-ÿ]+"
re.findall(padrao, texto)

No padrão acima, ele procurará todas as palavras iniciadas com letra maiúscula no intervalo de A até Z ou do intervalo de Á até Ý; logo após, dizemos para o padrão identificar uma ou mais letras minúsculas de a eté z ou de á até ÿ. O resultado é
['José', 'Silva', 'São', 'Paulo', 'Av', 'Paulista']

Match e Search

Além de identificar padrões e receber uma lista dos encontrados, podemos usar as função match e search do módulo re. A principal vantagem, além de ter os pedaços de textos identificados, é saber onde no texto principal essa string está localizada. A função match irá buscar a string que segue o padrão informado, ao passo que a função search varrerá o texto inteiro. O resultado é um objeto do tipo match, caso haja a identificação e do qual podem ser extraídas tanto o texto identificado bem como a sua posição, ou None, caso não haja identificação.

texto = "José da Silva, nascido em 23/05/1950 em São Paulo - SP, solteiro, residente na Av. Paulista nº 1500 e portador do CPF nº 999.999.999-99. O telefone para contato é (11) 9999-9999"
padrao = r"\d{3}\.\d{3}\.\d{3}-\d{2}"
match = re.search(padrao, texto)

O valor identificado pode ser acessado através do método .group() do objeto match gerado e a sua posição na string é dada pelos métodos .start() e .end().

print(match.group())

Irá retornar o valor identificado, no caso o CPF 999.999.999-99.

print(match.start())
print(match.end())

Irão retornar 121 e 135, respectivamente, que é a posição que o CPF se inicia e a posição final. Dessa forma, é possível recortar as string e obter o texto encontrado:

inicio = match.start()
fim = match.end()
print(texto[inicio:fim])

que resulta no CPF 999.999,999-99

Quando o padrão não é encontrado no texto, o resultado das funções search e match são None.

texto = "José da Silva, nascido em 23/05/1950 em São Paulo - SP, solteiro, residente na Av. Paulista nº 1500 e portador do CPF nº 999.999.999-99. O telefone para contato é (11) 9999-9999"
padrao = r"\d{3}\.\d{3}\.\d{3}-\d{2}"
match = re.match(padrao, texto)

A função match, nesse caso, irá retornar None, uma vez que o valor identificado não está no inicio da string.

Exemplos e mais exemplos

Vejamos um exemplo com datas. Existem vários formatos de datas, tais como

  • DD/MM/AAAA;
  • AAAA/MM/DD;
  • DD-MM-AAAA;
  • AAAA-MM-DD;
  • DD/MM/AA;

Entre outros. Além de casos como 25 de Dezembro de 2019, que veremos na próxima postagem. Nos casos da lista acima, temos um padrão que é: um conjunto numérico de dois a quatro números, um carácter que faz a separação um grupo de dois números, uma nova ocorrência do carácter que os separam e mais um grupo de dois a quatro números. Assim, podemos montar o regex \d{2,4}.\d{2}.\d{2,4}. Vejamos o resultado:

texto = "Esse é um exemplo de data: 23/05/1950; esse é outro exemplo: 23-05-1950; esse é outro 1950-05-23; mais um: 23/05/50; e mais outro 23-05-50"
padrao = "\d{2,4}.\d{2}.\d{2,4}"
re.findall(padrao, texto)

Cujo resultado é a lista
['23/05/1950', '23-05-1950', '1950-05-23', '23/05/50', '23-05-50']

Um uso muito útil dos objetos match é sua “propriedade booleana”. Se a função match ou search do regex identificar um padrão no texto, esse objeto retornado pode ser usado como o valor True em um bloco condicional, por exemplo. Caso o padrão não seja identificado, essas funções irão retornar None, que agirá como False. Confira abaixo:

texto = "José da Silva, nascido em 23/05/1950 em São Paulo - SP, solteiro, filho de Maria d'Ana da Silva residente na Av. Paulista nº 1500 e portador do CPF nº 999.999.999-99. O telefone para contato é (11) 9999-9999"
padrao_estado_civil = r"solteiro"
match = re.search(padrao, texto)
if match:
    print("Pessoa solteira")
else:
    print("Pessoa não solteira")

Nesse caso, o padrão será identificado, logo o resultado será
pessoa solteira

Conclusão

Lidar com strings é sempre complicado já que não existe uma fórmula fechada para os padrões que desejamos obter do texto, ainda mais quando essas são acentuadas e providas por usuários. Porém, com um mínimo de esforço, vimos acima o poder da regex para extrair informações de textos. Na próxima postagem, veremos padrões ainda mais complexos e mais operações para facilitar o dia-a-dia com strings.