Todos os artigos

Como remover e-mails inválidos e duplicados de um arquivo CSV

··7 min de leitura

Para remover e-mails inválidos e duplicados de um CSV, converta a coluna de e-mail para minúsculas e tire os espaços, deduplique com base nesse valor limpo, descarte os endereços que falham na checagem de sintaxe e, por fim, descarte os domínios sem servidor de e-mail. O Excel e o Google Sheets fazem as três primeiras etapas; um script Python curto faz as quatro e registra por que cada linha foi removida.

As checagens de sintaxe e de MX pegam erros de formatação e domínios mortos. Elas não dizem se uma caixa postal existe, o que exige uma conversa SMTP com o servidor do destinatário. Trate isto como a primeira passada gratuita antes da verificação de verdade.

O que cada método consegue fazer

Tarefa Excel Google Sheets Script Python abaixo
Minúsculas e remoção de espaços Sim Sim Sim
Espaços não separáveis Precisa de SUBSTITUTE(…, CHAR(160), " ") Também precisa de SUBSTITUTE Tratado
Remover duplicados Sim Sim Sim, mantém o primeiro
Checagem de sintaxe Regex só no Microsoft 365 Sim, REGEXMATCH Sim
Registrar por que cada linha saiu Manual Manual Automático
Checagem de MX Não Não Sim, com dnspython
A caixa postal existe Não Não Não

Para arquivos de alguns milhares de linhas que você limpa uma vez, uma planilha resolve. Para limpezas repetidas, arquivos muito grandes ou qualquer coisa que precise de trilha de auditoria, use o script.

No Excel ou no Google Sheets

Normalize numa coluna auxiliar, faça a checagem de sintaxe nessa coluna e depois deduplique por ela. Temos guias completos e testados para o Excel e para o Google Sheets, então aqui vai a versão curta.

  1. Coluna auxiliar: =LOWER(TRIM(SUBSTITUTE(A2, CHAR(160), " "))). O SUBSTITUTE está ali porque tanto o TRIM do Excel quanto o Remover espaços em branco do Sheets deixam os espaços não separáveis no lugar.
  2. Sintaxe: REGEXMATCH no Sheets, REGEXTEST no Excel para Microsoft 365, com o padrão usado no script abaixo. O Excel mais antigo precisa de uma fórmula alternativa mais longa que, no nosso teste, deixou passar quatro endereços malformados.
  3. Deduplique pela coluna auxiliar com Remover duplicatas (a Microsoft documenta que o Excel mantém a primeira ocorrência), ou com UNIQUE numa nova aba.
  4. Filtre as linhas que falharam e salve como CSV.

Em Python: um script testado de 20 linhas

Este script limpa, checa e deduplica numa única passada, e grava as linhas rejeitadas num arquivo separado, com o motivo. Ele usa só a biblioteca padrão do Python.

import csv, re, sys

PATTERN = re.compile(r"^[a-z0-9_%+'-]+(\.[a-z0-9_%+'-]+)*@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$")

src, column = sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else "email"
seen = set()
with open(src, newline="", encoding="utf-8-sig") as f, \
     open("clean.csv", "w", newline="", encoding="utf-8") as ok, \
     open("rejected.csv", "w", newline="", encoding="utf-8") as bad:
    reader = csv.DictReader(f)
    good = csv.DictWriter(ok, fieldnames=reader.fieldnames)
    rejected = csv.DictWriter(bad, fieldnames=reader.fieldnames + ["reason"])
    good.writeheader(); rejected.writeheader()
    for row in reader:
        email = (row.get(column) or "").replace(" ", " ").strip().lower()
        reason = "empty" if not email else "bad syntax" if not PATTERN.match(email) else "duplicate" if email in seen else ""
        if reason:
            rejected.writerow({**row, "reason": reason})
            continue
        seen.add(email)
        good.writerow({**row, column: email})
print(f"kept {len(seen)} unique addresses")

Rode com o nome do arquivo e, se a coluna não se chamar email, com o nome da coluna:

python3 clean_emails.py leads.csv
python3 clean_emails.py leads.csv "Email Address"

