A leads export is never clean. The same person appears twice with different capitalisation, someone typed gmial.com, a phone number is (415) 555-0134 in one row and 415-555-0134 in the next, and there is a bob@@initech that no mail server will ever accept. Mailing that list means bounces, and bounces hurt your sending reputation.
This script reads a CSV with the columns name, company, email, phone and writes a clean one. Standard library only.
What it does, in order:
- Trims and lowercases emails, removes stray spaces, angle brackets, trailing punctuation and
mailto:. - Fixes a short list of common domain typos (
gmial.com,gmail.con,hotmial.comand a few more). The list is a plain dictionary; add the typos you see in your own data. - Validates the shape of each address with one regular expression and drops what fails.
- Drops duplicates by cleaned email, keeping the first row. Duplicates are found after cleaning, so
John.Smith@GMIAL.comandjohn.smith@gmail.comcount as the same person. - Normalises phones to
+<digits>. A number with 10 digits and no plus gets a default country code (1; changedefault_ccfor your market). Anything shorter than 8 or longer than 15 digits becomes empty rather than a wrong number. - Flags role addresses (
support@,info@,sales@) in a separate column so you decide whether to mail them.
#!/usr/bin/env python3 """Clean a messy leads CSV: trim, fix emails, normalise phones, drop duplicates. Standard library only. Usage: python clean_leads.py leads.csv [clean.csv]""" import csv, re, sys EMAIL = re.compile(r"^[a-z0-9._%+-]+@[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}$") TYPOS = {"gmial.com": "gmail.com", "gmail.con": "gmail.com", "gamil.com": "gmail.com", "hotmial.com": "hotmail.com", "outlok.com": "outlook.com", "yaho.com": "yahoo.com"} ROLE = {"info", "admin", "support", "sales", "noreply", "no-reply", "contact", "hello"} def clean_email(raw): e = re.sub(r"\s+", "", raw or "").lower().strip("<>.,;:'\"") e = e.replace("mailto:", "") if "@" in e: user, dom = e.rsplit("@", 1) e = f"{user}@{TYPOS.get(dom, dom)}" return e def clean_phone(raw, default_cc="1"): digits = re.sub(r"\D", "", raw or "") if (raw or "").strip().startswith("+") or len(digits) > 10: pass elif len(digits) == 10: digits = default_cc + digits return ("+" + digits) if 8 <= len(digits) <= 15 else "" def clean_name(raw): s = re.sub(r"\s+", " ", raw or "").strip() return " ".join(w.capitalize() if w.isupper() or w.islower() else w for w in s.split(" ")) def main(src, dst="clean.csv"): seen, out, dropped = set(), [], {"bad_email": 0, "duplicate": 0} with open(src, newline="", encoding="utf-8-sig") as f: for row in csv.DictReader(f): row = {k.strip().lower(): v for k, v in row.items() if k} email = clean_email(row.get("email")) if not EMAIL.match(email): dropped["bad_email"] += 1; continue if email in seen: dropped["duplicate"] += 1; continue seen.add(email) out.append({"name": clean_name(row.get("name")), "company": clean_name(row.get("company")), "email": email, "phone": clean_phone(row.get("phone")), "role_address": "yes" if email.split("@")[0] in ROLE else ""}) with open(dst, "w", newline="", encoding="utf-8-sig") as f: w = csv.DictWriter(f, fieldnames=["name", "company", "email", "phone", "role_address"]) w.writeheader(); w.writerows(out) print(f"kept {len(out)}, dropped {dropped['bad_email']} bad emails and {dropped['duplicate']} duplicates -> {dst}") if __name__ == "__main__": if len(sys.argv) < 2: sys.exit(__doc__) main(*sys.argv[1:3]) A five-row test file:
Name , Company,Email,Phone JOHN smith ,ACME INC, John.Smith@GMIAL.com ,(415) 555-0134 Mary O'Neil,globex,mary@globex.io,+44 20 7946 0958 john smith,Acme Inc,john.smith@gmail.com,415-555-0134 Bob,Initech,bob@@initech,555 Support Team,Hooli,<Support@hooli.com>, python clean_leads.py leads.csv clean.csv kept 3, dropped 1 bad emails and 1 duplicates -> clean.csv name,company,email,phone,role_address John Smith,Acme Inc,john.smith@gmail.com,+14155550134, Mary O'Neil,Globex,mary@globex.io,+442079460958, Support Team,Hooli,support@hooli.com,,yes Limits worth knowing:
- A well-formed address can still bounce. There is no MX lookup and no mailbox check; for that you need a verification service.
- Name capitalisation is naive. It handles
JOHNandsmith, but will not fixmcdonaldintoMcDonald. - The phone logic assumes one default country. If your list mixes countries, add the code to the source column before cleaning.
- Duplicates are matched by email only. Two different emails for the same person stay as two rows.
Run it on a copy of your file first and compare the counts; it prints how many rows it dropped and why.
More ready-to-use prompts and scripts: https://swarmagency.shop/kit.html
S.W.A.R.M / swarm_agency
