Scraping di e-mail con Python: Guida completa con esempi

Commenti: 0

Affinché il direct outreach funzioni, occorre una base solida: un database di indirizzi e-mail reali e aggiornati. È qui che entra in gioco l'email scraping con Python: un modo per raccogliere programmaticamente gli indirizzi dai siti web.

In questa guida vedremo come costruire da zero lo scraping delle e-mail con Python, come gestire le pagine dinamiche, come filtrare e convalidare gli indirizzi raccolti e come utilizzare i dati risultanti in veri flussi di lavoro di marketing o aziendali.

Questo materiale è utile se avete bisogno di:

  • capire come raschiare gli indirizzi e-mail da un sito web con Python da soli, senza servizi già pronti;
  • automatizzare la creazione di mailing list per newsletter, CRM o ricerche;
  • collegare il codice a casi d'uso reali, dall'estrazione all'integrazione.

Successivamente, vedremo come trasformare le pagine disponibili pubblicamente in un canale di comunicazione diretta con persone che potrebbero diventare vostri clienti, utilizzando Python.

Che cos'è lo scraping delle e-mail e come può essere utile

Di base, questo tipo di scraping consiste nell'analizzare automaticamente le pagine HTML o dinamiche e cercare nel contenuto o negli attributi modelli che corrispondano ai formati degli indirizzi (ad esempio, username@domain.tld). Quindi si filtrano, si convalidano e si salvano i risultati.

Attività in cui viene utilizzato Python Email Scraper

È ampiamente utilizzato nelle attività commerciali, nel marketing, nella ricerca e nell'automazione dei processi di routine. È particolarmente utile quando è necessario raccogliere e strutturare un grande volume di informazioni pubbliche provenienti da più fonti.

Esempi di attività specifiche in cui viene applicato lo scraping di e-mail con Python:

  • Creare un database di contatti per le campagne e-mail;
  • Marketing e lead generation;
  • Ricerca e analisi dei contatti disponibili pubblicamente;
  • Popolamento e aggiornamento dei sistemi CRM;
  • Monitoraggio dell'attività dei concorrenti;
  • Controllo e verifica dei propri dati di contatto.

Se siete interessati a raccogliere dati di contatto per progetti di e-commerce, esplorate la nostra guida su Scraping di dati di e-commerce.

Le basi: Strumenti e preparazione

Per rendere efficace lo scraping, è necessario preparare l'ambiente e scegliere gli strumenti giusti. Questi strumenti aiutano a recuperare i dati più velocemente, a gestire pagine complesse o dinamiche e a organizzare progetti più ampi.

Scegliere le biblioteche per raschiare gli indirizzi e-mail

Strumenti Python comuni per lo scraping:

Strumento Utilizzo
requests / httpx Recupero di pagine statiche
BeautifulSoup Parsing HTML / ricerca di elementi
re (espressioni regolari) Estrazione di modelli
lxml Parsing più veloce
Selenium / Playwright Gestione di pagine guidate da JavaScript
Scarti Un framework su larga scala per crawl di grandi dimensioni

Preparazione dell'ambiente di lavoro

  1. Creare un ambiente virtuale (venv o virtualenv).
  2. Installare le dipendenze:
    pip install requests beautifulsoup4 lxml
    pip install selenium  # if you need dynamic rendering
  3. (Se necessario) impostare un driver del browser (ChromeDriver, GeckoDriver).
  4. Preparate un elenco di URL o domini di partenza.
  5. Decidere la strategia di attraversamento: ricorsiva o limitata.

Per vedere come vengono applicati metodi simili ad altre piattaforme, consultate la nostra guida dettagliata su scrape Reddit usando Python.

Esempio: Scraping di e-mail con Python - Logica di base (pseudocodice)

# 1. Create an HTTP session with timeouts and retries
session = make_session()
# 2. Load the page
html = session.get(url)
# 3. Look for email addresses:
#    - via regex across the entire text
#    - via mailto: links in HTML
emails = extract_emails_from_text(html)
emails.update(find_mailto_links(html))
# 4. Return a unique list of addresses
return emails

