Spaces:
Sleeping
Sleeping
File size: 5,793 Bytes
76089f2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | import re
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
from typing import Optional
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
}
EMAIL_REGEX = re.compile(
r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Z|a-z]{2,7}\b"
)
PHONE_REGEX = re.compile(
r"(?:(?:\+?1\s*(?:[.-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)"
r"|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[.-]\s*)?)"
r"?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\s*(?:[.-]\s*)?([0-9]{4})"
r"|(?:\+\d{1,3}[\s.-]?)?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}"
)
SKIP_EMAIL_DOMAINS = {
"example.com", "test.com", "domain.com", "email.com",
"yourdomain.com", "sentry.io", "wixpress.com"
}
def fetch_page(url: str, timeout: int = 10) -> Optional[BeautifulSoup]:
try:
resp = requests.get(url, headers=HEADERS, timeout=timeout, allow_redirects=True)
resp.raise_for_status()
return BeautifulSoup(resp.text, "lxml")
except Exception:
try:
resp = requests.get(url, headers=HEADERS, timeout=timeout, allow_redirects=True)
return BeautifulSoup(resp.text, "html.parser")
except Exception:
return None
def extract_emails(soup: BeautifulSoup, page_text: str) -> list[str]:
emails = set()
# From mailto links
for tag in soup.find_all("a", href=True):
href = tag["href"]
if href.startswith("mailto:"):
email = href[7:].split("?")[0].strip()
if email:
emails.add(email.lower())
# From page text
for match in EMAIL_REGEX.finditer(page_text):
email = match.group().lower()
domain = email.split("@")[-1]
if domain not in SKIP_EMAIL_DOMAINS and not email.endswith((".png", ".jpg", ".gif")):
emails.add(email)
return list(emails)
def extract_phones(page_text: str) -> list[str]:
phones = set()
for match in PHONE_REGEX.finditer(page_text):
phone = match.group().strip()
if len(re.sub(r"\D", "", phone)) >= 7:
phones.add(phone)
return list(phones)
def extract_company_name(soup: BeautifulSoup, url: str) -> Optional[str]:
# Try OG site name
og_site = soup.find("meta", property="og:site_name")
if og_site and og_site.get("content"):
return og_site["content"].strip()
# Try title tag
title = soup.find("title")
if title and title.text:
name = title.text.strip().split("|")[0].split("-")[0].strip()
if name:
return name
# Fallback: domain name
domain = urlparse(url).netloc.replace("www.", "")
return domain.split(".")[0].title() if domain else None
def extract_social_links(soup: BeautifulSoup) -> dict:
socials = {}
patterns = {
"linkedin": r"linkedin\.com/(?:company|in)/[\w\-]+",
"twitter": r"twitter\.com/[\w]+",
"facebook": r"facebook\.com/[\w\-\.]+",
}
for tag in soup.find_all("a", href=True):
href = tag["href"]
for platform, pattern in patterns.items():
if platform not in socials and re.search(pattern, href, re.IGNORECASE):
socials[platform] = href
return socials
def scrape_url(url: str, config: dict) -> list[dict]:
"""Scrape a single URL and return extracted lead data."""
leads = []
if not url.startswith(("http://", "https://")):
url = "https://" + url
soup = fetch_page(url)
if not soup:
return leads
page_text = soup.get_text(separator=" ", strip=True)
emails = extract_emails(soup, page_text) if config.get("extract_emails", True) else []
phones = extract_phones(page_text) if config.get("extract_phones", True) else []
company_name = extract_company_name(soup, url) if config.get("extract_company_name", True) else None
socials = extract_social_links(soup)
# Apply custom CSS selectors if provided
custom = config.get("custom_selectors", {}) or {}
custom_data = {}
for field, selector in custom.items():
el = soup.select_one(selector)
custom_data[field] = el.get_text(strip=True) if el else None
if emails:
for email in emails:
leads.append({
"company_name": company_name,
"email": email,
"phone": phones[0] if phones else None,
"website": url,
"linkedin_url": socials.get("linkedin"),
"source": "web_scrape",
"status": "new",
"custom_fields": custom_data if custom_data else None,
})
elif company_name or phones:
leads.append({
"company_name": company_name,
"email": None,
"phone": phones[0] if phones else None,
"website": url,
"linkedin_url": socials.get("linkedin"),
"source": "web_scrape",
"status": "new",
"custom_fields": custom_data if custom_data else None,
})
# Follow internal links if configured
if config.get("follow_links") and config.get("max_pages", 1) > 1:
base = f"{urlparse(url).scheme}://{urlparse(url).netloc}"
visited = {url}
queue = []
for a in soup.find_all("a", href=True):
href = a["href"]
full = urljoin(base, href)
if full.startswith(base) and full not in visited:
queue.append(full)
for link in queue[: config["max_pages"] - 1]:
visited.add(link)
sub_leads = scrape_url(link, {**config, "follow_links": False})
leads.extend(sub_leads)
return leads
|