Todos los artículos

Cómo eliminar correos inválidos y duplicados de un archivo CSV

··7 min de lectura

Para eliminar correos electrónicos inválidos y duplicados de un CSV, pasa la columna de correo a minúsculas y quítale los espacios sobrantes, elimina los duplicados sobre ese valor limpio, descarta las direcciones que no superan una validación de sintaxis y, por último, descarta los dominios sin servidor de correo. Excel y Google Sheets pueden hacer los tres primeros pasos; un script corto de Python hace los cuatro y registra por qué se eliminó cada fila.

Las comprobaciones de sintaxis y de MX detectan errores de formato y dominios muertos. No pueden decirte si un buzón existe, porque para eso hace falta una conversación SMTP con el servidor del destinatario. Tómalo como la primera pasada gratuita antes de la verificación de verdad.

Qué puede hacer cada método

Tarea Excel Google Sheets Script de Python de abajo
Minúsculas y recorte de espacios Sí Sí Sí
Espacios de no separación Necesita SUBSTITUTE(…, CHAR(160), " ") También necesita SUBSTITUTE Resuelto
Eliminar duplicados Sí Sí Sí, conserva el primero
Validación de sintaxis Regex solo en Microsoft 365 Sí, REGEXMATCH Sí
Registrar por qué se eliminó cada fila Manual Manual Automático
Comprobación MX No No Sí, con dnspython
Existencia del buzón No No No

Para archivos de unos pocos miles de filas que limpias una sola vez, una hoja de cálculo basta. Para limpiezas repetidas, archivos muy grandes o cualquier caso en que necesites un registro de auditoría, usa el script.

En Excel o Google Sheets

Normaliza en una columna auxiliar, valida la sintaxis de esa columna y elimina los duplicados sobre ella. Tenemos guías completas y probadas para Excel y Google Sheets, así que aquí va la versión corta.

  1. Columna auxiliar: =LOWER(TRIM(SUBSTITUTE(A2, CHAR(160), " "))). El SUBSTITUTE está ahí porque tanto la función TRIM (ESPACIOS) de Excel como la opción Recortar espacios en blanco de Sheets dejan intactos los espacios de no separación.
  2. Sintaxis: REGEXMATCH en Sheets y REGEXTEST en Excel para Microsoft 365, con el patrón que usa el script de abajo. Las versiones antiguas de Excel necesitan una fórmula alternativa más larga que, en nuestra prueba, dejó pasar cuatro direcciones mal formadas.
  3. Elimina los duplicados sobre la columna auxiliar con Quitar duplicados (Microsoft documenta que Excel conserva la primera aparición) o con UNIQUE en una hoja nueva.
  4. Filtra las filas que no pasaron y guarda como CSV.

En Python: un script probado de 20 líneas

Este script limpia, valida y elimina duplicados en una sola pasada, y escribe las filas rechazadas en un archivo aparte con el motivo. Solo usa la biblioteca estándar de 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")

Ejecútalo con el nombre del archivo y, si la columna no se llama email, con el nombre de la columna:

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

Escribe clean.csv (todas las columnas originales, con el correo normalizado) y rejected.csv (las filas originales más una columna reason con el motivo: empty (vacío), bad syntax (sintaxis incorrecta) o duplicate (duplicado)).

Qué pasó cuando lo ejecutamos

Ejecutamos el script el 1 de octubre de 2026 (Python 3.14, macOS) sobre un archivo de prueba de 19 filas creado con los errores que aparecen en las exportaciones reales. Imprimió kept 4 unique addresses.

Entrada Resultado
ana.silva@example.com Conservada (primera aparición)
Ana.Silva@Example.com Rechazada: duplicate
ana.silva@example.com + espacio final Rechazada: duplicate
ana.silva@example.com + espacio de no separación Rechazada: duplicate
o'brien@example.co.uk Conservada
jo+news@example.com Conservada
kai@sub.example.io Conservada
ben@example Rechazada: bad syntax
cara@@example.com Rechazada: bad syntax
dev@example..com Rechazada: bad syntax
eve@example,com Rechazada: bad syntax
gus.@example.com Rechazada: bad syntax
max@-example.com Rechazada: bad syntax
ned@example.c Rechazada: bad syntax
mailto:finn@example.com Rechazada: bad syntax
Pia <pia@example.com> Rechazada: bad syntax
ola@example.com;ola2@example.com Rechazada: bad syntax
hana o@example.com Rechazada: bad syntax
(vacía) Rechazada: empty

Las últimas cuatro filas rechazadas contienen direcciones reales envueltas en basura. Por eso el script escribe las filas rechazadas en un archivo en lugar de descartarlas: abre rejected.csv, corrige a mano lo que sea obviamente corregible y vuelve a añadir esas filas. No "corrijas" las ambiguas. hana o@example.com podría ser hanao@ o hana.o@, y adivinar crea una dirección que nadie te dio.

Dos trampas del CSV que el script evita

Separar por comas. La fila eve@example,com está guardada en el archivo como Eve,"eve@example,com", entre comillas porque contiene una coma. Lo comprobamos: un line.split(",") ingenuo la convierte en tres campos, ['Eve', '"eve@example', 'com"'], mientras que el módulo csv de Python devuelve correctamente dos. Usa siempre un analizador de CSV de verdad.

