如何用免 API keys 的 Python 腳本搜尋公司網站上的聯絡電郵
How to find a contact email for a list of company websites (free Python script, no API keys)
一個免費 Python 腳本可逐一檢查公司網站首頁及最多四個相關頁面,擷取同一網域的聯絡電郵,並將地址標記為職能帳戶或個人帳戶後寫入 CSV。腳本只使用 Python 標準程式庫,不需 API keys 或瀏覽器;測試的四個 SaaS 網站中有三個找到電郵。 JavaScript 生成內容、網站防護及聯絡表格可能令腳本找不到地址。
Disclosure: I build and sell a paid Apify Actor that automates this workflow. It's mentioned at the end as one option. Everything else here works with free tools. This article was drafted with AI assistance; the code was tested on Oct 27, 2026.
You have a list of 200 company websites from a conference, a directory or a spreadsheet, and you need one contact email for each. Doing it by hand means opening the homepage, hunting for "Contact", scrolling the footer, and copying an address. That's two or three minutes per site, so most of a day for 200.
The good news: most small and mid-size companies publish a contact address on their own site, usually on one of four pages: the homepage footer, /contact, /about or (in Europe) /impressum. A short script can check those pages for you.
What the script does
- Fetches the homepage of each domain.
- Finds links on the same site whose URL contains
contact,kontakt,impressum,about,teamorsupportand fetches up to four of them, with a one-second pause between requests. - Extracts emails in three forms: plain text and
mailto:links, thename [at] domain [dot] comstyle, and Cloudflare'sdata-cfemailprotection (a simple XOR that the browser decodes; we do the same). - Keeps only addresses on the company's own domain, so you don't collect agency or plugin emails.
- Flags each address as role (
info@,hello@,sales@…) or personal (jane@), so you can decide how to use it. - Writes
contacts.csv.
Only the standard library is used, no keys, no browser.
The code
import csv, re, sys, time, urllib.parse, urllib.request
from html import unescape
UA = {"User-Agent": "contact-finder/1.0 (research script; respects robots.txt)"}
EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}")
OBFUSCATED = re.compile(r"([A-Za-z0-9._%+-]+)\s*[\[(]\s*at\s*[\])]\s*([A-Za-z0-9-]+)\s*[\[(]\s*dot\s*[\])]\s*([A-Za-z]{2,24})", re.I)
CFEMAIL = re.compile(r'data-cfemail="([0-9a-f]+)"')
LINK = re.compile(r'href="([^"#]+)"', re.I)
CONTACT_HINTS = ("contact", "kontakt", "impressum", "about", "team", "support")
ROLE = {"info", "hello", "contact", "sales", "support", "admin", "office", "team", "press", "media", "privacy", "legal", "billing", "help", "careers", "jobs", "marketing"}
JUNK = (".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", "example.com", "sentry.io", "wixpress.com")
def fetch(url):
try:
req = urllib.request.Request(url, headers=UA)
with urllib.request.urlopen(req, timeout=15) as r:
if "html" not in r.headers.get("Content-Type", ""):
return ""
return r.read(2_000_000).decode("utf-8", "replace")
except Exception:
return ""
def decode_cf(hexstr):
key = int(hexstr[:2], 16)
return "".join(chr(int(hexstr[i:i+2], 16) ^ key) for i in range(2, len(hexstr), 2))
def emails_in(html):
text = unescape(html)
found = set(EMAIL.findall(text))
found |= {f"{a}@{b}.{c}" for a, b, c in OBFUSCATED.findall(text)}
found |= {decode_cf(h) for h in CFEMAIL.findall(html)}
return {e.lower().strip(".") for e in found if not e.lower().endswith(JUNK)}
def contact_pages(base, html, limit=4):
pages = []
for href in LINK.findall(html):
url = urllib.parse.urljoin(base, href)
if urllib.parse.urlparse(url).netloc != urllib.parse.urlparse(base).netloc:
continue
if any(h in url.lower() for h in CONTACT_HINTS) and url not in pages:
pages.append(url)
return pages[:limit]
def scan(domain):
base = domain if domain.startswith("http") else f"https://{domain}"
home = fetch(base)
if not home:
return {"domain": domain, "status": "unreachable", "emails": ""}
emails = emails_in(home)
for url in contact_pages(base, home):
time.sleep(1) # be polite
emails |= emails_in(fetch(url))
host = urllib.parse.urlparse(base).netloc.removeprefix("www.")
on_domain = sorted(e for e in emails if e.endswith("@" + host) or e.endswith("." + host))
rows = [f"{e} ({'role' if e.split('@')[0] in ROLE else 'personal'})" for e in on_domain]
return {"domain": domain, "status": "ok", "emails": "; ".join(rows)}
if __name__ == "__main__":
domains = [d.strip() for d in open(sys.argv[1]) if d.strip()]
with open("contacts.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=["domain", "status", "emails"])
w.writeheader()
for d in domains:
row = scan(d)
print(row)
w.writerow(row)
Run it with a text file of domains, one per line:
python3 find_contacts.py domains.txt
What I got
I ran it against four well-known SaaS sites:
| Domain | Result |
|---|---|
| apify.com |
hello@, support@ (both role) |
| plausible.io |
hello@ (role) |
| zapier.com | nothing found |
| basecamp.com | one personal address |
Three out of four is typical. Sites like Zapier render their pages with JavaScript and route contact through forms, so there's no address in the HTML at all. For those, the realistic fallback is the contact form or LinkedIn, not more scraping.
Things that will bite you
-
JavaScript-only sites.
urllibsees the raw HTML only. If a site builds its footer in the browser you'll get nothing. A headless browser fixes some of these but costs 10x more time per site. - Bot protection. Some sites return 403 to scripts. Don't try to get around it; mark the site and move on.
-
False positives. Image file names like
logo@2x.pngmatch the email pattern, which is why the script drops common file extensions and keeps only on-domain addresses. - Rate. One request per second per site is polite. Run sites in parallel if you need speed, not pages within one site.
-
Legal. Collecting a published business address is one thing; emailing it is another. Follow CAN-SPAM in the US (real sender, physical address, working unsubscribe) and GDPR in the EU, where personal addresses like
jane@company.comare personal data. Role addresses are the safer default for cold outreach. Respectrobots.txt(the paid version below does by default; addurllib.robotparserto the script if you run it at scale).
If you'd rather not maintain it
I packaged a more complete version as an Apify Actor: Website Email Extractor & Contact Scraper. On top of the above it extracts phone numbers (normalized to E.164), LinkedIn, X, Facebook, Instagram, YouTube, TikTok and GitHub links, company name and address, and about 80 technologies (CMS, ecommerce, analytics, chat). There's an optional WHOIS and MX/SPF/DMARC add-on.
Pricing is per website, not per page: $3 per 1,000 websites however many pages it reads, plus $1 per 1,000 domains for the WHOIS/DNS add-on, and unreachable or bot-protected sites are free. It has the same limits as the script: it reads HTML and won't get past sites that block bots.
Checklist
- [ ] Domain list cleaned (one per line, no duplicates)
- [ ] Homepage plus contact/about/impressum pages checked
- [ ] Obfuscated and Cloudflare-protected emails decoded
- [ ] Only on-domain addresses kept, role vs personal flagged
- [ ] Outreach plan follows CAN-SPAM/GDPR, role addresses first
來源:DEV Community · dev.to