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
    )