Expressões regulares com o módulo re

1. Introdução ao módulo re e conceitos fundamentais

Expressões regulares (regex) são sequências de caracteres que definem padrões de busca em textos. Em Python, o módulo re fornece todas as ferramentas necessárias para trabalhar com regex de forma eficiente e intuitiva.

import re

# Exemplo básico: buscar um padrão em uma string
texto = "O telefone é 11-99999-8888"
padrao = r"\d{2}-\d{5}-\d{4}"
resultado = re.search(padrao, texto)
if resultado:
    print(f"Telefone encontrado: {resultado.group()}")

Os metacaracteres fundamentais do módulo re incluem:
- . — qualquer caractere (exceto nova linha)
- ^ — início da string
- $ — final da string
- * — zero ou mais repetições
- + — uma ou mais repetições
- ? — zero ou uma repetição
- [] — conjunto de caracteres
- | — operador OU
- () — grupo de captura

# Exemplos de metacaracteres
texto_teste = "casa, carro, conta, curto"
padrao_c = r"c[aeiou]"
resultados = re.findall(padrao_c, texto_teste)
print(resultados)  # ['ca', 'ca', 'co', 'cu']

2. Compilação de padrões e flags

A compilação de padrões com re.compile() oferece vantagens significativas de desempenho quando o mesmo padrão é usado repetidamente.

# Compilação simples
padrao_compilado = re.compile(r"\b\w{5}\b")
texto = "Python é uma linguagem poderosa e versátil"
palavras_5_letras = padrao_compilado.findall(texto)
print(palavras_5_letras)  # ['Python', 'linguagem']

# Compilação com flags
padrao_email = re.compile(
    r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
    re.IGNORECASE
)

texto_emails = "Contato: EMAIL@EXAMPLE.COM ou teste@exemplo.com"
emails = padrao_email.findall(texto_emails)
print(emails)

Flags importantes:
- re.IGNORECASE — ignora diferenças entre maiúsculas/minúsculas
- re.MULTILINE^ e $ correspondem a início/fim de linha
- re.DOTALL. corresponde também a nova linha
- re.VERBOSE — permite espaços e comentários no padrão

# Exemplo com múltiplas flags
padrao_complexo = re.compile(r"""
    ^                    # início da linha
    \d{3}                # código de área
    [-.\s]?              # separador opcional
    \d{3}                # três primeiros dígitos
    [-.\s]?              # separador opcional
    \d{4}                # quatro últimos dígitos
    $                    # final da linha
""", re.VERBOSE | re.MULTILINE)

telefones = """123-456-7890
987.654.3210
555 123 4567"""
print(padrao_complexo.findall(telefones))

3. Funções de busca e correspondência

Cada função de busca tem seu propósito específico:

texto_exemplo = "Python é uma linguagem Pythonica"

# re.match() - busca apenas no início da string
match_inicio = re.match(r"Python", texto_exemplo)
print(match_inicio.group() if match_inicio else "Não encontrado")

# re.search() - busca em toda a string
search_qualquer = re.search(r"Python", texto_exemplo)
print(search_qualquer.group() if search_qualquer else "Não encontrado")

# re.fullmatch() - correspondência exata da string inteira
texto_exato = "Python"
full_match = re.fullmatch(r"Python", texto_exato)
print(full_match.group() if full_match else "Não corresponde")

# re.findall() - retorna todas as ocorrências
todas_ocorrencias = re.findall(r"\b\w+\b", texto_exemplo)
print(todas_ocorrencias)

# re.finditer() - iterador sobre todas as ocorrências
for match in re.finditer(r"\b\w{5}\b", texto_exemplo):
    print(f"Palavra: {match.group()}, posição: {match.start()}-{match.end()}")

4. Grupos, grupos nomeados e referências retroativas

Grupos de captura permitem extrair partes específicas de uma correspondência:

# Grupos de captura tradicionais
data = "15/03/2024"
padrao_data = r"(\d{2})/(\d{2})/(\d{4})"
match = re.search(padrao_data, data)
if match:
    dia, mes, ano = match.groups()
    print(f"Dia: {dia}, Mês: {mes}, Ano: {ano}")

# Grupos nomeados
padrao_nomeado = r"(?P<dia>\d{2})/(?P<mes>\d{2})/(?P<ano>\d{4})"
match_nomeado = re.search(padrao_nomeado, data)
if match_nomeado:
    print(f"Dia: {match_nomeado.group('dia')}")
    print(f"Mês: {match_nomeado.group('mes')}")

# Referências retroativas
texto_repetido = "casa casa"
padrao_repeticao = r"(\b\w+\b) \1"
match_repetido = re.search(padrao_repeticao, texto_repetido)
if match_repetido:
    print(f"Palavra repetida: {match_repetido.group(1)}")

# Referência com grupo nomeado
padrao_nomeado_repetido = r"(?P<palavra>\b\w+\b) (?P=palavra)"
match_nomeado_repetido = re.search(padrao_nomeado_repetido, texto_repetido)
print(f"Palavra repetida (nomeada): {match_nomeado_repetido.group('palavra')}")

5. Substituição e divisão de strings

re.sub() e re.split() são poderosas ferramentas para manipulação de strings:

# Substituição básica
texto_original = "Telefone: 11-9999-8888 e 21-7777-6666"
texto_substituido = re.sub(r"\d{4}-\d{4}", "XXXX-XXXX", texto_original)
print(texto_substituido)

