161 lines
6.3 KiB
Text
161 lines
6.3 KiB
Text
|
|
"""
|
|||
|
|
TEE OFF - VEIEN TIL GOLF (VTG) SKRAPER MED GEMINI AI
|
|||
|
|
---------------------------------------------------------------------------
|
|||
|
|
Henter pris, beskrivelse (inkl. lånekøller/medlemskap) og kursdatoer fra VTG-sider.
|
|||
|
|
Støtter kommaseparerte URL-er.
|
|||
|
|
---------------------------------------------------------------------------
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
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_vtg_with_gemini(text: str, club_name: str) -> dict:
|
|||
|
|
print(f" 🧠 Sender {len(text)} tegn til Gemini for VTG-analyse...")
|
|||
|
|
|
|||
|
|
prompt = f"""
|
|||
|
|
Du er en ekspert på norske golfklubber. Din oppgave er å lese en lang tekst fra nettsidene til "{club_name}" og koke dette ned til essensen om deres "Veien til Golf" (VTG) nybegynnerkurs.
|
|||
|
|
|
|||
|
|
OPPGAVER:
|
|||
|
|
1. Finn standardprisen for VTG-kurset for en vanlig voksen person. (Returner KUN tallet).
|
|||
|
|
2. Skriv en KOMPRIMERT, selgende beskrivelse (maks 3-4 setninger). Du MÅ inkludere informasjon om:
|
|||
|
|
- Er lån av køller/utstyr inkludert i kurset?
|
|||
|
|
- Inkluderer prisen et medlemskap/spillerett i klubben (og ev. for hvor lenge)?
|
|||
|
|
- Hva er omfanget? (F.eks. "12 timer praksis pluss e-læring").
|
|||
|
|
Ignorer uvesentlig støy og lange historiske utgreiinger.
|
|||
|
|
3. Finn alle kommende kursdatoer. Finn startdato/sluttdato for hvert kurs, og noter status ("Ledig", "Fulltegnet", "Venteliste").
|
|||
|
|
|
|||
|
|
TEKST FRA NETTSIDEN:
|
|||
|
|
{text}
|
|||
|
|
|
|||
|
|
OPPGAVE:
|
|||
|
|
Returner KUN et gyldig JSON-objekt med nøyaktig følgende struktur:
|
|||
|
|
{{
|
|||
|
|
"foreslatt_vtg_pris": 1990,
|
|||
|
|
"foreslatt_vtg_beskrivelse": "Kurset går over 12 timer inkludert obligatorisk e-læring. Lån av golfkøller er inkludert under hele kurset, og prisen gir deg også fritt spill og medlemskap ut året.",
|
|||
|
|
"foreslatt_vtg_datoer": [
|
|||
|
|
{{"dato": "12.-14. mai", "status": "Fulltegnet"}},
|
|||
|
|
{{"dato": "5.-7. juni", "status": "Ledig"}}
|
|||
|
|
],
|
|||
|
|
"ai_begrunnelse": "Fant voksenpris på 1990,-. Teksten nevnte eksplisitt at medlemskap ut året er med i prisen, og at man får låne utstyr."
|
|||
|
|
}}
|
|||
|
|
Merk: Sett foreslatt_vtg_pris til null (null) hvis du ikke finner den. Hvis du ikke finner datoer, la listen være tom [].
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
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_vtg_scraper(facility_ids=None):
|
|||
|
|
print("🚀 Starter Veien til Golf (VTG) skraperen...")
|
|||
|
|
conn = await asyncpg.connect(DB_URL)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
query = "SELECT id, name, vtg_lenke FROM facilities WHERE vtg_lenke IS NOT NULL AND vtg_lenke != ''"
|
|||
|
|
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['vtg_lenke']
|
|||
|
|
|
|||
|
|
print(f"\n▶️ Behandler VTG 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.")
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
draft_data = analyze_vtg_with_gemini(combined_text[:25000], name)
|
|||
|
|
|
|||
|
|
if not draft_data:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
print(f" ✅ AI fant pris: {draft_data.get('foreslatt_vtg_pris')}, og {len(draft_data.get('foreslatt_vtg_datoer', []))} datoer.")
|
|||
|
|
|
|||
|
|
await conn.execute("""
|
|||
|
|
UPDATE facilities
|
|||
|
|
SET vtg_draft = $1::jsonb
|
|||
|
|
WHERE id = $2
|
|||
|
|
""", json.dumps(draft_data), fac_id)
|
|||
|
|
|
|||
|
|
print(" 💾 VTG-utkast lagret i databasen!")
|
|||
|
|
|
|||
|
|
await browser.close()
|
|||
|
|
|
|||
|
|
finally:
|
|||
|
|
await conn.close()
|
|||
|
|
print("\n🏁 Skraping fullført.")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
parser = argparse.ArgumentParser(description="Skrap VTG 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_vtg_scraper(ids_to_scrape))
|