Nye-TeeOff/kode_eksport_1/backend_scrape_membership_py.txt

163 lines
6.6 KiB
Text
Raw Normal View History

"""
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))