Perché in questo modo?

  • Sessione + tentativi - per evitare fallimenti casuali ed eseguire richieste ripetute su errori.
  • Regex + mailto: - due percorsi semplici ed efficaci da subito.
  • lxml in BeautifulSoup - un parser HTML più veloce e preciso.
  • Normalizzazione del mailto: - eliminare tutto ciò che è extra (?subject=...), mantenendo solo l'indirizzo.

Una variante estesa: Crawler multilivello

"""
Iterate over internal links within one domain and collect email addresses.
Highlights:
- Page limit (max_pages) to stop safely
- Verifying that a link belongs to the base domain
- Avoiding re-visits
- Optional respect for robots.txt
"""

from __future__ import annotations
from collections import deque
from typing import Set
from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit
import time
import requests
from bs4 import BeautifulSoup
import lxml # Import lxml to ensure it's available for BeautifulSoup
from urllib import robotparser  # standard robots.txt parser
# We use functions from the previous block:
# - make_session()
# - scrape_emails_from_url()
import re

# General regular expression for email addresses
EMAIL_RE = re.compile(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9.-]+")

def scrape_emails_from_url(url: str, session: requests.Session) -> Set[str]:
   """Collect email addresses from the given URL page."""
   emails: Set[str] = set()
   try:
       resp = session.get(url, timeout=getattr(session, "_default_timeout", 10.0))
       resp.raise_for_status()
       # Regular expression for email addresses
       # Note: this regex isn't perfect, but it's sufficient for typical cases
       email_pattern = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
       emails.update(email_pattern.findall(resp.text))
   except requests.RequestException:
       pass
   return emails

def make_session() -> requests.Session:
   """Create and return a requests session with basic settings."""
   session = requests.Session()
   session.headers.update({
       "User-Agent": "EmailScraper/1.0",
       "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
       "Accept-Language": "en-US,en;q=0.9",
       # Don't force Accept-Encoding to avoid br issues without brotli
       "Connection": "keep-alive",
   })
   return session
def same_host(url: str, base_netloc: str) -> bool:
   """True if the link belongs to the same host (domain/subdomain)."""
   return urlparse(url).netloc == base_netloc
def load_robots(start_url: str, user_agent: str = "EmailScraper") -> robotparser.RobotFileParser:
   """Read robots.txt and return a parser for permission checks."""
   base = urlparse(start_url)
   robots_url = f"{base.scheme}://{base.netloc}/robots.txt"
   rp = robotparser.RobotFileParser()
   rp.set_url(robots_url)
   try:
       rp.read()
   except Exception:
       pass
   rp.useragent = user_agent
   return rp

def normalize_url(url: str, base: str | None = None) -> str | None:
   try:
       abs_url = urljoin(base, url) if base else url
       parts = urlsplit(abs_url)
       if parts.scheme not in ("http", "https"):
           return None
       host = parts.hostname
       if not host:
           return None
       host = host.lower()
       netloc = host
       if parts.port:
           netloc = f"{host}:{parts.port}"
       parts = parts._replace(fragment="")
       return urlunsplit((parts.scheme.lower(), netloc, parts.path or "/", parts.query, ""))
   except Exception:
       return None

def in_scope(url: str, base_host: str, include_subdomains: bool) -> bool:
   try:
       host = urlsplit(url).hostname
       if not host:
           return False
       host = host.lower()
       base_host = (base_host or "").lower()
       if include_subdomains:
           return host == base_host or host.endswith("." + base_host)
       else:
           return host == base_host
   except Exception:
       return False
