Spaces:
Sleeping
Sleeping
File size: 17,015 Bytes
414f0bd | 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 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 | import gradio as gr
import pandas as pd
import requests
from bs4 import BeautifulSoup
from playwright.async_api import async_playwright
import asyncio
import re
import time
import random
from urllib.parse import urljoin, urlparse
from fake_useragent import UserAgent
import spacy
import json
import os
from typing import List, Dict, Tuple, Optional
import logging
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize components
ua = UserAgent()
# Load spaCy model for German NER
try:
nlp = spacy.load("de_core_news_sm")
except OSError:
logger.error("German spaCy model not found. Install with: python -m spacy download de_core_news_sm")
nlp = None
class ImprintScraper:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': ua.random,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
})
# Regex patterns
self.email_pattern = re.compile(r'[\w\.-]+@[\w\.-]+\.\w+')
self.phone_pattern = re.compile(r'(?:\+?\d{1,3})?[\s\-]?(?:\(?\d+\)?[\s\-]?)+')
# Keywords for imprint pages
self.imprint_keywords = [
'impressum', 'imprint', 'kontakt', 'contact', 'about',
'über uns', 'rechtliches', 'legal', 'datenschutz'
]
# Owner keywords (German focus)
self.owner_keywords = [
'inhaber', 'geschäftsführer', 'geschäftsführerin', 'vertretungsberechtigt',
'owner', 'ceo', 'managing director', 'eigentümer', 'betreiber',
'verantwortlich', 'ansprechpartner'
]
def clean_phone(self, phone: str) -> str:
"""Clean and format phone number"""
if not phone:
return ""
# Remove extra whitespace and common separators
phone = re.sub(r'[\s\-\(\)]+', '', phone.strip())
# Remove common prefixes like "Tel:" or "Telefon:"
phone = re.sub(r'^(tel|telefon|phone|fon)[:.]?', '', phone.lower())
# Ensure it looks like a valid phone number
if len(phone) < 6 or len(phone) > 20:
return ""
return phone
def extract_emails(self, text: str) -> List[str]:
"""Extract email addresses from text"""
emails = self.email_pattern.findall(text.lower())
# Filter out common false positives
filtered_emails = []
for email in emails:
if not any(exclude in email for exclude in ['example.', 'test.', 'placeholder']):
filtered_emails.append(email)
return list(set(filtered_emails))
def extract_phones(self, text: str) -> List[str]:
"""Extract phone numbers from text"""
# Find potential phone numbers
potential_phones = self.phone_pattern.findall(text)
cleaned_phones = []
for phone in potential_phones:
cleaned = self.clean_phone(phone)
if cleaned and len(cleaned) >= 6:
cleaned_phones.append(cleaned)
return list(set(cleaned_phones))
def extract_owner_with_spacy(self, text: str) -> Optional[str]:
"""Extract owner name using spaCy NER"""
if not nlp:
return None
doc = nlp(text)
# Look for person entities near owner keywords
text_lower = text.lower()
best_owner = None
min_distance = float('inf')
for ent in doc.ents:
if ent.label_ == "PERSON" and len(ent.text.strip()) > 1:
# Find the closest owner keyword
for keyword in self.owner_keywords:
keyword_pos = text_lower.find(keyword)
if keyword_pos != -1:
distance = abs(ent.start_char - keyword_pos)
if distance < min_distance and distance < 200: # Within 200 characters
min_distance = distance
best_owner = ent.text.strip()
return best_owner
def extract_owner_with_regex(self, text: str) -> Optional[str]:
"""Extract owner name using regex patterns"""
text_lower = text.lower()
for keyword in self.owner_keywords:
# Look for pattern: "Keyword: Name" or "Keyword Name"
patterns = [
rf'{keyword}[:.]?\s*([A-ZÄÖÜ][a-zäöüß]+(?:\s+[A-ZÄÖÜ][a-zäöüß]+)*)',
rf'({keyword})\s*[:.]?\s*([A-ZÄÖÜ][a-zäöüß]+(?:\s+[A-ZÄÖÜ][a-zäöüß]+)*)',
]
for pattern in patterns:
matches = re.search(pattern, text, re.IGNORECASE | re.MULTILINE)
if matches:
# Return the captured name part
name = matches.group(-1).strip()
if len(name) > 1 and not any(skip in name.lower() for skip in ['gmbh', 'ag', 'kg', 'ltd', 'inc']):
return name
return None
def find_imprint_links(self, soup: BeautifulSoup, base_url: str) -> List[str]:
"""Find links that might lead to imprint pages"""
imprint_links = []
for link in soup.find_all('a', href=True):
href = link.get('href', '').lower()
text = link.get_text().lower().strip()
# Check if link text or href contains imprint keywords
for keyword in self.imprint_keywords:
if keyword in text or keyword in href:
full_url = urljoin(base_url, link['href'])
imprint_links.append(full_url)
break
return list(set(imprint_links))
async def scrape_with_playwright(self, url: str) -> Tuple[str, str]:
"""Scrape page content using Playwright for JavaScript rendering"""
try:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(
user_agent=ua.random,
viewport={'width': 1920, 'height': 1080}
)
page = await context.new_page()
# Set timeout and navigate
await page.goto(url, timeout=30000, wait_until='domcontentloaded')
await page.wait_for_timeout(2000) # Wait for JS to execute
content = await page.content()
title = await page.title()
await browser.close()
return content, title
except Exception as e:
logger.error(f"Playwright scraping failed for {url}: {str(e)}")
return "", ""
def scrape_with_requests(self, url: str) -> Tuple[str, str]:
"""Scrape page content using requests (static)"""
try:
# Rotate user agent
self.session.headers['User-Agent'] = ua.random
response = self.session.get(url, timeout=10, allow_redirects=True)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
# Remove script and style elements
for script in soup(["script", "style"]):
script.decompose()
text = soup.get_text()
title = soup.title.string if soup.title else ""
return text, title
except Exception as e:
logger.error(f"Requests scraping failed for {url}: {str(e)}")
return "", ""
async def extract_contact_info(self, url: str) -> Dict[str, str]:
"""Main extraction function for a single URL"""
result = {
'website': url,
'imprint_url': url,
'email': '',
'phone': '',
'owner': '',
'status': 'Processing...'
}
try:
# Step 1: Try with requests first (faster)
logger.info(f"Scraping {url} with requests...")
content, title = self.scrape_with_requests(url)
if not content:
# Step 2: Fallback to Playwright
logger.info(f"Falling back to Playwright for {url}...")
content, title = await self.scrape_with_playwright(url)
if not content:
result['status'] = 'Failed to load page'
return result
# Step 3: Try to find imprint page if this is homepage
soup = BeautifulSoup(content, 'html.parser')
imprint_links = self.find_imprint_links(soup, url)
best_content = content
best_url = url
# Check imprint pages for better information
for imprint_url in imprint_links[:3]: # Check max 3 imprint links
try:
imp_content, imp_title = self.scrape_with_requests(imprint_url)
if not imp_content:
imp_content, imp_title = await self.scrape_with_playwright(imprint_url)
if imp_content and len(imp_content) > len(best_content) * 0.1:
# Use imprint page if it has substantial content
best_content = imp_content
best_url = imprint_url
result['imprint_url'] = imprint_url
break
time.sleep(random.uniform(1, 3)) # Random delay
except Exception as e:
logger.warning(f"Failed to check imprint link {imprint_url}: {str(e)}")
continue
# Step 4: Extract information
emails = self.extract_emails(best_content)
phones = self.extract_phones(best_content)
# Owner extraction with multiple methods
owner = self.extract_owner_with_spacy(best_content)
if not owner:
owner = self.extract_owner_with_regex(best_content)
# Set results
result['email'] = emails[0] if emails else ''
result['phone'] = phones[0] if phones else ''
result['owner'] = owner if owner else ''
result['status'] = 'Success'
if not any([result['email'], result['phone'], result['owner']]):
result['status'] = 'No contact info found'
except Exception as e:
logger.error(f"Error processing {url}: {str(e)}")
result['status'] = f'Error: {str(e)}'
return result
async def process_urls(urls: List[str], progress_callback=None) -> pd.DataFrame:
"""Process multiple URLs and return results as DataFrame"""
scraper = ImprintScraper()
results = []
for i, url in enumerate(urls):
if not url.strip():
continue
# Ensure URL has protocol
if not url.startswith(('http://', 'https://')):
url = 'https://' + url.strip()
if progress_callback:
progress_callback(f"Processing {i+1}/{len(urls)}: {url}")
result = await scraper.extract_contact_info(url)
results.append(result)
# Random delay between requests
if i < len(urls) - 1:
await asyncio.sleep(random.uniform(2, 5))
return pd.DataFrame(results)
def parse_input_urls(text_input: str, file_input) -> List[str]:
"""Parse URLs from text input or uploaded file"""
urls = []
# Parse text input
if text_input.strip():
urls.extend([url.strip() for url in text_input.strip().split('\n') if url.strip()])
# Parse file input
if file_input:
try:
if file_input.name.endswith('.csv'):
df = pd.read_csv(file_input.name)
# Try to find URL column
for col in df.columns:
if 'url' in col.lower() or 'website' in col.lower() or 'domain' in col.lower():
urls.extend(df[col].dropna().astype(str).tolist())
break
else:
# If no URL column found, use first column
urls.extend(df.iloc[:, 0].dropna().astype(str).tolist())
else:
# Assume text file with one URL per line
with open(file_input.name, 'r', encoding='utf-8') as f:
urls.extend([url.strip() for url in f.readlines() if url.strip()])
except Exception as e:
logger.error(f"Error reading file: {str(e)}")
return list(set(urls)) # Remove duplicates
async def scrape_websites(text_input: str, file_input, progress=gr.Progress()) -> Tuple[pd.DataFrame, str]:
"""Main function called by Gradio interface"""
# Parse input URLs
urls = parse_input_urls(text_input, file_input)
if not urls:
return pd.DataFrame(), "No URLs provided. Please enter URLs in the text box or upload a file."
progress(0, desc="Starting scraper...")
def update_progress(msg):
progress(0.5, desc=msg)
# Process URLs
try:
results_df = await process_urls(urls, progress_callback=update_progress)
# Save results to CSV
output_file = "scraped_contacts.csv"
results_df.to_csv(output_file, index=False, encoding='utf-8')
progress(1.0, desc="Scraping complete!")
success_count = len(results_df[results_df['status'] == 'Success'])
total_count = len(results_df)
summary = f"Processed {total_count} URLs. Successfully extracted data from {success_count} sites ({success_count/total_count*100:.1f}% success rate)."
return results_df, summary
except Exception as e:
error_msg = f"Error during scraping: {str(e)}"
logger.error(error_msg)
return pd.DataFrame(), error_msg
# Create Gradio interface
def create_interface():
with gr.Blocks(title="🇩🇪 DACH Store Imprint Scraper") as app:
gr.Markdown("""
# 🇩🇪 DACH Store Imprint Scraper
Extract contact details from legally required imprint pages in Germany, Austria, and Switzerland.
**Features:**
- 📧 Email extraction
- 📞 Phone number extraction
- 👤 Owner name extraction using AI and NER
- 🔄 Automatic imprint page detection
- 🎭 Anti-blocking measures with user agent rotation
- ⚡ JavaScript rendering fallback with Playwright
""")
with gr.Row():
with gr.Column():
gr.Markdown("### Input URLs")
text_input = gr.Textbox(
label="URLs (one per line)",
placeholder="https://example-shop.de\nhttps://another-store.at\nhttps://swiss-shop.ch",
lines=10,
max_lines=20
)
file_input = gr.File(
label="Or upload CSV file with URLs",
file_types=['.csv', '.txt']
)
scrape_btn = gr.Button("🚀 Start Scraping", variant="primary", size="lg")
with gr.Row():
status_text = gr.Textbox(label="Status", interactive=False)
with gr.Row():
results_df = gr.Dataframe(
label="Extraction Results",
headers=["Website", "Imprint URL", "Email", "Phone", "Owner", "Status"],
interactive=False
)
with gr.Row():
download_btn = gr.DownloadButton(
"📥 Download Results CSV",
value="scraped_contacts.csv",
visible=False
)
# Event handlers
scrape_btn.click(
fn=scrape_websites,
inputs=[text_input, file_input],
outputs=[results_df, status_text]
).then(
fn=lambda df: gr.DownloadButton(visible=len(df) > 0),
inputs=[results_df],
outputs=[download_btn]
)
return app
if __name__ == "__main__":
# Create and launch the application
app = create_interface()
app.launch(
server_name="0.0.0.0",
server_port=7860,
share=True
) |