Nye-TeeOff/kode_eksport_3/backend_scrape_membership_py.txt
2026-04-10 09:52:34 +02:00

168 lines
No EOL
7 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 (MULTI-URL VERSJON)
---------------------------------------------------------------------------
Går til oppgitte medlemskaps-URLer (støtter flere URLer adskilt med komma),
henter ut tekst, og bruker Gemini til å summere og finne 'Standard' og
'Rimeligste' medlemskap.
---------------------------------------------------------------------------
"""
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
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!")
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel('gemini-2.5-flash')
async def fetch_page_text(url: str, browser) -> str:
"""Bruker Playwright for å hente all synlig tekst fra EN nettside."""
url = url.strip()
if not url.startswith("http"):
return ""
print(f" 🌐 Laster inn: {url}")
try:
page = await browser.new_page()
await page.goto(url, wait_until="domcontentloaded", timeout=15000)
html_content = await page.content()
await page.close()
soup = BeautifulSoup(html_content, 'html.parser')
for script in soup(["script", "style", "nav", "footer", "header"]):
script.extract()
text = soup.get_text(separator=' ', strip=True)
return text
except Exception as e:
print(f" ❌ Feil ved lasting av {url}: {e}")
return ""
def analyze_with_gemini(text: str, club_name: str) -> dict:
"""Sender den kombinerte teksten til Gemini for å trekke ut og evt. summere priser."""
print(f" 🧠 Sender {len(text)} tegn til Gemini for analyse...")
prompt = f"""
Du er en ekspert på norske golfklubber. Din oppgave er å lese teksten hentet fra nettsidene til "{club_name}" og finne to spesifikke priser.
VIKTIG REGEL OM NORSK GOLF:
Mange steder er "Klubbkontingent/Medlemskap" og "Spillerett/Årskort" to forskjellige ting.
For å få spille ubegrenset (Fritt spill) MÅ man betale BEGGE DELER. Hvis du ser at prisene for kontingent og spillerett er oppgitt hver for seg, SKAL DU SUMMERE disse to summene og bruke totalen som "Standard pris".
ALDERSPREMISS FOR BEGGE PRISER:
Vi forutsetter at personen som skal ha medlemskap er en VOKSEN GOLFER PÅ MINST 35 ÅR. Du må ALDRI velge priser som gjelder for barn, junior, ung voksen (f.eks. 20-29 år), student eller senior/pensjonist.
DEFINISJONER DU MÅ FØLGE STRENGT:
1. "Standard medlemskap": Hva er TOTALPRISEN (inkludert evt. spillerett/årskort) for en voksen person (35+ år) for å spille SÅ MYE VEDKOMMENDE ØNSKER (Fritt spill) i år?
2. "Rimeligste alternativ": Det absolutt billigste alternativet FOR EN VOKSEN PERSON (35+ år) som gir medlemskap i klubben (golfkortet), forutsatt at man betaler greenfee for hver runde. (Ofte kalt Greenfeemedlem, Postkassemedlem, Fjernmedlem, eller kun "Klubbkontingent for voksne" uten spillerett).
TEKST FRA NETTSIDEN(E):
{text}
OPPGAVE:
Returner KUN et gyldig JSON-objekt med følgende struktur:
{{
"foreslatt_standard_navn": "Navn (eks: Hovedmedlem Voksen inkl. spillerett)",
"foreslatt_standard_pris": 1234,
"foreslatt_standard_kommentar": "Kort kommentar (eks: Måtte summere kontingent på 900 og årskort på 5000)",
"foreslatt_rimeligste_navn": "Navn (eks: Greenfeemedlemskap Voksen)",
"foreslatt_rimeligste_pris": 500,
"ai_begrunnelse": "Kort forklaring på utregningen din."
}}
Merk: Prisene SKAL være tall (integer), ikke tekst. Sett til null hvis du ikke finner det.
"""
try:
response = model.generate_content(prompt)
raw_response = response.text.strip()
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):
print("🚀 Starter Medlemskaps-skraperen (Støtter multi-URL)...")
conn = await asyncpg.connect(DB_URL)
try:
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.")
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
for facility in facilities:
fac_id = facility['id']
name = facility['name']
urls_raw = facility['medlemskap_url']
print(f"\n▶ Behandler: {name} (ID: {fac_id})")
# Sjekker om det er flere URL-er adskilt med komma
urls = [u.strip() for u in urls_raw.split(',')]
combined_text = ""
for idx, url in enumerate(urls, 1):
page_text = await fetch_page_text(url, browser)
if page_text:
combined_text += f"\n\n--- TEKST FRA SIDE {idx} ({url}) ---\n{page_text}"
if len(combined_text) < 50:
print(" ⚠️ Fant for lite tekst, hopper over.")
continue
# Kutter teksten for å ikke overbelaste Gemini (ca 25000 tegn maks)
draft_data = analyze_with_gemini(combined_text[:25000], name)
if not draft_data:
continue
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!")
await browser.close()
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))