""" TEE OFF - MEDLEMSKAPSSKRAPER MED GEMINI AI --------------------------------------------------------------------------- Går til oppgitte medlemskaps-URLer, henter ut tekst, og bruker Gemini til å finne 'Standard' og 'Rimeligste' medlemskap basert på TeeOffs definisjoner. Lagrer resultatet som et utkast i databasen (membership_draft). --------------------------------------------------------------------------- """ import asyncio import asyncpg import os import json import argparse from bs4 import BeautifulSoup from playwright.async_api import async_playwright import google.generativeai as genai from dotenv import load_dotenv # Last inn miljøvariabler load_dotenv() DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") if not GEMINI_API_KEY: raise ValueError("🚨 GEMINI_API_KEY mangler i .env filen!") # Konfigurer Gemini genai.configure(api_key=GEMINI_API_KEY) model = genai.GenerativeModel('gemini-2.5-flash') # Eller gemini-1.5-pro avhengig av hva du har tilgang til async def fetch_page_text(url: str) -> str: """Bruker Playwright for å hente all synlig tekst fra nettsiden.""" print(f" 🌐 Laster inn: {url}") try: async with async_playwright() as p: browser = await p.chromium.launch(headless=True) page = await browser.new_page() # Setter timeout til 15 sekunder await page.goto(url, wait_until="domcontentloaded", timeout=15000) # Hent hele HTML-innholdet html_content = await page.content() await browser.close() # Bruk BeautifulSoup til å renske ut bare den synlige teksten soup = BeautifulSoup(html_content, 'html.parser') # Fjern script og style tags for script in soup(["script", "style", "nav", "footer", "header"]): script.extract() text = soup.get_text(separator=' ', strip=True) # Begrens teksten slik at vi ikke sprenger token-grensen til AI (f.eks max 15000 tegn) return text[:15000] except Exception as e: print(f" ❌ Feil ved lasting av side: {e}") return "" def analyze_with_gemini(text: str, club_name: str) -> dict: """Sender teksten til Gemini for å trekke ut priser.""" print(f" 🧠 Sender {len(text)} tegn til Gemini for analyse...") prompt = f""" Du er en ekspert på norske golfklubber og medlemskap. Din oppgave er å lese teksten hentet fra nettsiden til "{club_name}" og trekke ut to spesifikke medlemskapspriser. DEFINISJONER DU MÅ FØLGE STRENGT: 1. "Standard medlemskap": Hva vil det koste for en gjennomsnittsgolfer (voksen over 25/30 år, ikke student/senior) å spille SÅ RYE VEDKOMMENDE ØNSKER (Fritt spill) på denne banen i år? 2. "Rimeligste alternativ": Det absolutt billigste medlemskapet som gir medlemskap i klubben (golfkortet), forutsatt at man aksepterer å måtte betale greenfee for hver runde man spiller. (Ofte kalt Greenfeemedlem, Postkassemedlem, Fjernmedlem el.l.) TEKST FRA NETTSIDEN: {text} OPPGAVE: Returner KUN et gyldig JSON-objekt med følgende struktur (og ingenting annet, ingen markdown): {{ "foreslatt_standard_navn": "Navnet på medlemskapet (eks: Hovedmedlem Voksen)", "foreslatt_standard_pris": 1234, "foreslatt_standard_kommentar": "Kort evt kommentar (eks: Inkluderer ikke 500kr i dugnadsavgift)", "foreslatt_rimeligste_navn": "Navnet (eks: Greenfeemedlemskap)", "foreslatt_rimeligste_pris": 500, "ai_begrunnelse": "Kort forklaring på hvorfor du valgte disse to, f.eks: 'Valgte Hovedmedlem for fritt spill og Greenfeemedlem fordi...'." }} Merk: Hvis prisene mangler, sett pris til null og skriv "Fant ikke" i navnet. Prisen SKAL være et tall (integer), ikke en tekststreng (bruk 6500, ikke "6 500"). """ try: response = model.generate_content(prompt) raw_response = response.text.strip() # Rensker vekk eventuell markdown-formatering som ```json if raw_response.startswith("```json"): raw_response = raw_response[7:] if raw_response.endswith("```"): raw_response = raw_response[:-3] return json.loads(raw_response.strip()) except Exception as e: print(f" ❌ AI-analyse feilet: {e}") return None async def run_scraper(facility_ids=None): """Hovedfunksjon som henter fra DB, skraper, og lagrer utkast.""" print("🚀 Starter Medlemskaps-skraperen...") conn = await asyncpg.connect(DB_URL) try: # Hent anlegg som har en url for medlemskap query = "SELECT id, name, medlemskap_url FROM facilities WHERE medlemskap_url IS NOT NULL AND medlemskap_url != ''" if facility_ids: query += f" AND id IN ({','.join(map(str, facility_ids))})" facilities = await conn.fetch(query) print(f"📋 Fant {len(facilities)} anlegg å skrape.") for facility in facilities: fac_id = facility['id'] name = facility['name'] url = facility['medlemskap_url'] print(f"\n▶️ Behandler: {name} (ID: {fac_id})") # 1. Hent tekst page_text = await fetch_page_text(url) if not page_text or len(page_text) < 50: print(" ⚠️ Fant for lite tekst på siden, hopper over.") continue # 2. Analyser med Gemini draft_data = analyze_with_gemini(page_text, name) if not draft_data: continue # 3. Lagre i databasen som utkast print(f" ✅ AI foreslår: Standard: {draft_data.get('foreslatt_standard_pris')} | Rimeligste: {draft_data.get('foreslatt_rimeligste_pris')}") await conn.execute(""" UPDATE facilities SET membership_draft = $1::jsonb WHERE id = $2 """, json.dumps(draft_data), fac_id) print(" 💾 Utkast lagret i databasen!") finally: await conn.close() print("\n🏁 Skraping fullført.") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Skrap medlemskapspriser via AI.") parser.add_argument("--ids", type=str, help="Kommaseparert liste med facility IDs (eks: 1,5,12)") args = parser.parse_args() ids_to_scrape = None if args.ids: ids_to_scrape = [int(x.strip()) for x in args.ids.split(",")] asyncio.run(run_scraper(ids_to_scrape))