def collect_emails_from_site(
   start_url: str,
   max_pages: int = 100,
   delay_sec: float = 0.5,
   respect_robots: bool = True,
   include_subdomains: bool = True,
) -> Set[str]:
   """
   Traverse pages within a domain and return unique email addresses.
   - max_pages: hard limit on visited pages.
   - delay_sec: polite pause between requests.
   - respect_robots: if True — checks access rules.
   - include_subdomains: if True — allows subdomains (www, etc.).
   """
   session = make_session()
   base_host = (urlparse(start_url).netloc or "").lower()
   visited: Set[str] = set()
   queue: deque[str] = deque()
   enqueued: Set[str] = set()
   all_emails: Set[str] = set()

   start_norm = normalize_url(start_url)
   if start_norm:
       queue.append(start_norm)
       enqueued.add(start_norm)

   rp = load_robots(start_url, user_agent="EmailScraper/1.0") if respect_robots else None

   while queue and len(visited) < max_pages:
       url = queue.popleft()
       if url in visited:
           continue

       # robots.txt check
       if respect_robots and rp is not None:
           try:
               if not rp.can_fetch("EmailScraper/1.0", url):
                   continue
           except Exception:
               pass

       # One request: used both for emails and links
       try:
           resp = session.get(url, timeout=10)
           resp.raise_for_status()
           html_text = resp.text or ""
       except requests.RequestException:
           continue

       visited.add(url)

       # Skip non-HTML pages
       ctype = resp.headers.get("Content-Type", "")
       if ctype and "text/html" not in ctype:
           continue

       # Collect emails
       for m in EMAIL_RE.findall(html_text):
           all_emails.add(m.lower())

       # Parse links
       soup = BeautifulSoup(html_text, "lxml")

       # Emails from mailto:
       for a in soup.find_all("a", href=True):
           href = a["href"].strip()
           if href.lower().startswith("mailto:"):
               addr_part = href[7:].split("?", 1)[0]
               for piece in addr_part.split(","):
                   email = piece.strip()
                   if EMAIL_RE.fullmatch(email):
                       all_emails.add(email.lower())

       for a in soup.find_all("a", href=True):
           href = a["href"].strip()
           if not href or href.startswith(("javascript:", "mailto:", "tel:", "data:")):
               continue
           next_url = normalize_url(href, base=url)
           if not next_url:
               continue
           if not in_scope(next_url, base_host, include_subdomains):
               continue
           if next_url not in visited and next_url not in enqueued:
               queue.append(next_url)
               enqueued.add(next_url)

       if delay_sec > 0:
           time.sleep(delay_sec)

   try:
       session.close()
   except Exception:
       pass
   return all_emails
if __name__ == "__main__":
   import argparse

parser = argparse.ArgumentParser(
   description="An email scraper that traverses pages within a site and prints discovered addresses."
)

parser.add_argument(
   "start_url",
   help="Starting URL, for example: https://example.com"
)

parser.add_argument(
   "--max-pages",
   type=int,
   default=100,
   dest="max_pages",
   help="Maximum number of pages to traverse (default: 100)"
)

parser.add_argument(
   "--delay",
   type=float,
   default=0.5,
   help="Delay between requests in seconds (default: 0.5)"
)

parser.add_argument(
   "--no-robots",
   action="store_true",
   help="Ignore robots.txt (use carefully)"
)

scope = parser.add_mutually_exclusive_group()

scope.add_argument(
   "--include-subdomains",
   dest="include_subdomains",
   action="store_true",
   default=True,
   help="Include subdomains (default)"
)

scope.add_argument(
   "--exact-host",
   dest="include_subdomains",
   action="store_false",
   help="Restrict traversal to the exact host (no subdomains)"
)

