Expressões regulares com Python

Esse é um tópico que sempre patinei bastante, ainda tenho alguma dificuldade pra ler e interpretar expressões regulares no python, então estou escrevendo esse artigo principalmente para me ajudar 🙂

O python tem uma biblioteca bem poderosa para expressões regulares:

import re

A função match serve para casar uma string em um texto, por exemplo:

In [2]: re.match("The", "The book is on the table")
Out[2]: <_sre.SRE_Match object; span=(0, 3), match='The'>

Note que a saída é um objeto que tem duas propriedades interessantes, span que devolve onde a string foi encontrada, no caso na posição 0 até 3, e a propriedade match que mostra o que foi encontrado. Mas a função match tem uma limitação, ela só funciona se a string buscada estiver no começo do texto.

Para encontrar strings ao longo do texto devemos usar a função search:

In [8]: re.search("book", "The book is on the table")
Out[8]: <_sre.SRE_Match object; span=(4, 8), match='book'>

Mas o search se limita a retornar apenas uma ocorrência, para encontrar todas as ocorrências da string no texto usamos a função findall:

In [11]: re.findall("book", "The book is on the book table")
Out[11]: ['book', 'book']

Nesse caso ele retornou uma lista com as strings encontradas.

Em expressão regular existem os metacaracteres, são caracteres especiais que usamos para dar match em determinados padrões, por exemplo  o ‘. ‘ (ponto ) , que da match em qualquer caracter, exceto quebra de linha, por exemplo:

In [12]: re.match(".", "The book is on the book table")
Out[12]: <_sre.SRE_Match object; span=(0, 1), match='T'>

In [13]: re.match(".", "12341234")
Out[13]: <_sre.SRE_Match object; span=(0, 1), match='1'>

In [14]: re.match(".", "\t\t\t")
Out[14]: <_sre.SRE_Match object; span=(0, 1), match='\t'>

In [18]: print(re.match(".", "\n\t\t"))
None

Com o search o funcionamento é similar:

In [33]: re.search('.', '\n\t\t')
Out[33]: <_sre.SRE_Match object; span=(1, 2), match='\t'>

Note que nesse caso ele deu match no \t mesmo com o texto começando com \n, o search ignorou o \n e partiu para processar o próximo caractere.

O “.” (ponto) quando usado com  findall tem um comportamento interessante, ele retorna uma lista de cada caractere da sequencia:

In [35]: re.findall(".", "The book")
Out[35]: ['T', 'h', 'e', ' ', 'b', 'o', 'o', 'k']

Outros dois metacaracteres  importantes são os do tipo âncora como o ^ e o $ , eles delimitam o início e o fim da string, por exemplo:

In [39]: re.findall("^.", "The book\n is on the\n book table")
Out[39]: ['T']

Mas é possível pegar os caracteres de todas as linhas do texto:

In [41]: re.findall("^.", "The book\nis on the\nbook table", re.MULTILINE)
Out[41]: ['T', 'i', 'b']

A âncora de fim de linha ( $ ) funciona de forma similar a ^:

In [47]: re.findall(".$", "The book\nis on the\nbook table", re.MULTILINE)
Out[47]: ['k', 'e', 'e']

Podemos combinar as duas âncoras:

In [48]: re.match("^.$", "The")

In [49]: re.match("^.$", "T")
Out[49]: <_sre.SRE_Match object; span=(0, 1), match='T'>

In [50]: re.match("^.$", "")

O “.” é muito útil porém muito abrangente , podemos limitar os caracters buscados, os colchetes “[ ]” funcionam como conjunto, por exemplo:

In [52]: re.findall("[abcdef]", "The book\nis on the\nbook table", re.MULTILINE)
Out[52]: ['e', 'b', 'e', 'b', 'a', 'b', 'e']

Ele entrou dentro do conjunto de caracteres e fez uma busca para cada ocorrência bem sucedida.

Podemos também buscar pelos caracteres que não estão dentro da sequencia declarada dentro dos colchetes:

In [56]: re.findall("[^abcdef]", "The book\n", re.MULTILINE)
Out[56]: ['T', 'h', ' ', 'o', 'o', 'k', '\n']

Existem também os ranges, declaramos eles com o ‘-‘ hifen, por exemplo:

In [57]: re.findall("[a-c]", "The book\n", re.MULTILINE)
Out[57]: ['b']

