Tous les articles

Comment supprimer les emails invalides et les doublons d'un fichier CSV

··8 min de lecture

Pour supprimer les emails invalides et les doublons d'un CSV, mettez la colonne email en minuscules et retirez les espaces superflus, dédoublonnez sur cette valeur nettoyée, écartez les adresses qui échouent au contrôle de syntaxe, puis écartez les domaines sans serveur de messagerie. Excel et Google Sheets savent faire les trois premières étapes ; un court script Python fait les quatre et note pourquoi chaque ligne a été retirée.

Les contrôles de syntaxe et MX repèrent les erreurs de format et les domaines morts. Ils ne peuvent pas vous dire si une boîte mail existe : cela exige une conversation SMTP avec le serveur du destinataire. Considérez cette étape comme le premier passage gratuit, avant une vraie vérification.

Ce que permet chaque méthode

Tâche Excel Google Sheets Script Python ci-dessous
Minuscules et suppression des espaces Oui Oui Oui
Espaces insécables Nécessite SUBSTITUTE(…, CHAR(160), " ") Nécessite aussi SUBSTITUTE Gérés
Suppression des doublons Oui Oui Oui, garde la première
Contrôle de syntaxe Regex uniquement sur Microsoft 365 Oui, REGEXMATCH Oui
Motif de suppression de chaque ligne Manuel Manuel Automatique
Contrôle MX Non Non Oui, avec dnspython
Existence de la boîte mail Non Non Non

Pour des fichiers de quelques milliers de lignes nettoyés une seule fois, un tableur suffit. Pour des nettoyages répétés, des fichiers très volumineux ou tout ce qui exige une piste d'audit, utilisez le script.

Dans Excel ou Google Sheets

