179 lines
6.9 KiB
Python
179 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Spiele-Offensive Top-30: Strict AID-to-image mapping from listing pages."""
|
|
import re, urllib.request, sys
|
|
from datetime import datetime
|
|
|
|
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
|
|
NOW = datetime.now().strftime("%d.%m.%Y %H:%M")
|
|
OUT = "/home/hermes/workspace/spiele_offensive_top30.html"
|
|
|
|
def fetch(url, timeout=15):
|
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
raw = r.read()
|
|
try: return raw.decode("utf-8")
|
|
except: return raw.decode("iso-8859-1", errors="replace")
|
|
|
|
# ── Build AID→image map ──
|
|
aid_img = {}
|
|
|
|
for page in range(10):
|
|
try:
|
|
h = fetch(f"https://www.spiele-offensive.de/Kat/Sonderangebote.html?sse={page}", 15)
|
|
except Exception as e:
|
|
print(f" p{page}: {e}", file=sys.stderr); continue
|
|
|
|
for m in re.finditer(
|
|
r'registerItemSelection\(dataLayer,\s*(\d+)\s*,.*?'
|
|
r'<center><img src="https://mediaservice[^"]*/([^/"]+)"',
|
|
h, re.DOTALL
|
|
):
|
|
aid, fn = m.group(1), m.group(2).strip()
|
|
if aid not in aid_img:
|
|
aid_img[aid] = fn
|
|
|
|
# Also startpage
|
|
try:
|
|
hp = fetch("https://www.spiele-offensive.de/", 20)
|
|
for m in re.finditer(
|
|
r"registerPromotionSelection\(dataLayer,\s*\d+\s*,[^)]+,[^)]+,\s*(\d+)\s*,[^)]+\)",
|
|
hp
|
|
):
|
|
aid = m.group(1)
|
|
if aid not in aid_img:
|
|
tail = hp[m.end():m.end()+2000]
|
|
im = re.search(r'<img src="https://mediaservice[^"]*/([^/"]+)"', tail)
|
|
if im:
|
|
aid_img[aid] = im.group(1).strip()
|
|
except Exception as e:
|
|
print(f" hp: {e}", file=sys.stderr)
|
|
|
|
print(f"Map: {len(aid_img)} images", file=sys.stderr)
|
|
|
|
# ── Read deals ──
|
|
with open("/home/hermes/workspace/spiele_offensive_deals.html") as f:
|
|
dhtml = f.read()
|
|
|
|
deals = []
|
|
for m in re.finditer(
|
|
r'<div class="card-name">([^<]+)</div>.*?'
|
|
r'<div class="card-badge"[^>]*>([^<]+)</div>.*?'
|
|
r'<span class="card-old">([^<]*)</span>.*?'
|
|
r'<span class="card-new">([^<]+)</span>.*?'
|
|
r'<a href="[^"]*aid=(\d+)',
|
|
dhtml, re.DOTALL
|
|
):
|
|
rabatt = int(re.search(r'(\d+)', m.group(2)).group(1))
|
|
deals.append({
|
|
"name": m.group(1).strip(),
|
|
"rabatt": rabatt,
|
|
"old": m.group(3).strip().replace(" €", ""),
|
|
"new": m.group(4).strip().replace(" €", ""),
|
|
"aid": m.group(5).strip()
|
|
})
|
|
|
|
SKIP = ['puzzle', 'jigsaw', 'wundertute', 'ratselpuzzle', 'tragetasche']
|
|
deals = [d for d in deals if not any(w in d["name"].lower() for w in SKIP)]
|
|
seen = set(); uniq = []
|
|
for d in deals:
|
|
if d["aid"] not in seen: seen.add(d["aid"]); uniq.append(d)
|
|
uniq.sort(key=lambda d: -d["rabatt"])
|
|
top30 = uniq[:30]
|
|
|
|
# ── Test images ──
|
|
def img_ok(aid, fn):
|
|
url = "https://mediaservice.happyshops.com/ANY/Article/{}/width=200/{}".format(
|
|
int(aid).__format__("012d"), fn)
|
|
try:
|
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
|
with urllib.request.urlopen(req, timeout=5) as r:
|
|
return r.headers.get("Content-Type", "").startswith("image/")
|
|
except: return False
|
|
|
|
# ── Build HTML ──
|
|
def img_url(aid, fn):
|
|
return "https://mediaservice.happyshops.com/ANY/Article/{}/width=200/{}".format(
|
|
int(aid).__format__("012d"), fn)
|
|
|
|
cards = []
|
|
ok = 0
|
|
for d in top30:
|
|
name = d["name"]
|
|
rabatt = d["rabatt"]
|
|
old = d["old"]
|
|
new = d["new"]
|
|
aid = d["aid"]
|
|
|
|
saved = ""
|
|
try:
|
|
s = float(old.replace(",",".")) - float(new.replace(",","."))
|
|
saved = "({:.2f} EUR gespart)".format(s).replace(".", ",")
|
|
except: pass
|
|
|
|
bc = "#b12704" if rabatt >= 80 else ("#e63946" if rabatt >= 60 else "#cc0c39")
|
|
|
|
fn = aid_img.get(aid)
|
|
if fn and img_ok(aid, fn):
|
|
src = img_url(aid, fn)
|
|
img = '<img src="{}" alt="" class="dimg" loading="lazy">'.format(src)
|
|
ok += 1
|
|
elif fn:
|
|
src = img_url(aid, fn)
|
|
img = '<img src="{}" alt="" class="dimg" loading="lazy">'.format(src)
|
|
else:
|
|
img = '<div class="dimg noimg">?</div>'
|
|
|
|
cards.append("""<div class="d">
|
|
""" + img + """
|
|
<div class="dinfo">
|
|
<span class="dname">""" + name + """</span>
|
|
<span class="dprice">""" + new + """ EUR</span><span class="cold">UVP """ + old + """ EUR</span>
|
|
<span class="dbadge" style="background:""" + bc + """">-""" + str(rabatt) + """%</span>
|
|
<span class="dsave">""" + saved + """</span>
|
|
<a href="https://www.spiele-offensive.de/index.php?cmd=artikel_anzeigen&aid=""" + aid + """&pid=505" target="_blank" class="dlink">Zu Spiele-Offensive →</a>
|
|
</div></div>""")
|
|
|
|
html = """<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<title>Top 30 Brettspiel-Angebote \u2014 Spiele-Offensive.de</title>
|
|
<style>
|
|
body{margin:0;padding:0;background:#f9f7f4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#2c2416;line-height:1.6}
|
|
article{max-width:720px;margin:0 auto;padding:2rem 1.5rem;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.06)}
|
|
h1{font-size:1.5rem;color:#1a1a1a}.meta{color:#999;font-size:.85rem;margin-bottom:1.5rem}
|
|
a{color:#007185;text-decoration:none}a:hover{text-decoration:underline}
|
|
.d{display:flex;gap:1rem;padding:1rem 0;border-bottom:1px solid #eee;align-items:flex-start}
|
|
.dimg{width:100px;height:100px;object-fit:contain;flex-shrink:0;border-radius:4px;background:#f5f5f5}
|
|
.noimg{display:flex;align-items:center;justify-content:center;color:#ccc;font-size:2rem}
|
|
.dinfo{flex:1;min-width:0}
|
|
.dname{font-weight:600;color:#1a1a1a;font-size:.95rem;display:block;line-height:1.3;margin-bottom:3px}
|
|
.dprice{color:#b12704;font-size:1.2rem;font-weight:bold}
|
|
.cold{text-decoration:line-through;color:#999;margin-left:.5rem;font-size:.9rem}
|
|
.dbadge{color:#fff;padding:2px 6px;border-radius:3px;margin-left:.5rem;font-size:.85rem;font-weight:600}
|
|
.dsave{color:#b12704;margin-left:.3rem;font-size:.9rem}
|
|
.dlink{display:inline-block;margin-top:4px;color:#e63946;font-size:.9rem;font-weight:500}
|
|
.dlink:hover{text-decoration:underline}
|
|
.footer{margin-top:2rem;padding-top:1rem;border-top:1px solid #eee;font-size:.85rem;color:#888}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<article>
|
|
<h1>Top 30 Brettspiel-Angebote auf Spiele-Offensive.de</h1>
|
|
<p class="meta">Stand: """ + NOW + """ · Sortiert nach Rabatt · Preise inkl. MwSt. · Affiliate-Links (PID 505) · Bilder: Spiele-Offensive</p>
|
|
|
|
""" + "\n".join(cards) + """
|
|
|
|
<div class="footer">
|
|
<p>Alle Preise und Verf\u00fcgbarkeiten zum Zeitpunkt der Erhebung. Zwischenverkauf und Preis\u00e4nderungen vorbehalten.</p>
|
|
<p>Dieser Beitrag enth\u00e4lt Affiliate-Links. Bei einem Kauf \u00fcber diese Links erhalten wir eine kleine Provision \u2014 f\u00fcr dich entstehen keine Mehrkosten.</p>
|
|
</div>
|
|
</article></body></html>"""
|
|
|
|
with open(OUT, "w") as f:
|
|
f.write(html)
|
|
|
|
print("Done: {} deals, {} verified images, {} bytes".format(len(top30), ok, len(html)), file=sys.stderr)
|
|
print("FILE:{}".format(OUT))
|