La marca de orden de bytes. Algunos programas escriben tres bytes invisibles, una marca de orden de bytes (BOM), al principio de los archivos UTF-8. Si lo abres como utf-8 sin más, Python lee el primer encabezado como 'email', así que una búsqueda de email no encuentra nada y todas las filas parecen vacías. Lo reprodujimos con un archivo de prueba de dos líneas. encoding="utf-8-sig" elimina la marca si está y no hace nada si no está.

Una nota sobre el espacio de no separación: el strip() de Python ya lo trata como espacio en blanco, a diferencia de las funciones de recorte de las hojas de cálculo. El replace explícito también detecta uno que esté en medio de una dirección, que entonces falla correctamente la validación de sintaxis.

Añadir una comprobación MX

Una comprobación de dominio elimina las direcciones de dominios que no pueden recibir correo, y es barata porque cada dominio se consulta una sola vez. Necesita un paquete: 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])}")

Ejecutado sobre un archivo de prueba de cinco filas, con dnspython 2.8.0 el 1 de octubre de 2026, devolvió:

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

Contrastamos cada resultado con dig. Dos de estas filas explican por qué el script es más largo que un simple "¿tiene registro MX?":

  • MX nulo. example.com publica un registro MX, pero es 0 ., que el RFC 7505 define como la señal de que un dominio no acepta correo. Una comprobación que solo pregunta "¿hay un registro MX?" lo da por bueno.
  • Registro A como alternativa. neverssl.com no tiene registro MX, pero sí un registro A. El resumen del RFC 7505 describe la entrega de correo como "primero buscando un registro MX y después buscando un registro A/AAAA como alternativa", así que la falta de MX por sí sola no demuestra que un dominio no pueda recibir correo. Marca estos casos para verificarlos en lugar de eliminarlos.

Elimina domain does not exist (el dominio no existe), null MX (MX nulo) y no mail server (sin servidor de correo). Vuelve a ejecutar más tarde los lookup failed, retry (consulta fallida, reintentar), porque un tiempo de espera agotado en el DNS no dice nada sobre el dominio. La herramienta de consulta MX comprueba dominios sueltos en el navegador si quieres hacer verificaciones puntuales.

Lo que esto no hace

Ni el script ni una hoja de cálculo pueden decirte si un buzón existe. nobody-here-12345@gmail.com supera la validación de sintaxis y la comprobación MX, y rebota si ese buzón no existe. Confirmar un buzón requiere una conversación con el servidor de correo del destinatario en el puerto 25, que la mayoría de las redes domésticas y en la nube bloquean; en por qué los proveedores de nube bloquean el puerto 25 están nuestras mediciones. Tampoco puede detectar los dominios catch-all, que aceptan todas las direcciones.

Para ese paso, pasa el archivo limpio por un verificador. Como ya has eliminado los duplicados y los dominios muertos, pagas por comprobar menos filas.

En resumen

Normaliza antes de eliminar duplicados, usa un analizador de CSV de verdad, abre los archivos con utf-8-sig y conserva las filas rechazadas con su motivo en lugar de borrarlas. Añade una comprobación MX que entienda el MX nulo y el registro A como alternativa. Eso elimina gratis los errores de formato y los dominios muertos; las comprobaciones a nivel de buzón son el único paso que queda, y la calculadora de coste de verificación te muestra cuánto cuesta lo que queda.

Preguntas frecuentes

¿Cómo elimino correos duplicados de un CSV?

Primero pasa la columna de correo a minúsculas y quítale los espacios sobrantes; después elimina los duplicados sobre ese valor limpio, conservando la primera aparición. Si eliminas duplicados sobre la columna sin limpiar, se te escapan las copias que solo se diferencian en mayúsculas o en espacios ocultos.

¿Cómo elimino correos inválidos de un CSV?

Pasa cada dirección por una validación de sintaxis para descartar las mal formadas y luego consulta los registros MX de cada dominio para descartar los dominios que no pueden recibir correo. Para eliminar las direcciones cuyo buzón ya no existe necesitas un servicio de verificación a nivel SMTP; ningún script local ni hoja de cálculo puede hacerlo de forma fiable.

¿Por qué mi script dice que ninguna fila tiene correo?

A menudo porque el archivo empieza con una marca de orden de bytes (BOM), que algunos programas escriben al principio de los archivos UTF-8. Python lee entonces el primer encabezado como '\ufeffemail' en lugar de 'email'. Abrir el archivo con encoding='utf-8-sig' la elimina.

¿Es seguro pasar las direcciones de correo a minúsculas?

En la práctica, sí. El RFC 5321 técnicamente permite que la parte anterior a la @ distinga mayúsculas y minúsculas, pero dice que aprovechar eso dificulta la interoperabilidad y se desaconseja, y los proveedores principales no crean buzones que solo se diferencien en mayúsculas.

Verifica direcciones sin límite por 29,99 USD/mes

Comprobación SMTP real del buzón. Sin créditos ni cobro por correo.

Empezar

Leer el original en inglés