Unidade 9 · o filtro

O filtro da ficha

O caderno já guarda e-mail e CEP. Agora o filtro: o que a ficha aceita. Padrão no módulo re, não dezenas de if.

Nesta unidade "@" in email é frágil. . é qualquer caractere; o ponto de verdade é \.. String crua r"...". ^ e $ (ou fullmatch). \w \d + * ?. Grupos, morsa, re.sub, re.IGNORECASE.

@ no meio não basta

Arquivo valida.py. O cliente digita o e-mail da ficha. Primeira ideia: tem arroba?

email = input("e-mail? ").strip()
if "@" in email:
    print("ok")
else:
    print("inválido")

ana@padaria.com passa. @ sozinho também. Três arrobas também. A pergunta “tem @ no meio” não é a pergunta “parece e-mail”.

Segunda tentativa: usuario, dominio = email.split("@"). Só funciona com um arroba. Dois: ValueError (unidade 5, vírgula extra no caderno — o mesmo contrato quebrado). E ainda: ".com" in dominio aceita com.padaria.foo no meio da string. dominio.endswith(".com") é melhor e ainda deixa passar ana@.com, espaço, ou maluco@.com.com.

Condicional não escala. Cada canto que você tapa abre outro. O que falta é uma linguagem para descrever o formato.

Expressão regular — regex — é um padrão: uma receita do que o texto pode ser. Python traz isso no módulo re, biblioteca padrão (unidade 7: import).

re.search(padrão, texto) devolve um objeto match ou None. No if, match vale True; None vale False. Não chame .group() no None: AttributeError.

import re

email = input("e-mail? ").strip()
if re.search(".+@.+", email):
    print("ok")
else:
    print("inválido")

+ é “um ou mais do que veio antes”. . é qualquer caractere (exceto quebra de linha). Então .+@.+ lê: alguma coisa, arroba, alguma coisa. Já é melhor que "@" in. Ainda aceita muita coisa — inclusive um ponto que você queria literal no domínio.

9.A · re

Importe re e use search, match, fullmatch ou sub.

import re
re.search(r"\d+", texto)
Exemplo: import re
re.search(r"\d+", "senha 12")

Ponto, barra, string crua

Na regex, . não é o ponto de padaria.com. É coringa. maluco@padaria!com passa em .+@.+\.com? Não, se você escapar. Sem escapar, .+@.+.com aceita ana@padariaXcom — o X ocupa o lugar do ponto.

Ponto de verdade: \.. Barra invertida já é especial nas strings do Python (\n é quebra). Se você escrever ".+@.+\.com" sem o r, o interpretador pode comer a barra antes do re ver. Padrão em string crua: r".+@.+\.com" — o \ chega intacto.

if re.search(r".+@.+\.com", email):
    print("ok")

Ainda aceita lixo antes e depois: xxx ana@padaria.com yyy passa, porque search acha o trecho no meio. Validar o campo inteiro é outra pergunta.

9.B · ponto

Em regex, o que . (sem barra) significa?

É o coringa mais famoso da regex.
Domínio .com precisa escapar o ponto.
Solução: qualquer caractere. Literal: \.

Do começo ao fim

^ ancora o começo da string. $ ancora o fim. Juntos: o padrão tem de cobrir o texto inteiro.

if re.search(r"^.+@.+\.com$", email):
    print("ok")

re.fullmatch já implica as duas âncoras — o padrão é a string toda, sem escrever ^ e $. Os dois caminhos valem; na ficha, fullmatch deixa a intenção explícita.

Espaço nas pontas: .strip() antes, crachá da unidade 1. Regex não substitui o hábito de limpar o que veio do teclado.

9.C · âncora

Para exigir que o padrão cubra a string inteira num search, o que se usa?

search acha substring. Validar o campo inteiro pede bordas.
fullmatch já implica “do começo ao fim”.
Solução: âncoras ou fullmatch.
9.D · search no meio

re.search(r".+@.+\.com", "xxx ana@padaria.com yyy") sem ^ e $. Por que passa?

search ≠ fullmatch.
O padrão pode ser um pedaço.
Solução: sem ^ e $, o meio basta.

Maiúscula no domínio

O cliente digita Ana@Padaria.COM. Sem flag, \.com no fim recusa. E-mail no mundo real não distingue maiúscula no domínio. Terceiro argumento (ou flags=):

if re.fullmatch(r".+@.+\.com", email, re.IGNORECASE):
    print("ok")

Flags se combinam com |: re.IGNORECASE | re.MULTILINE. Nesta unidade, uma já resolve o crachá.

9.E · maiúscula

re.fullmatch(r".+@.+\.com", "ana@PADARIA.COM") sem flag. O que acontece?

Padrão é case-sensitive.
Terceiro argumento: re.IGNORECASE.
Solução: não passa. Passe a flag.

Classes e quantificadores

.+ no usuário aceita espaço, segundo arroba, qualquer lixo. Mais honesto: \w — letra, dígito ou _ (“caractere de palavra”). \d é dígito. \s é espaço (e tab, e quebra). Os contrários: \W \D \S.

Conjunto na mão: [a-zA-Z0-9_] é quase o \w. [^@] é “tudo menos arroba” — útil quando o usuário pode ter ponto (ana.silva) mas não um segundo @.

Quantos: + um ou mais; * zero ou mais; ? zero ou um. {5} exatamente cinco; {4,5} de quatro a cinco; {3,} três ou mais. Hífen opcional no CEP: -?.

if re.fullmatch(r"\w+@\w+\.com", email, re.IGNORECASE):
    print("ok")

Ainda é um e-mail de brinquedo — não valida a internet, não cobre ana.silva@padaria.com.br. Serve para o formato da ficha. Produção usa biblioteca pronta; aqui o ponto é ler o padrão.

Cinco, hífen, três

CEP brasileiro: cinco dígitos, hífen opcional, três dígitos. r"\d{5}-?\d{3}". fullmatch no campo inteiro, senão 01310-100-extra passa no meio.

import re

cep = input("CEP: ").strip()
if re.fullmatch(r"\d{5}-?\d{3}", cep):
    print("ok")
else:
    print("inválido")

01310100 e 01310-100 passam. 0131-0100 não: a conta dos dígitos quebrou.

Parênteses que guardam

Até agora o match só dizia sim ou não. Parênteses capturam. r"^(\w+)@(\w+)\.com$": m.group(1) é o usuário, m.group(2) o domínio, m.group(0) o match inteiro. Os pares numeram da esquerda para a direita, de 1 em diante.

Operador morsa (unidade 10 aprofunda): if m := re.fullmatch(...): atribui e testa numa linha. Sem ele, você chama fullmatch duas vezes ou guarda antes do if.

import re

cep = input("CEP: ").strip()
if m := re.fullmatch(r"(\d{5})-?(\d{3})", cep):
    print(m.group(1), m.group(2))

(?:...) agrupa sem capturar — quando você só quer | ou quantificar, sem ocupar group(1). | é “ou”: padaria\.com|fila\.local.

9.F · group

Numa regex com parênteses, o que é group(1)?

group(0) é o pedaço inteiro que bateu.
A numeração dos pares começa em 1.
Solução: o primeiro parêntese.

O link do perfil

O recado chega com URL, não com o @. Às vezes https://instagram.com/ana, às vezes http://www.instagram.com/ana/, às vezes só instagram.com/ana. Você quer ana. split("/") quebra em cada variação de protocolo, www, barra no fim — de novo a floresta de if.

import re

url = input("url? ").strip()
m = re.search(
    r"(?:https?://)?(?:www\.)?instagram\.com/(\w+)",
    url,
    re.IGNORECASE,
)
if m:
    print("@" + m.group(1))

https?: o s é opcional. (?:www\.)?: o www opcional, grupo sem captura — o group(1) continua sendo o nome. Ponto do domínio escapado: instagram\.com.

Trocar o que bateu

re.sub(padrão, troca, texto) devolve a string já substituída. Se não achar, devolve o texto igual. Tirar hífen do CEP, mascarar dígito, colapsar espaço.

limpo = re.sub(r"\D", "", "01310-100")
print(limpo)  # 01310100

Na troca, \1 é o primeiro grupo: re.sub(r"(\d{5})-?(\d{3})", r"\1-\2", cep) normaliza com hífen. String crua na troca também, senão \1 vira outra coisa.

re.MULTILINE faz ^ e $ valerem por linha. re.VERBOSE deixa o padrão quebrado, com comentário ao lado. Entre quando o padrão crescer; o mínimo da unidade é search + âncora + um grupo.

9.G · sub

Um trecho com re.sub(...).

import re no topo, se ainda não tiver.
re.sub(r"\d", "#", texto)
Exemplo: re.sub(r"\d", "#", "senha 12")

Oficina · validar CEP

Problema da unidade Peça um texto e diga se bate num padrão (CEP, e-mail da ficha, só dígitos…). re + print. Arquivo: valida.py.
Oficina 9

Cole um programa com re e print.

import re; texto = input(...)
if re.fullmatch(r"\d{8}", texto): print("ok")
Modelo:
import re
cep = input("CEP: ").strip()
print("ok" if re.fullmatch(r"\d{5}-?\d{3}", cep) else "inválido")