Ele grava clean.csv (todas as colunas originais, com o e-mail normalizado) e rejected.csv (as linhas originais mais uma coluna reason: empty para vazio, bad syntax para sintaxe inválida ou duplicate para duplicado).

O que aconteceu quando rodamos

Rodamos o script em 1º de outubro de 2026 (Python 3.14, macOS) com um arquivo de teste de 19 linhas montado a partir dos erros que aparecem em exportações reais. Ele imprimiu kept 4 unique addresses.

Entrada Resultado
ana.silva@example.com Mantido (primeira ocorrência)
Ana.Silva@Example.com Rejeitado: duplicate
ana.silva@example.com + espaço no final Rejeitado: duplicate
ana.silva@example.com + espaço não separável Rejeitado: duplicate
o'brien@example.co.uk Mantido
jo+news@example.com Mantido
kai@sub.example.io Mantido
ben@example Rejeitado: bad syntax
cara@@example.com Rejeitado: bad syntax
dev@example..com Rejeitado: bad syntax
eve@example,com Rejeitado: bad syntax
gus.@example.com Rejeitado: bad syntax
max@-example.com Rejeitado: bad syntax
ned@example.c Rejeitado: bad syntax
mailto:finn@example.com Rejeitado: bad syntax
Pia <pia@example.com> Rejeitado: bad syntax
ola@example.com;ola2@example.com Rejeitado: bad syntax
hana o@example.com Rejeitado: bad syntax
(em branco) Rejeitado: empty

As quatro últimas linhas rejeitadas contêm endereços reais embrulhados em lixo. É por isso que o script grava as rejeições num arquivo em vez de descartá-las: abra o rejected.csv, corrija à mão o que é obviamente corrigível e devolva essas linhas à lista. Não "corrija" as ambíguas. hana o@example.com pode ser hanao@ ou hana.o@, e chutar cria um endereço que ninguém lhe deu.

Duas armadilhas de CSV que o script evita

Separar por vírgulas. A linha eve@example,com fica gravada no arquivo como Eve,"eve@example,com", entre aspas porque contém uma vírgula. Nós conferimos: um line.split(",") ingênuo a transforma em três campos, ['Eve', '"eve@example', 'com"'], enquanto o módulo csv do Python devolve corretamente dois. Use sempre um parser de CSV de verdade.

A marca de ordem de bytes. Alguns programas gravam três bytes invisíveis, uma marca de ordem de bytes (byte order mark, BOM), no início de arquivos UTF-8. Aberto como utf-8 simples, o Python lê o primeiro cabeçalho como 'email', então a busca por email não encontra nada e toda linha parece vazia. Reproduzimos isso com um arquivo de teste de duas linhas. encoding="utf-8-sig" remove a marca se ela estiver lá e não faz nada se não estiver.

Uma observação sobre o espaço não separável: o strip() do Python já o trata como espaço em branco, ao contrário das funções de remoção de espaços das planilhas. O replace explícito também pega um que esteja no meio de um endereço, que então falha corretamente na checagem de sintaxe.

Adicionando uma checagem de MX

Uma checagem de domínio remove endereços em domínios que não recebem e-mail, e é barata porque cada domínio é consultado uma única vez. Ela precisa de um pacote: pip install dnspython.

import csv, sys
import dns.resolver  # pip install dnspython

cache = {}

def mail_status(domain):
    if domain in cache:
        return cache[domain]
    try:
        answers = dns.resolver.resolve(domain, "MX", lifetime=5)
        hosts = [str(r.exchange) for r in answers]
        status = "null MX (accepts no mail)" if hosts == ["."] else "has MX"
    except dns.resolver.NXDOMAIN:
        status = "domain does not exist"
    except dns.resolver.NoAnswer:
        try:
            dns.resolver.resolve(domain, "A", lifetime=5)
            status = "no MX, A record fallback"
        except Exception:
            status = "no mail server"
    except Exception:
        status = "lookup failed, retry"
    cache[domain] = status
    return status

