Nye-TeeOff/kode_eksport_1/backend_scrape_membership_py.txt

163 lines
No EOL
6.6 KiB
Text
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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