In [58]: re.findall("[a-h]", "The book\n", re.MULTILINE)
Out[58]: ['h', 'e', 'b']

In [60]: re.findall("[a-zA-Z]", "The book\n", re.MULTILINE)
Out[60]: ['T', 'h', 'e', 'b', 'o', 'o', 'k']

Podemos usar ranges para números e caracteres, segue um exemplo de um range que abrange todos os caracteresque formam palavras:

In [61]: re.findall("[a-zA-Z0-9_]", "The book\n", re.MULTILINE)
Out[61]: ['T', 'h', 'e', 'b', 'o', 'o', 'k']

Esse range é bem útil para filtrar campos de formulário em sites, e por isso usamos bastante esse padrão, por isso existe atalho para abreviar a digitação, o \w :

In [63]: re.findall("\w", "The book @\n", re.MULTILINE)
Out[63]: ['T', 'h', 'e', 'b', 'o', 'o', 'k']

Existem outras abreviaturas para ranges, são elas:

  • \d == [0-9]
  • \D == [^0-9]
  • \s == [\t\n\r\f\v]
  • \S == [^\t\n\r\f\v]
  • \w == [a-zA-Z0-9_]
  • \W == [^a-zA-Z0-9_]

Todas sequências especiais começam com “\”.  Isso gera problemas já que nosso texto pode vir com \ espalhadas no seu conteúdo, para isso temos que escapar a \, por exemplo:

Antes:

olá \n

Correto:

olá \\n

Para evitar dores de cabeça e tornar o código mais legível podemos usar as Raw strings, alertando o python de que aquela string não contem caracteres especiais, por exemplo:

In [88]: re.match(r'\\www', r'\www.otimo.com.gr')
Out[88]: <_sre.SRE_Match object; span=(0, 4), match='\\www'>

Podemos usar o metacaractere  | para expressões do tipo OU:

In [91]: re.match('ww|ss', r'www.otimo.com.gr')
Out[91]: <_sre.SRE_Match object; span=(0, 2), match='ww'>

Expressões regulares também suportam repetições:

In [96]: re.match(r'\w{5}', 'abcdef')
Out[96]: <_sre.SRE_Match object; span=(0, 5), match='abcde'>

In [97]: re.match(r'\w{5}', 'abcdefg')
Out[97]: <_sre.SRE_Match object; span=(0, 5), match='abcde'>

In [98]: re.match(r'\w{5}', 'abcdefg df')
Out[98]: <_sre.SRE_Match object; span=(0, 5), match='abcde'>

In [99]: re.match(r'\w{5}', 'abcd')

In [100]:

Note que ele pegou apenas textos com mais de 4 caracteres, os textos que excederam esse número foram processado mas retornaram apenas o limite de 5 caracteres.

Essa repetição pode ser configurada para retornar os caracteres excedentes, bastando colocar uma vírgula:

In [100]: re.match(r'\w{5,}', 'abcdefg df')
Out[100]: <_sre.SRE_Match object; span=(0, 7), match='abcdefg'>

Note que ele não pegou o “df”
Como você já suspeita, é possível configurar o mínimo e o máximo nas expressões:

In [108]: re.match(r'\w{2,6}', 'abcdefg df')
Out[108]: <_sre.SRE_Match object; span=(0, 6), match='abcdef'>

O metacaractere “?” é usado de duas formas, para limitar o mínimo de repetições (equivalente a {,1}) e para transformar expressões greed em expressões  lazy, por exemplo:

# Transformando a expressão greed em lazy
In [111]: re.match(r'\w{2,}', 'abcdefg df')
Out[111]: <_sre.SRE_Match object; span=(0, 7), match='abcdefg'>

In [112]: re.match(r'\w{2,}?', 'abcdefg df')
Out[112]: <_sre.SRE_Match object; span=(0, 2), match='ab'>

Já o metacaractere “*” é usado para pegar zero ou mais ocorrências ( equivalente a {,} ) :

In [120]: re.match(r'\w', 'abcdefg df')
 Out[120]: <_sre.SRE_Match object; span=(0, 1), match='a'>

In [121]: re.match(r'\w*', 'abcdefg df')
 Out[121]: <_sre.SRE_Match object; span=(0, 7), match='abcdefg'>

O metacaractere “+” é usado para pegar 1 ou mais ocorrências  ( equivale a { 1 , } ):

In [128]: re.match(r'\w+', '')

