2026-03-12 13:39:10 +01:00
"""
TEE OFF - GREENFEE - SKRAPER MED GEMINI AI
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Henter alle greenfee - varianter fra en ( eller flere ) URL - er og strukturerer
dem i en JSON - liste . Finner også avtaleklubber / vennskapsklubber .
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
"""
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 :
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 ( )
return soup . get_text ( separator = ' ' , strip = True )
except Exception as e :
print ( f " ❌ Feil ved lasting av { url } : { e } " )
return " "
def analyze_greenfee_with_gemini ( text : str , club_name : str ) - > dict :
print ( f " 🧠 Sender { len ( text ) } tegn til Gemini for greenfee-analyse... " )
prompt = f """
Du er en ekspert på norske golfklubber og prissetting .
Din oppgave er å lese teksten hentet fra nettsidene til " {club_name} " og hente ut TO ting :
1. ALLE varianter av greenfee - priser .
2. Navn på eventuelle vennskapsklubber / avtaleklubber ( hvis nevnt ) .
REGLER FOR GREENFEE :
- Trekk ut absolutt alle priskategorier du finner ( f . eks . " Hverdag høysesong " , " Helg før kl 14 " , " Gjest av medlem " , " 9 hull kveld " , osv . ) .
- Finn både voksenpris og juniorpris for hver kategori .
- HVIS juniorpris er oppgitt som en regel ( f . eks . " Juniorer betaler halv pris " eller " 50 % r abatt for junior " ) , MÅ du selv regne ut prisen og skrive inn heltallet .
- " banenavn " : Bruk navnet på banen hvis det er spesifisert ( f . eks . " 18-hullsbanen " , " Korthullsbanen " ) . Hvis ikke spesifisert , bruk " {club_name} " .
- Priser SKAL være tall ( integer ) . Sett pris til null ( null ) hvis den ikke finnes .
REGLER FOR AVTALEKLUBBER :
- Let etter overskrifter som " Vennskapsklubber " , " Avtaleklubber " , " Gjestespill " , " Samarbeidsklubber " .
- Trekk ut kun navnene på klubbene i en liste ( f . eks . [ " Haga GK " , " Oslo GK " ] ) . La listen være tom hvis du ikke finner noen .
TEKST FRA NETTSIDEN :
{ text }
OPPGAVE :
Returner KUN et gyldig JSON - objekt med nøyaktig følgende struktur :
{ {
" foreslatt_greenfee " : [
{ {
" banenavn " : " Navn på banen " ,
" priskategori " : " F.eks: Hverdag Gjest av Medlem " ,
" pris_voksne " : 600 ,
" pris_junior " : 300
} }
] ,
" foreslatt_avtaleklubber " : [
" Klubb 1 GK " ,
" Klubb 2 GK "
] ,
" ai_begrunnelse " : " Kort forklaring, f.eks: ' Fant et komplekst prissystem for høy/lavsesong. Regnet ut juniorpriser til 50 % s om angitt i teksten. Fant 3 samarbeidsklubber nederst. ' "
} }
"""
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_greenfee_scraper ( facility_ids = None ) :
print ( " 🚀 Starter Greenfee-skraperen... " )
conn = await asyncpg . connect ( DB_URL )
2026-04-10 18:37:33 +02:00
facilities = [ ]
analyzed_count = 0
saved_count = 0
skipped_count = 0
2026-03-12 13:39:10 +01:00
try :
query = " SELECT id, name, greenfee_url FROM facilities WHERE greenfee_url IS NOT NULL AND greenfee_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 [ ' greenfee_url ' ]
print ( f " \n ▶️ Behandler Greenfee for: { name } (ID: { fac_id } ) " )
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. " )
2026-04-10 18:37:33 +02:00
skipped_count + = 1
2026-03-12 13:39:10 +01:00
continue
draft_data = analyze_greenfee_with_gemini ( combined_text [ : 25000 ] , name )
if not draft_data :
2026-04-10 18:37:33 +02:00
skipped_count + = 1
2026-03-12 13:39:10 +01:00
continue
2026-04-10 18:37:33 +02:00
analyzed_count + = 1
2026-03-12 13:39:10 +01:00
funnet_priser = len ( draft_data . get ( ' foreslatt_greenfee ' , [ ] ) )
funnet_klubber = len ( draft_data . get ( ' foreslatt_avtaleklubber ' , [ ] ) )
print ( f " ✅ AI fant { funnet_priser } greenfee-varianter og { funnet_klubber } avtaleklubber. " )
await conn . execute ( """
UPDATE facilities
SET greenfee_draft = $ 1 : : jsonb
WHERE id = $ 2
""" , json.dumps(draft_data), fac_id)
print ( " 💾 Greenfee-utkast lagret i databasen! " )
2026-04-10 18:37:33 +02:00
saved_count + = 1
2026-03-12 13:39:10 +01:00
await browser . close ( )
finally :
await conn . close ( )
print ( " \n 🏁 Skraping fullført. " )
2026-04-10 18:37:33 +02:00
return {
" processed_facilities " : len ( facilities ) ,
" analyzed_facilities " : analyzed_count ,
" saved_drafts " : saved_count ,
" skipped_facilities " : skipped_count ,
}
2026-03-12 13:39:10 +01:00
if __name__ == " __main__ " :
parser = argparse . ArgumentParser ( description = " Skrap greenfeepriser 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 ( " , " ) ]
2026-04-10 18:37:33 +02:00
asyncio . run ( run_greenfee_scraper ( ids_to_scrape ) )