with open(sys.argv[1], newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        email = row["email"]
        print(f"{email:45} {mail_status(email.rsplit('@', 1)[1])}")

Rodado num arquivo de teste de cinco linhas, com dnspython 2.8.0, em 1º de outubro de 2026, ele retornou:

someone@gmail.com                             has MX
someone@example.com                           null MX (accepts no mail)
someone@nonexistent-domain-zz91x.com          domain does not exist
someone@neverssl.com                          no MX, A record fallback
other@gmail.com                               has MX

Conferimos cada um com dig. Duas dessas linhas explicam por que o script é mais longo que um simples "tem registro MX?":

  • Null MX. O example.com publica um registro MX, mas ele é 0 ., que a RFC 7505 define como o sinal de que um domínio não aceita e-mail. Uma checagem que só pergunta "existe registro MX?" deixa esse domínio passar.
  • Fallback para registro A. O neverssl.com não tem registro MX, mas tem um registro A. O resumo da RFC 7505 descreve a entrega de e-mail como feita "primeiro procurando um registro MX e depois procurando um registro A/AAAA como alternativa", então a ausência de MX sozinha não prova que um domínio não pode receber e-mail. Marque esses para verificação em vez de apagá-los.

Remova domain does not exist (domínio não existe), null MX e no mail server (sem servidor de e-mail). Rode de novo mais tarde os lookup failed, retry (consulta falhou, tente de novo), porque um timeout de DNS não diz nada sobre o domínio. A ferramenta de consulta de MX checa domínios avulsos no navegador, se você quiser conferir por amostragem.

O que isto não faz

Nem o script nem uma planilha conseguem dizer se uma caixa postal existe. nobody-here-12345@gmail.com passa na checagem de sintaxe e na de MX, e dá bounce (devolução) se essa caixa postal não existir. Confirmar uma caixa postal exige uma conversa com o servidor de e-mail do destinatário na porta 25, que a maioria das redes domésticas e de nuvem bloqueia; por que os provedores de nuvem bloqueiam a porta 25 traz as nossas medições. Também não enxerga domínios catch-all, que aceitam qualquer endereço.

Passe o arquivo limpo por um verificador para essa etapa. Como você já removeu duplicados e domínios mortos, vai pagar para checar menos linhas.

Resumindo

Normalize antes de deduplicar, use um parser de CSV de verdade, abra os arquivos com utf-8-sig e guarde as linhas rejeitadas com um motivo em vez de apagá-las. Adicione uma checagem de MX que entenda null MX e fallback para registro A. Isso elimina de graça os erros de formatação e os domínios mortos; a checagem no nível da caixa postal é a única etapa que resta, e a calculadora de custo de verificação mostra quanto isso custa para o que sobrou.

Perguntas frequentes

Como remover e-mails duplicados de um CSV?

Primeiro converta a coluna de e-mail para minúsculas e tire os espaços, depois remova os duplicados com base nesse valor limpo, mantendo a primeira ocorrência. Deduplicar a coluna bruta deixa passar cópias que só diferem por maiúsculas ou espaços invisíveis.

Como remover e-mails inválidos de um CSV?

Passe cada endereço por uma checagem de sintaxe para descartar os malformados e depois consulte os registros MX de cada domínio para descartar domínios que não recebem e-mail. Remover endereços cuja caixa postal não existe mais exige um serviço de verificação na camada SMTP; nenhum script local ou planilha faz isso de forma confiável.

Por que meu script diz que nenhuma linha tem e-mail?

Muitas vezes porque o arquivo começa com uma marca de ordem de bytes (BOM), que alguns programas gravam no início de arquivos UTF-8. O Python então lê o primeiro cabeçalho como '\ufeffemail' em vez de 'email'. Abrir o arquivo com encoding='utf-8-sig' remove essa marca.

É seguro converter endereços de e-mail para minúsculas?

Na prática, sim. A RFC 5321 tecnicamente permite que a parte antes do @ diferencie maiúsculas de minúsculas, mas diz que explorar isso prejudica a interoperabilidade e é desaconselhado, e os grandes provedores não criam caixas postais que só diferem por maiúsculas.

Verifique endereços sem limite por USD 29,99/mês

Checagem SMTP real da caixa postal. Sem créditos e sem cobrança por e-mail.

Começar agora

Leia o original em inglês