In [129]: re.match(r'\w+', 'abcdefg df')
Out[129]: <_sre.SRE_Match object; span=(0, 7), match='abcdefg'>

Um exemplo prático do uso do caractere “?” em uma  expressão:

In [132]: re.findall(r'".+"?', 'src="blablabla" alt="altaltalt"')
Out[132]: ['"blablabla" alt="altaltalt"']

Note que queriamos apenas os valores dos atributos de forma separada, mas como o “+” é greedy ele trouxe o resto da expressão, para resolver:

In [133]: re.findall(r'".+?"', 'src="bla bla bla" alt="altaltalt"')
Out[133]: ['"bla bla bla"', '"altaltalt"']

Para buscar valores vazios nos atributos temos que substituir o + por * :

In [134]: re.findall(r'".+?"', 'src="" alt=""')
Out[134]: ['"" alt="']

In [135]: re.findall(r'".*?"', 'src="" alt=""')
Out[135]: ['""', '""']

Um exemplo fazendo parsing de uma tag HTML:

In [136]: html = '<input type="email" id="id_email" name="user mail">'

In [137]: padrao = r'<(.+?) type="(.+?)" id="(.+?)" name="(.+?)"'

In [138]: re.match(padrao, html).groups()
Out[138]: ('input', 'email', 'id_email', 'user mail')

É possível criar um dicionário com os resultados da expressão regular:

In [143]: padrao = r'<(?P<tag>.+?) (?:(?:type="(?P<type>.+?)"|id="(?P<id>.+?)"|name="(P<name>.+?)") ?)*'

In [144]: re.match(padrao, html).groups()
Out[144]: ('input', 'email', 'id_email', None)

In [145]: re.match(padrao, html).groupdict()
Out[145]: {'id': 'id_email', 'tag': 'input', 'type': 'email'}

Espero que seja útil 😀

Inspiração:  http://henriquebastos.net/

Problema com PyEnv no OSX Sierra

Pyenv é um gerenciador de instalações do Python, permite a instalação de várias versões do interpretador em paralelo, incluindo pypy, jython stackless etc.

Após instalar o OSX Sierra tive um problema que me impedia de instalar o python 3.5.2:

zipimport.ZipImportError: can't decompress data; zlib not available

Por alguma razão, no Sierra a biblioteca zlib não vem instalado por padrão para uso no stack unix padrão, para resolver isso fiz o seguinte:

xcode-select --install

Com isso uma caixa de diálogo vai se oferecer para instalar o XCode inteiro ou apenas as ferramentas de console. Instalei as ferramentas de console e o problema foi resolvido 🙂

Como lidar com settings.py local vs produção no django

Não é legal colocar no github suas senhas ou configurações especificas da sua máquina de desenvolvimento, e no Django padrão ele praticamente te induz ao erro.

Mas não se preocupe, é bem simples adaptar seu projeto de tal forma que suas configurações locais não se misturem com o código que vai ser publicado. basta adicionar as seguintes linhas no fim do seu arquivo settings.py:

try:
   from local_settings import *
except ImportError, e:
   pass

Agora vc pode editar o local_settings.py de tal forma a ‘re-escrever’ variaveis, listas, tuplas do settings.py do seu projeto. Só não esqueça de manter o local_settings.py fora do seu repositório !

Customizando o prompt interativo do python

Em algumas distros linux notei que o interpretador padrão do Python, aquele invocado pelo comando python no terminal, possuiam autocomplete e histórico. Eu sei que existe o ipython o bpython, mas em várias situações onde eles não estão disponíveis, o interpretador interativo padrão é a melhor solução.

Existe uma variavel de ambiente chamada PYTHONSTARTUP, que guarda o path do seu script de inicialização, por exemplo:

export PYTHONSTARTUP="~/.pythonstartup"

O conteúdo de ~/.pythonstartup pode ser customizando à vontade, e ainda existe uma documentação básica sobre o assunto aqui.

Um exemplo de pythonstartup :

import readline
import rlcompleter
import atexit
import os
from datetime import datetime as d

readline.parse_and_bind('tab: complete')

histfile = os.path.join(os.environ['HOME'], '.pythonhistory')

try:
    readline.read_history_file(histfile)
except IOError:
    pass

atexit.register(readline.write_history_file, histfile)

def isodate():
    return d.now().isoformat()

del os, histfile, readline, rlcompleter

Como você pode ver é possível adicionar funções e objetos personalizados para tornar seu prompt mais flexivel.