#!/usr/bin/env python3 """ vast.ai Profile Extractor Script Dieses Skript läuft auf vast.ai und extrahiert Profildaten von Universitäts-Webseiten. Verwendung auf vast.ai: 1. Lade dieses Skript auf deine vast.ai Instanz 2. Installiere Abhängigkeiten: pip install requests beautifulsoup4 openai 3. Setze Umgebungsvariablen: - BREAKPILOT_API_URL=http://deine-ip:8086 - BREAKPILOT_API_KEY=dev-key - OPENAI_API_KEY=sk-... 4. Starte: python vast_ai_extractor.py """ import os import sys import json import time import logging import requests from bs4 import BeautifulSoup from typing import Optional, Dict, Any, List # Logging Setup logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Configuration API_URL = os.environ.get('BREAKPILOT_API_URL', 'http://localhost:8086') API_KEY = os.environ.get('BREAKPILOT_API_KEY', 'dev-key') OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '') BATCH_SIZE = 10 SLEEP_BETWEEN_REQUESTS = 1 # Sekunden zwischen Requests (respektiere rate limits) def fetch_pending_profiles(limit: int = 50) -> List[Dict]: """Hole Profile die noch extrahiert werden müssen.""" try: response = requests.get( f"{API_URL}/api/v1/ai/extraction/pending", params={"limit": limit}, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=30 ) response.raise_for_status() data = response.json() return data.get("tasks", []) except Exception as e: logger.error(f"Fehler beim Abrufen der Profile: {e}") return [] def fetch_profile_page(url: str) -> Optional[str]: """Lade den HTML-Inhalt einer Profilseite.""" try: headers = { 'User-Agent': 'Mozilla/5.0 (compatible; BreakPilot-Crawler/1.0; +https://breakpilot.de)', 'Accept': 'text/html,application/xhtml+xml', 'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8', } response = requests.get(url, headers=headers, timeout=30) response.raise_for_status() return response.text except Exception as e: logger.error(f"Fehler beim Laden von {url}: {e}") return None def extract_with_beautifulsoup(html: str, url: str) -> Dict[str, Any]: """Extrahiere Basis-Informationen mit BeautifulSoup (ohne AI).""" soup = BeautifulSoup(html, 'html.parser') data = {} # Email suchen email_links = soup.find_all('a', href=lambda x: x and x.startswith('mailto:')) if email_links: email = email_links[0]['href'].replace('mailto:', '').split('?')[0] data['email'] = email # Telefon suchen phone_links = soup.find_all('a', href=lambda x: x and x.startswith('tel:')) if phone_links: data['phone'] = phone_links[0]['href'].replace('tel:', '') # ORCID suchen orcid_links = soup.find_all('a', href=lambda x: x and 'orcid.org' in x) if orcid_links: orcid = orcid_links[0]['href'] # Extrahiere ORCID ID if '/' in orcid: data['orcid'] = orcid.split('/')[-1] # Google Scholar suchen scholar_links = soup.find_all('a', href=lambda x: x and 'scholar.google' in x) if scholar_links: href = scholar_links[0]['href'] if 'user=' in href: data['google_scholar_id'] = href.split('user=')[1].split('&')[0] # ResearchGate suchen rg_links = soup.find_all('a', href=lambda x: x and 'researchgate.net' in x) if rg_links: data['researchgate_url'] = rg_links[0]['href'] # LinkedIn suchen linkedin_links = soup.find_all('a', href=lambda x: x and 'linkedin.com' in x) if linkedin_links: data['linkedin_url'] = linkedin_links[0]['href'] # Institut/Abteilung Links sammeln (für Hierarchie-Erkennung) base_domain = '/'.join(url.split('/')[:3]) department_links = [] for link in soup.find_all('a', href=True): href = link['href'] text = link.get_text(strip=True) # Suche nach Links die auf Institute/Fakultäten hindeuten if any(kw in text.lower() for kw in ['institut', 'fakultät', 'fachbereich', 'abteilung', 'lehrstuhl']): if href.startswith('/'): href = base_domain + href if href.startswith('http'): department_links.append({'url': href, 'name': text}) if department_links: # Nimm den ersten gefundenen Department-Link data['department_url'] = department_links[0]['url'] data['department_name'] = department_links[0]['name'] return data def extract_with_ai(html: str, url: str, full_name: str) -> Dict[str, Any]: """Extrahiere strukturierte Daten mit OpenAI GPT.""" if not OPENAI_API_KEY: logger.warning("Kein OPENAI_API_KEY gesetzt - nutze nur BeautifulSoup") return extract_with_beautifulsoup(html, url) try: import openai client = openai.OpenAI(api_key=OPENAI_API_KEY) # Reduziere HTML auf relevanten Text soup = BeautifulSoup(html, 'html.parser') # Entferne Scripts, Styles, etc. for tag in soup(['script', 'style', 'nav', 'footer', 'header']): tag.decompose() # Extrahiere Text text = soup.get_text(separator='\n', strip=True) # Limitiere auf 8000 Zeichen für API text = text[:8000] prompt = f"""Analysiere diese Universitäts-Profilseite für {full_name} und extrahiere folgende Informationen im JSON-Format: {{ "email": "email@uni.de oder null", "phone": "Telefonnummer oder null", "office": "Raum/Büro oder null", "position": "Position/Titel (z.B. Wissenschaftlicher Mitarbeiter, Professorin) oder null", "department_name": "Name des Instituts/der Abteilung oder null", "research_interests": ["Liste", "der", "Forschungsthemen"] oder [], "teaching_topics": ["Liste", "der", "Lehrveranstaltungen/Fächer"] oder [], "supervisor_name": "Name des Vorgesetzten/Lehrstuhlinhabers falls erkennbar oder null" }} Profilseite von {url}: {text} Antworte NUR mit dem JSON-Objekt, keine Erklärungen.""" response = client.chat.completions.create( model="gpt-4o-mini", # Kostengünstig und schnell messages=[{"role": "user", "content": prompt}], temperature=0.1, max_tokens=500 ) result_text = response.choices[0].message.content.strip() # Parse JSON (entferne eventuelle Markdown-Blöcke) if result_text.startswith('```'): result_text = result_text.split('```')[1] if result_text.startswith('json'): result_text = result_text[4:] ai_data = json.loads(result_text) # Kombiniere mit BeautifulSoup-Ergebnissen (für Links wie ORCID) bs_data = extract_with_beautifulsoup(html, url) # AI-Daten haben Priorität, aber BS-Daten für spezifische Links for key in ['orcid', 'google_scholar_id', 'researchgate_url', 'linkedin_url']: if key in bs_data and bs_data[key]: ai_data[key] = bs_data[key] return ai_data except Exception as e: logger.error(f"AI-Extraktion fehlgeschlagen: {e}") return extract_with_beautifulsoup(html, url) def submit_extracted_data(staff_id: str, data: Dict[str, Any]) -> bool: """Sende extrahierte Daten zurück an BreakPilot.""" try: payload = {"staff_id": staff_id, **data} # Entferne None-Werte payload = {k: v for k, v in payload.items() if v is not None} response = requests.post( f"{API_URL}/api/v1/ai/extraction/submit", json=payload, headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" }, timeout=30 ) response.raise_for_status() return True except Exception as e: logger.error(f"Fehler beim Senden der Daten für {staff_id}: {e}") return False def process_profiles(): """Hauptschleife: Hole Profile, extrahiere Daten, sende zurück.""" logger.info(f"Starte Extraktion - API: {API_URL}") processed = 0 errors = 0 while True: # Hole neue Profile profiles = fetch_pending_profiles(limit=BATCH_SIZE) if not profiles: logger.info("Keine weiteren Profile zum Verarbeiten. Warte 60 Sekunden...") time.sleep(60) continue logger.info(f"Verarbeite {len(profiles)} Profile...") for profile in profiles: staff_id = profile['staff_id'] url = profile['profile_url'] full_name = profile.get('full_name', 'Unbekannt') logger.info(f"Verarbeite: {full_name} - {url}") # Lade Profilseite html = fetch_profile_page(url) if not html: errors += 1 continue # Extrahiere Daten extracted = extract_with_ai(html, url, full_name) if extracted: # Sende zurück if submit_extracted_data(staff_id, extracted): processed += 1 logger.info(f"Erfolgreich: {full_name} - Email: {extracted.get('email', 'N/A')}") else: errors += 1 else: errors += 1 # Rate limiting time.sleep(SLEEP_BETWEEN_REQUESTS) logger.info(f"Batch abgeschlossen. Gesamt: {processed} erfolgreich, {errors} Fehler") def main(): """Einstiegspunkt.""" logger.info("=" * 60) logger.info("BreakPilot vast.ai Profile Extractor") logger.info("=" * 60) # Prüfe Konfiguration if not API_KEY: logger.error("BREAKPILOT_API_KEY nicht gesetzt!") sys.exit(1) if not OPENAI_API_KEY: logger.warning("OPENAI_API_KEY nicht gesetzt - nutze nur BeautifulSoup-Extraktion") # Teste Verbindung try: response = requests.get( f"{API_URL}/v1/health", headers={"Authorization": f"Bearer {API_KEY}"}, timeout=10 ) logger.info(f"API-Verbindung OK: {response.status_code}") except Exception as e: logger.error(f"Kann API nicht erreichen: {e}") logger.error(f"Stelle sicher dass {API_URL} erreichbar ist!") sys.exit(1) # Starte Verarbeitung try: process_profiles() except KeyboardInterrupt: logger.info("Beendet durch Benutzer") except Exception as e: logger.error(f"Unerwarteter Fehler: {e}") sys.exit(1) if __name__ == "__main__": main()