parser.add_argument(
   "--output",
   type=str,
   default=None,
   help="Optional: path to a file to save found email addresses (one per line)"

   args = parser.parse_args()

   emails = collect_emails_from_site(
       args.start_url,
       max_pages=args.max_pages,
       delay_sec=args.delay,
       respect_robots=not args.no_robots,
       include_subdomains=args.include_subdomains,
   )

   for e in sorted(emails):
       print(e)

   print(f"Found {len(emails)} unique emails.")

   if args.output:
       try:
           with open(args.output, "w", encoding="utf-8") as f:
               for e in sorted(emails):
                   f.write(e + "\n")
       except Exception as ex:
           print(f"Could not write the output file: {ex}")

Come eseguire e configurare lo script esteso

main.py https://example.com
Parametri dello script
  • start_url - l'URL di partenza da cui inizia l'attraversamento (ad esempio, https://example.com).
  • --max-pages - numero massimo di pagine da attraversare. Valore predefinito: 100.
  • --delay - ritardo tra le richieste in secondi per ridurre il carico del server. Valore predefinito: 0,5.
  • --no-robots - ignora le regole di robots.txt. Usare con cautela, perché un sito potrebbe non consentire l'attraversamento automatico.
  • --include-subdomains - include i sottodomini durante l'attraversamento. Abilitato per impostazione predefinita.
  • --exact-host - limita l'attraversamento all'host esatto (senza sottodomini).
  • --output - percorso di un file per salvare gli indirizzi trovati (uno per riga). Se non viene fornito, gli indirizzi vengono stampati nella console.

Gestione dell'offuscamento e dei contenuti dinamici

Quando si esegue uno script, le cose non sono sempre semplici: molti siti nascondono deliberatamente gli indirizzi e-mail o li espongono solo dopo il rendering del JavaScript. Ecco cosa può intralciare e come gestirlo.

Problemi potenziali

1. Offuscamento

I siti utilizzano spesso tecniche per nascondere gli indirizzi ai bot:

  • JavaScript che assembla l'indirizzo da parti (ad esempio, utente + "@" + dominio.com);
  • Stringhe criptate o codificate (ad esempio, Base64, entità HTML);
  • Commenti o inserimenti HTML in cui parte dell'indirizzo rimane nascosta;
  • L'e-mail è un'immagine (un'immagine di testo), nel qual caso lo script non vede nulla;
  • Sostituzione dei caratteri: user [at] example [dot] com e altre forme "leggibili dall'uomo" (munging degli indirizzi).

2. Pagine dinamiche

I siti moderni caricano spesso il contenuto tramite JavaScript (ad esempio, fetch, AJAX). Un semplice requests.get() può restituire una shell HTML "vuota" senza il contenuto dell'email.

Come superare questi ostacoli

Approcci pratici quando si incontrano pagine di questo tipo:

  1. Selenium o Playwright:

    Avviare un browser, lasciare che la pagina venga "caricata", attendere gli elementi richiesti, quindi acquisire l'intero HTML. Questo funziona quando l'e-mail è iniettata da JS dopo il rendering.

  2. Chiamate API:

    Spesso la pagina estrae davvero i dati da un'API. Controllare le richieste di rete (DevTools → Network) per vedere se c'è una richiesta che restituisce l'e-mail o le informazioni di contatto in JSON. In caso affermativo, è meglio usare direttamente l'API.

  3. Parsing di JS/scritture inline:

    A volte l'indirizzo è "incorporato" in JavaScript (ad esempio, una stringa Base64 o suddivisa in parti). È possibile interpretare tale JS, estrarre la stringa e decodificare l'indirizzo.

  4. Se l'e-mail è un'immagine:

    Scaricare l'immagine e applicare l'OCR (Optical Character Recognition), ad esempio con Tesseract. Questa operazione richiede più risorse, ma a volte è necessaria.

  5. Ritardi e tempistiche:

    Alcuni elementi appaiono dopo alcuni secondi o dopo eventi specifici (scorrimento, clic). È opportuno che:

    • utilizzare sleep() o attendere un selettore;
    • tentare più volte;
    • applicare strategie di "riprova se non trovato".

Conclusione

Applicando le tecniche discusse in questo articolo per lo scraping delle e-mail con Python, è possibile far funzionare i propri script in modo affidabile in condizioni reali. Tenete presente che la qualità dei dati influisce direttamente sull'efficacia delle campagne successive, quindi vale la pena di implementare fin dall'inizio il filtraggio, la convalida e il salvataggio in un formato conveniente.

Commenti:

0 Commenti