feat: HKG Stopover Variante — Multi-City FRA→HKG→KTI
- db.py: jobs Tabelle um via/stopover_min_h/stopover_max_h erweitert - db.py: 2 neue kayak_multicity Jobs (CX + alle Airlines) via HKG - db.py: KI-Prompt um HKG_STOPOVER Vergleichsfeld ergänzt - worker.py: scrape_kayak_multicity() Funktion implementiert - worker.py: Kayak Multi-City URL FRA→HKG/D1→HKG→KTI/D2→KTI→FRA/D3 - agent.py: via/stopover_min_h/stopover_max_h Parameter entgegengenommen - scheduler.py: neue Parameter an Node-Agent weitergegeben - web.py: Multi-City Einträge im Dashboard lila hervorgehoben (+~150€ Hotel-Hinweis) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
639e6a4fc3
commit
2ce7d02bc5
5 changed files with 156 additions and 24 deletions
|
|
@ -42,6 +42,9 @@ def init_db():
|
||||||
layover_max INTEGER DEFAULT 300,
|
layover_max INTEGER DEFAULT 300,
|
||||||
max_flugzeit_h INTEGER DEFAULT 22,
|
max_flugzeit_h INTEGER DEFAULT 22,
|
||||||
max_stops INTEGER DEFAULT 2,
|
max_stops INTEGER DEFAULT 2,
|
||||||
|
via TEXT DEFAULT '',
|
||||||
|
stopover_min_h INTEGER DEFAULT 0,
|
||||||
|
stopover_max_h INTEGER DEFAULT 0,
|
||||||
intervall TEXT DEFAULT 'daily',
|
intervall TEXT DEFAULT 'daily',
|
||||||
aktiv INTEGER DEFAULT 1,
|
aktiv INTEGER DEFAULT 1,
|
||||||
created_at TEXT DEFAULT (datetime('now'))
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
|
@ -57,6 +60,9 @@ def init_db():
|
||||||
"ALTER TABLE jobs ADD COLUMN layover_max INTEGER DEFAULT 300",
|
"ALTER TABLE jobs ADD COLUMN layover_max INTEGER DEFAULT 300",
|
||||||
"ALTER TABLE jobs ADD COLUMN max_flugzeit_h INTEGER DEFAULT 22",
|
"ALTER TABLE jobs ADD COLUMN max_flugzeit_h INTEGER DEFAULT 22",
|
||||||
"ALTER TABLE jobs ADD COLUMN max_stops INTEGER DEFAULT 2",
|
"ALTER TABLE jobs ADD COLUMN max_stops INTEGER DEFAULT 2",
|
||||||
|
"ALTER TABLE jobs ADD COLUMN via TEXT DEFAULT ''",
|
||||||
|
"ALTER TABLE jobs ADD COLUMN stopover_min_h INTEGER DEFAULT 0",
|
||||||
|
"ALTER TABLE jobs ADD COLUMN stopover_max_h INTEGER DEFAULT 0",
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
c.execute(col_sql)
|
c.execute(col_sql)
|
||||||
|
|
@ -150,7 +156,8 @@ BESTER_PREIS: [Anbieter + Airline + Preis + Node]
|
||||||
BESTE_AIRLINE: [welche der 4 Airlines gerade am guenstigsten]
|
BESTE_AIRLINE: [welche der 4 Airlines gerade am guenstigsten]
|
||||||
TREND: [STEIGEND / FALLEND / STABIL]
|
TREND: [STEIGEND / FALLEND / STABIL]
|
||||||
GEO_UNTERSCHIED: [DE-Scanner vs. KH-Scanner Preisdifferenz]
|
GEO_UNTERSCHIED: [DE-Scanner vs. KH-Scanner Preisdifferenz]
|
||||||
PLAUSI_CHECK: [Preise unter 1000 EUR einzeln einordnen - was stimmt da nicht]"""
|
PLAUSI_CHECK: [Preise unter 1000 EUR einzeln einordnen - was stimmt da nicht]
|
||||||
|
HKG_STOPOVER: [Vergleich: Direktverbindung vs. FRA-HKG-KTI Multi-City — lohnt sich der HKG-Tag? Preis + ca. 150 EUR Hotel HKG einrechnen]"""
|
||||||
|
|
||||||
c.execute("INSERT OR IGNORE INTO prompts (name, inhalt) VALUES (?, ?)",
|
c.execute("INSERT OR IGNORE INTO prompts (name, inhalt) VALUES (?, ?)",
|
||||||
("ki_auswertung", PROMPT_TEXT))
|
("ki_auswertung", PROMPT_TEXT))
|
||||||
|
|
@ -169,14 +176,17 @@ PLAUSI_CHECK: [Preise unter 1000 EUR einzeln einordnen - was stimmt da nicht]"""
|
||||||
c.execute("""
|
c.execute("""
|
||||||
INSERT INTO jobs
|
INSERT INTO jobs
|
||||||
(scanner, von, nach, tage, aufenthalt_tage, trip_type, kabine, gepaeck,
|
(scanner, von, nach, tage, aufenthalt_tage, trip_type, kabine, gepaeck,
|
||||||
airline_filter, layover_min, layover_max, max_flugzeit_h, max_stops, intervall)
|
airline_filter, layover_min, layover_max, max_flugzeit_h, max_stops,
|
||||||
|
via, stopover_min_h, stopover_max_h, intervall)
|
||||||
VALUES
|
VALUES
|
||||||
('kayak','FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','', 120,300,22,2,'daily'),
|
('kayak', 'FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','', 120,300,22,2,'', 0, 0,'daily'),
|
||||||
('trip', 'FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','', 120,300,22,2,'daily'),
|
('trip', 'FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','', 120,300,22,2,'', 0, 0,'daily'),
|
||||||
('kayak','FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','CZ',120,300,22,2,'daily'),
|
('kayak', 'FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','CZ',120,300,22,2,'', 0, 0,'daily'),
|
||||||
('kayak','FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','CX',120,300,22,2,'daily'),
|
('kayak', 'FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','CX',120,300,22,2,'', 0, 0,'daily'),
|
||||||
('kayak','FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','SQ',120,300,22,2,'daily'),
|
('kayak', 'FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','SQ',120,300,22,2,'', 0, 0,'daily'),
|
||||||
('kayak','FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','TG',120,300,22,2,'daily')
|
('kayak', 'FRA','KTI',30,60,'roundtrip','premium_economy','1koffer+handgepaeck','TG',120,300,22,2,'', 0, 0,'daily'),
|
||||||
|
('kayak_multicity','FRA','KTI',30,60,'multicity', 'premium_economy','1koffer+handgepaeck','CX',120,300,22,2,'HKG',20,30,'daily'),
|
||||||
|
('kayak_multicity','FRA','KTI',30,60,'multicity', 'premium_economy','1koffer+handgepaeck','', 120,300,22,2,'HKG',20,30,'daily')
|
||||||
""")
|
""")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ def dispatch_job(node, job, tage_override=None):
|
||||||
"layover_max": job.get("layover_max", 300),
|
"layover_max": job.get("layover_max", 300),
|
||||||
"max_flugzeit_h": job.get("max_flugzeit_h", 22),
|
"max_flugzeit_h": job.get("max_flugzeit_h", 22),
|
||||||
"max_stops": job.get("max_stops", 2),
|
"max_stops": job.get("max_stops", 2),
|
||||||
|
"via": job.get("via", ""),
|
||||||
|
"stopover_min_h": job.get("stopover_min_h", 20),
|
||||||
|
"stopover_max_h": job.get("stopover_max_h", 30),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
|
|
@ -74,8 +77,10 @@ def dispatch_job(node, job, tage_override=None):
|
||||||
)
|
)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
results = r.json().get("results", [])
|
results = r.json().get("results", [])
|
||||||
|
via_label = f" via {job.get('via','')}" if job.get('via') else ""
|
||||||
log(f"{node['name']}: {len(results)} Preise ← {job['scanner']}"
|
log(f"{node['name']}: {len(results)} Preise ← {job['scanner']}"
|
||||||
f"{' ['+job.get('airline_filter','')+']' if job.get('airline_filter') else ''}"
|
f"{' ['+job.get('airline_filter','')+']' if job.get('airline_filter') else ''}"
|
||||||
|
f"{via_label}"
|
||||||
f"{' +'+str(tage_override)+'T' if tage_override else ''}")
|
f"{' +'+str(tage_override)+'T' if tage_override else ''}")
|
||||||
speichere_preise(results, node["name"], job)
|
speichere_preise(results, node["name"], job)
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ BASE_HTML = """<!DOCTYPE html>
|
||||||
|
|
||||||
OVERVIEW_HTML = BASE_HTML.replace("{% block content %}{% endblock %}", """
|
OVERVIEW_HTML = BASE_HTML.replace("{% block content %}{% endblock %}", """
|
||||||
<div style="background:#0c1a3a;border:1px solid#1e40af;border-radius:8px;padding:0.6rem 1rem;margin-bottom:0.6rem;font-size:0.85rem;color:#93c5fd;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem">
|
<div style="background:#0c1a3a;border:1px solid#1e40af;border-radius:8px;padding:0.6rem 1rem;margin-bottom:0.6rem;font-size:0.85rem;color:#93c5fd;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem">
|
||||||
<span>✈️ <strong>FRA → KTI</strong> · Roundtrip · Premium Economy · 1 Koffer + Handgepäck · ~2 Monate · max 22h / 2 Stopps / Umstieg 2-5h</span>
|
<span>✈️ <strong>FRA → KTI</strong> · Roundtrip · Premium Economy · 1 Koffer + Handgepäck · ~2 Monate · max 22h / 2 Stopps / Umstieg 2-5h · 🇭🇰 <strong>HKG Stopover</strong> Variante aktiv</span>
|
||||||
<span id="schedule-info" style="font-size:0.78rem;color:#60a5fa">Lade Zeitplan...</span>
|
<span id="schedule-info" style="font-size:0.78rem;color:#60a5fa">Lade Zeitplan...</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#451a03;border:1px solid#92400e;border-radius:8px;padding:0.5rem 1rem;margin-bottom:1.2rem;font-size:0.82rem;color:#fcd34d">
|
<div style="background:#451a03;border:1px solid#92400e;border-radius:8px;padding:0.5rem 1rem;margin-bottom:1.2rem;font-size:0.82rem;color:#fcd34d">
|
||||||
|
|
@ -244,7 +244,9 @@ async function ladeUebersicht() {
|
||||||
|
|
||||||
// Detail-Tabelle
|
// Detail-Tabelle
|
||||||
const tbody = document.getElementById('preise-tbody');
|
const tbody = document.getElementById('preise-tbody');
|
||||||
|
const HOTEL_HKG = 150; // geschätzte Hotel-Kosten HKG in EUR
|
||||||
tbody.innerHTML = preise.map(p => {
|
tbody.innerHTML = preise.map(p => {
|
||||||
|
const isMulticity = p.scanner === 'kayak_multicity';
|
||||||
const warn = p.preis < PLAUSI_GRENZE;
|
const warn = p.preis < PLAUSI_GRENZE;
|
||||||
const plausi = warn
|
const plausi = warn
|
||||||
? '<span style="background:#451a03;color:#fbbf24;padding:0.15rem 0.5rem;border-radius:4px;font-size:0.75rem">⚠ bitte prüfen</span>'
|
? '<span style="background:#451a03;color:#fbbf24;padding:0.15rem 0.5rem;border-radius:4px;font-size:0.75rem">⚠ bitte prüfen</span>'
|
||||||
|
|
@ -252,10 +254,16 @@ async function ladeUebersicht() {
|
||||||
const buchBtn = p.booking_url
|
const buchBtn = p.booking_url
|
||||||
? `<a href="${p.booking_url}" target="_blank" class="btn btn-sm" style="text-decoration:none">Öffnen ↗</a>`
|
? `<a href="${p.booking_url}" target="_blank" class="btn btn-sm" style="text-decoration:none">Öffnen ↗</a>`
|
||||||
: '—';
|
: '—';
|
||||||
return `<tr>
|
const scannerLabel = isMulticity
|
||||||
<td>${p.scanner}</td>
|
? `<strong style="color:#818cf8">🇭🇰 HKG Stopover</strong><br><span style="font-size:0.72rem;color:#64748b">+~${HOTEL_HKG}€ Hotel</span>`
|
||||||
|
: p.scanner;
|
||||||
|
const gesamtHtml = isMulticity
|
||||||
|
? `<strong style="color:${warn?'#fbbf24':'#a78bfa'}">${p.preis} €</strong><br><span style="font-size:0.75rem;color:#64748b">∑ ~${Math.round(p.preis)+HOTEL_HKG} € inkl. Hotel</span>`
|
||||||
|
: `<strong style="color:${warn?'#fbbf24':'#34d399'}">${p.preis} €</strong>`;
|
||||||
|
return `<tr${isMulticity?' style="background:rgba(99,102,241,0.06);border-left:3px solid #6366f1"':''}>
|
||||||
|
<td>${scannerLabel}</td>
|
||||||
<td style="font-size:0.8rem;color:#64748b">${p.node}</td>
|
<td style="font-size:0.8rem;color:#64748b">${p.node}</td>
|
||||||
<td><strong style="color:${warn?'#fbbf24':'#34d399'}">${p.preis} €</strong></td>
|
<td>${gesamtHtml}</td>
|
||||||
<td>${plausi}</td>
|
<td>${plausi}</td>
|
||||||
<td style="font-size:0.85rem">${p.abflug||'—'}</td>
|
<td style="font-size:0.85rem">${p.abflug||'—'}</td>
|
||||||
<td style="font-size:0.85rem">${p.ankunft||'—'}</td>
|
<td style="font-size:0.85rem">${p.ankunft||'—'}</td>
|
||||||
|
|
|
||||||
|
|
@ -22,20 +22,24 @@ def job():
|
||||||
trip_type = data.get("trip_type", "roundtrip")
|
trip_type = data.get("trip_type", "roundtrip")
|
||||||
kabine = data.get("kabine", "premium_economy")
|
kabine = data.get("kabine", "premium_economy")
|
||||||
gepaeck = data.get("gepaeck", "1koffer+handgepaeck")
|
gepaeck = data.get("gepaeck", "1koffer+handgepaeck")
|
||||||
airline_filter = data.get("airline_filter", "")
|
airline_filter = data.get("airline_filter", "")
|
||||||
layover_min = data.get("layover_min", 120)
|
layover_min = data.get("layover_min", 120)
|
||||||
layover_max = data.get("layover_max", 300)
|
layover_max = data.get("layover_max", 300)
|
||||||
max_flugzeit_h = data.get("max_flugzeit_h", 22)
|
max_flugzeit_h = data.get("max_flugzeit_h", 22)
|
||||||
max_stops = data.get("max_stops", 2)
|
max_stops = data.get("max_stops", 2)
|
||||||
|
via = data.get("via", "")
|
||||||
|
stopover_min_h = data.get("stopover_min_h", 20)
|
||||||
|
stopover_max_h = data.get("stopover_max_h", 30)
|
||||||
|
|
||||||
airline_label = f" [{airline_filter}]" if airline_filter else ""
|
airline_label = f" [{airline_filter}]" if airline_filter else ""
|
||||||
print(f"[{NODE_NAME}] Job: {scanner}{airline_label} {von}→{nach} | {kabine} | "
|
via_label = f" via {via}" if via else ""
|
||||||
|
print(f"[{NODE_NAME}] Job: {scanner}{airline_label}{via_label} {von}→{nach} | {kabine} | "
|
||||||
f"Umstieg {layover_min}-{layover_max}min | max {max_flugzeit_h}h/{max_stops} Stopps")
|
f"Umstieg {layover_min}-{layover_max}min | max {max_flugzeit_h}h/{max_stops} Stopps")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = scrape(scanner, von, nach, tage, aufenthalt, trip_type, kabine,
|
results = scrape(scanner, von, nach, tage, aufenthalt, trip_type, kabine,
|
||||||
gepaeck, airline_filter, layover_min, layover_max,
|
gepaeck, airline_filter, layover_min, layover_max,
|
||||||
max_flugzeit_h, max_stops)
|
max_flugzeit_h, max_stops, via, stopover_min_h, stopover_max_h)
|
||||||
print(f"[{NODE_NAME}] {len(results)} Preise gefunden")
|
print(f"[{NODE_NAME}] {len(results)} Preise gefunden")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"results": results,
|
"results": results,
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,21 @@ def scrape(scanner, von, nach, tage=30, aufenthalt_tage=60,
|
||||||
trip_type="roundtrip", kabine="premium_economy",
|
trip_type="roundtrip", kabine="premium_economy",
|
||||||
gepaeck="1koffer+handgepaeck", airline_filter="",
|
gepaeck="1koffer+handgepaeck", airline_filter="",
|
||||||
layover_min=120, layover_max=300,
|
layover_min=120, layover_max=300,
|
||||||
max_flugzeit_h=22, max_stops=2):
|
max_flugzeit_h=22, max_stops=2,
|
||||||
|
via="", stopover_min_h=20, stopover_max_h=30):
|
||||||
dispatcher = {
|
dispatcher = {
|
||||||
"google_flights": scrape_google_flights,
|
"google_flights": scrape_google_flights,
|
||||||
"kayak": scrape_kayak,
|
"kayak": scrape_kayak,
|
||||||
"skyscanner": scrape_skyscanner,
|
"kayak_multicity": scrape_kayak_multicity,
|
||||||
"trip": scrape_trip,
|
"skyscanner": scrape_skyscanner,
|
||||||
|
"trip": scrape_trip,
|
||||||
}
|
}
|
||||||
fn = dispatcher.get(scanner)
|
fn = dispatcher.get(scanner)
|
||||||
if not fn:
|
if not fn:
|
||||||
raise ValueError(f"Unbekannter Scanner: {scanner}")
|
raise ValueError(f"Unbekannter Scanner: {scanner}")
|
||||||
|
if scanner == "kayak_multicity":
|
||||||
|
return fn(von, nach, tage, aufenthalt_tage, kabine, gepaeck,
|
||||||
|
airline_filter, via, stopover_min_h, stopover_max_h)
|
||||||
return fn(von, nach, tage, aufenthalt_tage, trip_type, kabine, gepaeck,
|
return fn(von, nach, tage, aufenthalt_tage, trip_type, kabine, gepaeck,
|
||||||
airline_filter, layover_min, layover_max, max_flugzeit_h, max_stops)
|
airline_filter, layover_min, layover_max, max_flugzeit_h, max_stops)
|
||||||
|
|
||||||
|
|
@ -386,6 +391,106 @@ def scrape_trip(von, nach, tage=30, aufenthalt_tage=60,
|
||||||
return results[:10]
|
return results[:10]
|
||||||
|
|
||||||
|
|
||||||
|
def _booking_url_kayak_multicity(von, nach, via, abflug, via_datum, rueck, kc, bags=1, airline=""):
|
||||||
|
"""
|
||||||
|
Kayak Multi-City URL: FRA→HKG/DATE1 → HKG→KTI/DATE2 → KTI→FRA/DATE3
|
||||||
|
Kabinen-Code: w=Premium Economy
|
||||||
|
"""
|
||||||
|
filters = []
|
||||||
|
if bags:
|
||||||
|
filters.append(f"bfc%3D{bags}")
|
||||||
|
if airline:
|
||||||
|
filters.append(f"airlines%3D{airline}")
|
||||||
|
fs = ("&fs=" + "%3B".join(filters)) if filters else ""
|
||||||
|
# Kayak Multi-City Format: /flights/FRA-HKG/DATE/HKG-KTI/DATE/KTI-FRA/DATE
|
||||||
|
return (f"https://www.kayak.de/flights"
|
||||||
|
f"/{von}-{via}/{abflug}"
|
||||||
|
f"/{via}-{nach}/{via_datum}"
|
||||||
|
f"/{nach}-{von}/{rueck}"
|
||||||
|
f"?sort=price_a&cabin={kc}¤cy=EUR{fs}")
|
||||||
|
|
||||||
|
|
||||||
|
def scrape_kayak_multicity(von, nach, tage=30, aufenthalt_tage=60,
|
||||||
|
kabine="premium_economy",
|
||||||
|
gepaeck="1koffer+handgepaeck",
|
||||||
|
airline_filter="",
|
||||||
|
via="HKG", stopover_min_h=20, stopover_max_h=30):
|
||||||
|
"""
|
||||||
|
Multi-City Suche: FRA → HKG (1 Tag Aufenthalt) → KTI → FRA
|
||||||
|
Nutzt Cathay Pacific (CX) oder alle Airlines wenn airline_filter leer.
|
||||||
|
"""
|
||||||
|
abflug = (datetime.now() + timedelta(days=tage)).strftime("%Y-%m-%d")
|
||||||
|
via_datum = (datetime.now() + timedelta(days=tage + 1)).strftime("%Y-%m-%d")
|
||||||
|
rueck = (datetime.now() + timedelta(days=tage + 1 + aufenthalt_tage)).strftime("%Y-%m-%d")
|
||||||
|
kc = KABINE_KAYAK.get(kabine, "w")
|
||||||
|
bags = 1 if "koffer" in gepaeck else 0
|
||||||
|
airline_label = f" [{airline_filter}]" if airline_filter else ""
|
||||||
|
|
||||||
|
booking_url = _booking_url_kayak_multicity(von, nach, via, abflug, via_datum, rueck,
|
||||||
|
kc, bags, airline_filter)
|
||||||
|
booking_url_raw = _booking_url_kayak_multicity(von, nach, via, abflug, via_datum, rueck,
|
||||||
|
kc, 0, airline_filter)
|
||||||
|
|
||||||
|
print(f"[MC{airline_label}] Multi-City via {via}: {abflug} → +1T → {rueck}")
|
||||||
|
print(f"[MC{airline_label}] URL: {booking_url}")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
with SB(uc=True, headless=True, chromium_arg="--no-sandbox --disable-dev-shm-usage") as sb:
|
||||||
|
sb.open(booking_url)
|
||||||
|
sb.sleep(15)
|
||||||
|
|
||||||
|
title = sb.get_title()
|
||||||
|
body = sb.get_text("body")
|
||||||
|
print(f"[MC] Title: {title[:80]}")
|
||||||
|
|
||||||
|
for sel in ['.price-text', '.f8F1-price-text', 'div[class*="price"] span',
|
||||||
|
'span[class*="price"]', '.Iqt3', 'div.nrc6-price', '.price']:
|
||||||
|
try:
|
||||||
|
elems = sb.find_elements(sel, timeout=2)
|
||||||
|
if elems:
|
||||||
|
for e in elems[:15]:
|
||||||
|
p = _parse_preis(e.text)
|
||||||
|
if p and p > 600:
|
||||||
|
results.append({
|
||||||
|
"scanner": "kayak_multicity",
|
||||||
|
"preis": p,
|
||||||
|
"waehrung": "EUR",
|
||||||
|
"airline": airline_filter or via,
|
||||||
|
"abflug": abflug,
|
||||||
|
"ankunft": rueck,
|
||||||
|
"booking_url": booking_url,
|
||||||
|
})
|
||||||
|
if results:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
for r in _preise_aus_body(body, "kayak_multicity", abflug):
|
||||||
|
if r["preis"] > 600:
|
||||||
|
r["ankunft"] = rueck
|
||||||
|
r["booking_url"] = booking_url
|
||||||
|
r["airline"] = airline_filter or via
|
||||||
|
results.append(r)
|
||||||
|
|
||||||
|
# Fallback ohne Bags-Filter
|
||||||
|
if not results and bags > 0:
|
||||||
|
print(f"[MC] Kein Ergebnis mit Bags — Fallback ohne Bags-Filter")
|
||||||
|
sb.open(booking_url_raw)
|
||||||
|
sb.sleep(12)
|
||||||
|
body2 = sb.get_text("body")
|
||||||
|
for r in _preise_aus_body(body2, "kayak_multicity", abflug):
|
||||||
|
if r["preis"] > 600:
|
||||||
|
r["ankunft"] = rueck
|
||||||
|
r["booking_url"] = booking_url_raw
|
||||||
|
r["airline"] = airline_filter or via
|
||||||
|
results.append(r)
|
||||||
|
|
||||||
|
print(f"[MC{airline_label}] Ergebnis: {[r['preis'] for r in results[:5]]}")
|
||||||
|
return results[:10]
|
||||||
|
|
||||||
|
|
||||||
def scrape_skyscanner(von, nach, tage=30, aufenthalt_tage=60,
|
def scrape_skyscanner(von, nach, tage=30, aufenthalt_tage=60,
|
||||||
trip_type="roundtrip", kabine="premium_economy",
|
trip_type="roundtrip", kabine="premium_economy",
|
||||||
gepaeck="1koffer+handgepaeck", airline_filter="",
|
gepaeck="1koffer+handgepaeck", airline_filter="",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue