跳到正文
DEV Community· tmeteor-data·· 4 小時前AI 評分42

用 Python 從網站提取企業電郵和電話

Extracting business emails and phones from websites with Python

AI 導讀

作者提供 Python 腳本及 Apify actor Website Contact Extractor,從企業網站提取已公開的電郵和電話。腳本檢查首頁及 /contact 的 mailto、tel 連結和 JSON-LD;actor 會按連結文字搜尋更多聯絡頁面,並遵守 robots.txt。

正文

I built an Apify actor called Website Contact Extractor, so treat my opinions with that in mind. An AI assistant helped draft this article and I reviewed and edited it. The free script comes first because for a few sites it is enough.

The problem

You have a list of business websites and need a way to reach them: a published email, a phone number, a social profile. Doing it by hand means opening the homepage, hunting for a Contact link, and copying values into a sheet. That scales to about twenty sites before it hurts.

The free route

Businesses that want to be contacted usually publish details in predictable places: mailto: and tel: links on the homepage and on a /contact page, and sometimes structured data in a JSON-LD block (schema.org Organization or LocalBusiness). The script below checks all three, and honors robots.txt before fetching.

import json, re, requests
from urllib.parse import urljoin
from urllib.robotparser import RobotFileParser

UA = "contact-check/0.1 (you@example.com)"

def contacts(base):
    rp = RobotFileParser(urljoin(base, "/robots.txt"))
    rp.read()
    out = {"emails": set(), "phones": set(), "jsonld": []}
    for path in ("/", "/contact"):
        url = urljoin(base, path)
        if not rp.can_fetch(UA, url):
            continue
        r = requests.get(url, timeout=15, headers={"User-Agent": UA})
        if r.status_code != 200:
            continue
        html = r.text
        out["emails"] |= {m.split("?")[0] for m in
                          re.findall(r'href=["\']mailto:([^"\']+)', html, re.I)}
        out["phones"] |= set(re.findall(r'href=["\']tel:([^"\']+)', html, re.I))
        for blk in re.findall(r'<script[^>]+ld\+json[^>]*>(.*?)</script>',
                              html, re.S | re.I):
            try:
                out["jsonld"].append(json.loads(blk))
            except ValueError:
                pass
    return out

print(contacts("https://example.com"))

Politeness matters more than cleverness here:

  • Read robots.txt (the script does) and skip what it disallows.
  • Identify yourself in the User-Agent, with a way to reach you.
  • Fetch one page at a time per site, with a delay when you loop over many pages.
  • Only collect what the business published for contact purposes. A named person's email on a team page can be personal data under rules like GDPR, so think about what you store and why. This is a general caution, not legal advice.

What the free route does not do

The script finds the easy cases and nothing more:

  • Finding the right pages. /contact is a guess. Real sites use /contact-us, /impressum, /about, /locations or a footer link with some other name. You need link-text scoring to find them.
  • Phone formats. tel: values come as (313) 555-0142, 313.555.0142 or +1 313.... Turning them into one comparable format needs a phone-number library and a country assumption.
  • Obfuscated and text emails. Addresses written as info [at] example.com, or just shown as text rather than a mailto: link, are missed.
  • Role versus named. info@ and jane.doe@ are different kinds of lead, and the script does not tell them apart.
  • Social profiles and address. You would need more regexes and JSON-LD parsing for each.
  • Dead sites. Parked domains, DNS failures, bot challenges and timeouts need handling and reporting, or your results silently go missing.
  • Change monitoring. Knowing when a site's published contacts changed means keeping state between runs.

The actor

The actor wraps those problems into one run. You give it websites or bare domains and get one row per site. It fetches the homepage over plain HTTP (no browser), then the best contact, impressum, about, team and privacy pages it finds by link text and URL, up to a configurable cap. It respects robots.txt, fetches pages within one site one after another, and does not use logins or captcha bypass.

Example input:

{
  "startUrls": [{ "url": "https://buddyspizza.com" }],
  "maxPagesPerSite": 5,
  "defaultCountry": "US",
  "includeNamedEmails": true,
  "onlyWithContacts": false,
  "maxSites": 100
}

Output excerpt from a real run (fields trimmed; the full row also has per-source detail and a flat copy of each social field):

{
  "domain": "buddyspizza.com",
  "status": "ok",
  "companyName": "Buddy's Pizza",
  "companyNameSource": "schema.org",
  "emails": [
    { "email": "buddyspizza@buddyspizza.com", "type": "role",
      "foundOn": "https://buddyspizza.com/privacy-policy/",
      "pageType": "privacy", "method": "mailto" }
  ],
  "primaryEmail": "buddyspizza@buddyspizza.com",
  "phones": [
    { "phone": "+18009650505", "national": "(800) 965-0505", "pageType": "home", "method": "tel-link" },
    { "phone": "+18334510774", "national": "(833) 451-0774", "pageType": "contact", "method": "tel-link" }
  ],
  "socials": {
    "facebook": "https://www.facebook.com/buddyspizza",
    "instagram": "https://www.instagram.com/buddyspizzadet",
    "x": "https://x.com/buddyspizza"
  },
  "contactPageUrl": "https://buddyspizza.com/contact/",
  "pagesCrawled": 4
}

Notice where the email came from: the privacy page, not the homepage. That is the kind of thing a two-URL script misses. Each email and phone records the page it was found on and the method (mailto link, tel link and so on), so you can verify any value.

Other options: monitorMode remembers each site's emails, phones and socials between runs and returns only sites that changed; urlIncludePatterns and urlExcludePatterns steer the crawl; sameDomainOnly is on by default.

Limits and pricing

  • It finds only contacts the site publishes. It does not guess addresses and it does not verify that an email is deliverable.
  • A site that loads but publishes no email is still charged, because it was crawled. The row will still show whatever phones or socials exist.
  • Dead, blocked, parked and robots-disallowed sites come back with a status and are not charged.
  • Pricing is pay per event: $0.002 per site crawled plus $0.00005 per run start, so 1,000 sites is about $2.00 whether it crawls one page or five. Volume tiers are scheduled for paid Apify plans; check the Pricing tab for the current figure.
  • Named personal emails can be personal data. Use the output in line with the laws that apply to you.

Actor page: https://apify.com/transparent_meteorite/website-contact-extractor

Disclosure again: I'm the builder of this actor, and an AI assistant helped draft this article; I reviewed it before publishing.

來源:DEV Community · dev.to