# Substituição com função
def mascarar_telefone(match):
    return match.group(0)[:2] + "XXXX-XXXX"

texto_mascarado = re.sub(r"\d{2}-\d{4}-\d{4}", mascarar_telefone, texto_original)
print(texto_mascarado)

# re.subn() - retorna tupla com resultado e contagem
resultado, contagem = re.subn(r"\d", "#", "Telefone 1234")
print(f"Resultado: {resultado}, Substituições: {contagem}")

# re.split() com padrão complexo
texto_para_dividir = "maçã;banana, laranja|uva"
frutas = re.split(r"[;,\s|]+", texto_para_dividir)
print(frutas)

6. Lookahead e lookbehind (asserções de largura zero)

Asserções de largura zero permitem verificar condições sem consumir caracteres:

# Lookahead positivo
senhas = ["abc123", "123456", "abcde", "a1b2c3"]
padrao_senha = re.compile(r"^(?=.*[a-zA-Z])(?=.*\d).{6,}$")
for senha in senhas:
    if padrao_senha.match(senha):
        print(f"Senha válida: {senha}")

# Lookahead negativo
texto_sql = "SELECT * FROM usuarios WHERE id = 1; DROP TABLE usuarios;"
padrao_seguro = re.compile(r"^(?!.*DROP\s+TABLE).*$", re.IGNORECASE)
if padrao_seguro.match(texto_sql):
    print("Comando SQL seguro")
else:
    print("Comando SQL potencialmente perigoso")

# Lookbehind positivo
precos = "O preço é R$ 50,00 e o desconto é R$ 10,00"
padrao_preco = re.compile(r"(?<=R\$ )\d+,\d{2}")
precos_encontrados = padrao_preco.findall(precos)
print(f"Preços encontrados: {precos_encontrados}")

# Lookbehind negativo
texto_links = "Visite https://exemplo.com ou http://site.com"
padrao_https = re.compile(r"(?<!https:)//[^\s]+")
links_http = padrao_https.findall(texto_links)
print(f"Links HTTP: {links_http}")

7. Tratamento de erros e desempenho

# Tratamento de exceções
try:
    padrao_invalido = re.compile(r"[a-z")
except re.error as e:
    print(f"Erro na expressão regular: {e}")

# Uso de re.DEBUG para depuração
try:
    re.compile(r"\d+\.\d+", re.DEBUG)
except:
    pass  # Saída de debug será mostrada

# Evitando backtracking catastrófico
# Padrão problemático (lento)
padrao_lento = r"(a+)+b"

# Padrão otimizado com grupo atômico (Python 3.11+)
padrao_otimizado = r"(?>a+)+b"

# Exemplo prático de otimização
texto_grande = "a" * 30 + "c"
import time

inicio = time.time()
match_lento = re.search(r"(a+)+b", texto_grande)
fim_lento = time.time()
print(f"Tempo do padrão lento: {fim_lento - inicio:.4f}s")

inicio = time.time()
match_rapido = re.search(r"(?>a+)+b", texto_grande)
fim_rapido = time.time()
print(f"Tempo do padrão otimizado: {fim_rapido - inicio:.4f}s")

8. Casos de uso práticos no ecossistema Python

# Validação de e-mail
def validar_email(email):
    padrao = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
    return bool(re.match(padrao, email))

emails_teste = ["user@example.com", "inválido@", "teste@dominio.org"]
for email in emails_teste:
    print(f"{email}: {'válido' if validar_email(email) else 'inválido'}")

# Validação de URL
def validar_url(url):
    padrao = r"^https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+(?::\d+)?(?:/[-\w%.~+]*)*(?:\?[-\w&=%.~+]*)?(?:#[-\w]*)?$"
    return bool(re.match(padrao, url, re.IGNORECASE))

# Parsing de logs
log_entries = """2024-03-15 10:30:45 ERROR: Conexão perdida
2024-03-15 10:31:12 INFO: Reconexão estabelecida
2024-03-15 10:32:00 WARNING: Latência alta"""

padrao_log = re.compile(
    r"(?P<data>\d{4}-\d{2}-\d{2})\s+"
    r"(?P<hora>\d{2}:\d{2}:\d{2})\s+"
    r"(?P<nivel>ERROR|INFO|WARNING):\s+"
    r"(?P<mensagem>.+)"
)

for match in padrao_log.finditer(log_entries):
    print(f"Data: {match.group('data')}, "
          f"Nível: {match.group('nivel')}, "
          f"Mensagem: {match.group('mensagem')}")

# Limpeza e normalização de dados
dados_sujos = "Nome: João; Idade: 30; Email: joao@email.com"
dados_limpos = re.sub(r"[;:]", ",", dados_sujos)
dados_limpos = re.sub(r"\s+", " ", dados_limpos)
print(f"Dados normalizados: {dados_limpos}")

# Extração de números de telefone em diferentes formatos
telefones_mistos = """
(11) 99999-8888
21 98888-7777
31 9 7777-6666
"""

padrao_telefone = re.compile(r"""
    \(?\d{2}\)?     # código de área com ou sem parênteses
    \s*             # espaço opcional
    9?              # dígito 9 opcional
    \s*             # espaço opcional
    \d{4}           # quatro primeiros dígitos
    -?\s*           # hífen opcional
    \d{4}           # quatro últimos dígitos
""", re.VERBOSE)

telefones_encontrados = padrao_telefone.findall(telefones_mistos)
for tel in telefones_encontrados:
    print(f"Telefone encontrado: {tel.strip()}")

Referências