Normalisez dans une colonne auxiliaire, contrôlez la syntaxe de cette colonne, puis dédoublonnez sur celle-ci. Nous avons des tutoriels complets et testés pour Excel et Google Sheets ; voici la version courte.

  1. Colonne auxiliaire : =LOWER(TRIM(SUBSTITUTE(A2, CHAR(160), " "))). Le SUBSTITUTE est là parce que la fonction TRIM d'Excel comme l'outil Supprimer les espaces de Sheets laissent les espaces insécables en place. Dans une version française d'Excel, la même formule s'écrit =MINUSCULE(SUPPRESPACE(SUBSTITUE(A2;CAR(160);" "))) ; dans Google Sheets avec les paramètres régionaux français, remplacez les virgules par des points-virgules.
  2. Syntaxe : REGEXMATCH dans Sheets, REGEXTEST dans Excel pour Microsoft 365, avec le motif utilisé dans le script ci-dessous. Les anciennes versions d'Excel nécessitent une formule de repli plus longue qui, lors de notre test, a laissé passer quatre adresses mal formées.
  3. Dédoublonnez sur la colonne auxiliaire avec Supprimer les doublons (Microsoft indique qu'Excel conserve la première occurrence), ou avec UNIQUE dans une nouvelle feuille.
  4. Filtrez les lignes en échec, puis enregistrez au format CSV.

En Python : un script testé de 20 lignes

Ce script nettoie, contrôle et dédoublonne en un seul passage, et écrit les lignes rejetées dans un fichier séparé, avec un motif. Il n'utilise que la bibliothèque standard 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")

Lancez-le avec le nom du fichier, et le nom de la colonne si elle ne s'appelle pas email :

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

Il écrit clean.csv (toutes les colonnes d'origine, avec l'email normalisé) et rejected.csv (les lignes d'origine plus une colonne reason : empty pour vide, bad syntax pour syntaxe incorrecte ou duplicate pour doublon).

Ce qui s'est passé quand nous l'avons lancé

Nous avons lancé le script le 1er octobre 2026 (Python 3.14, macOS) sur un fichier de test de 19 lignes, construit à partir des erreurs que l'on retrouve dans de vrais exports. Il a affiché kept 4 unique addresses.

Entrée Résultat
ana.silva@example.com Conservée (première occurrence)
Ana.Silva@Example.com Rejetée : duplicate
ana.silva@example.com + espace final Rejetée : duplicate
ana.silva@example.com + espace insécable Rejetée : duplicate
o'brien@example.co.uk Conservée
jo+news@example.com Conservée
kai@sub.example.io Conservée
ben@example Rejetée : bad syntax
cara@@example.com Rejetée : bad syntax
dev@example..com Rejetée : bad syntax
eve@example,com Rejetée : bad syntax
gus.@example.com Rejetée : bad syntax
max@-example.com Rejetée : bad syntax
ned@example.c Rejetée : bad syntax
mailto:finn@example.com Rejetée : bad syntax
Pia <pia@example.com> Rejetée : bad syntax
ola@example.com;ola2@example.com Rejetée : bad syntax
hana o@example.com Rejetée : bad syntax
(vide) Rejetée : empty

Les quatre dernières lignes rejetées contiennent de vraies adresses entourées de parasites. C'est pour cela que le script écrit les rejets dans un fichier au lieu de les jeter : ouvrez rejected.csv, corrigez à la main ce qui est manifestement corrigeable, et réintégrez ces lignes. Ne « corrigez » pas les cas ambigus. hana o@example.com pourrait être hanao@ ou hana.o@, et deviner crée une adresse que personne ne vous a donnée.

Deux pièges du CSV que le script évite

Découper sur les virgules. La ligne eve@example,com est stockée dans le fichier sous la forme Eve,"eve@example,com", entre guillemets parce qu'elle contient une virgule. Nous avons vérifié : un line.split(",") naïf la transforme en trois champs, ['Eve', '"eve@example', 'com"'], alors que le module csv de Python en renvoie correctement deux. Utilisez toujours un vrai analyseur CSV.

La marque d'ordre des octets. Certains programmes écrivent trois octets invisibles, une marque d'ordre des octets (BOM), au début des fichiers UTF-8. Ouvert en simple utf-8, Python lit le premier en-tête comme 'email' : une recherche sur email ne trouve rien et chaque ligne semble vide. Nous l'avons reproduit avec un fichier de test de deux lignes. encoding="utf-8-sig" supprime la marque si elle est présente et ne fait rien sinon.

Un mot sur l'espace insécable : la fonction strip() de Python le traite déjà comme un espace, contrairement aux fonctions de suppression d'espaces des tableurs. Le replace explicite attrape aussi celui qui se trouve au milieu d'une adresse, laquelle échoue alors, à juste titre, au contrôle de syntaxe.

Ajouter un contrôle MX

Un contrôle de domaine retire les adresses dont le domaine ne peut pas recevoir de courrier, et il coûte peu, car chaque domaine n'est interrogé qu'une fois. Il ne demande qu'un paquet : 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])}")

Lancé sur un fichier de test de cinq lignes, avec dnspython 2.8.0, le 1er octobre 2026, il a renvoyé :

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

Nous avons recoupé chaque résultat avec dig. Deux de ces lignes expliquent pourquoi le script va plus loin que « y a-t-il un enregistrement MX ? » :

  • Null MX. example.com publie un enregistrement MX, mais c'est 0 ., que la RFC 7505 définit comme le signal qu'un domaine n'accepte aucun courrier. Un contrôle qui se contente de demander « y a-t-il un enregistrement MX ? » le laisse passer.
  • Repli sur l'enregistrement A. neverssl.com n'a pas d'enregistrement MX, mais possède un enregistrement A. Le résumé de la RFC 7505 décrit la remise du courrier comme se faisant « first by looking for an MX record and then by looking for an A/AAAA record as a fallback » (d'abord en cherchant un enregistrement MX, puis un enregistrement A/AAAA en repli) : l'absence de MX ne prouve donc pas à elle seule qu'un domaine est injoignable. Signalez ces adresses pour vérification au lieu de les supprimer.

Supprimez domain does not exist (le domaine n'existe pas), null MX et no mail server (pas de serveur de messagerie). Relancez plus tard lookup failed, retry (échec de la requête, à réessayer), car un délai DNS dépassé ne dit rien du domaine. L'outil de recherche MX traite un domaine à la fois dans le navigateur si vous voulez faire un contrôle ponctuel.

Ce que cela ne fait pas

Ni le script ni un tableur ne peuvent vous dire si une boîte mail existe. nobody-here-12345@gmail.com passe le contrôle de syntaxe et le contrôle MX, puis rebondit si cette boîte mail n'existe pas. Confirmer une boîte mail exige une conversation avec le serveur de messagerie du destinataire sur le port 25, que la plupart des réseaux domestiques et cloud bloquent ; notre article pourquoi les fournisseurs cloud bloquent le port 25 présente nos mesures. Cela ne permet pas non plus de repérer les domaines catch-all, qui acceptent toutes les adresses.

Pour cette étape, passez le fichier nettoyé dans un vérificateur. Comme vous avez déjà retiré les doublons et les domaines morts, vous payez pour contrôler moins de lignes.

À retenir

Normalisez avant de dédoublonner, utilisez un vrai analyseur CSV, ouvrez les fichiers en utf-8-sig et conservez les lignes rejetées avec leur motif au lieu de les supprimer. Ajoutez un contrôle MX qui comprend le null MX et le repli sur l'enregistrement A. Cela élimine gratuitement les erreurs de format et les domaines morts ; seuls restent les contrôles au niveau de la boîte mail, et le calculateur de coût de vérification indique ce que cela coûte sur ce qui reste.

Questions fréquentes

Comment supprimer les emails en double d'un CSV ?

Mettez d'abord la colonne email en minuscules et supprimez les espaces superflus, puis dédoublonnez sur cette valeur nettoyée en gardant la première occurrence. Dédoublonner la colonne brute laisse passer les copies qui ne diffèrent que par des majuscules ou des espaces invisibles.

Comment supprimer les emails invalides d'un CSV ?

Passez chaque adresse dans un contrôle de syntaxe pour écarter celles qui sont mal formées, puis vérifiez les enregistrements MX de chaque domaine pour écarter les domaines qui ne peuvent pas recevoir de courrier. Supprimer les adresses dont la boîte mail n'existe plus exige un service de vérification au niveau SMTP : aucun script local ni tableur ne peut le faire de façon fiable.

Pourquoi mon script indique-t-il qu'aucune ligne ne contient d'email ?

Souvent parce que le fichier commence par une marque d'ordre des octets (BOM), que certains programmes écrivent au début des fichiers UTF-8. Python lit alors le premier en-tête comme '\ufeffemail' au lieu de 'email'. Ouvrir le fichier avec encoding='utf-8-sig' la supprime.

Peut-on mettre les adresses email en minuscules sans risque ?

En pratique, oui. La RFC 5321 autorise techniquement la partie située avant le @ à être sensible à la casse, mais elle précise que s'en servir nuit à l'interopérabilité et le déconseille, et les grands fournisseurs ne créent pas de boîtes mail qui ne diffèrent que par la casse.

Vérifiez un nombre illimité d’adresses pour 29,99 USD/mois

Vérification SMTP réelle de la boîte aux lettres. Sans crédits ni frais par e-mail.

Commencer

Lire l’article original en anglais