Live-Crawler scharf: Frontend-Overlay (LIVE-gescrapte OBI/Bauhaus-Preise im Monitor) + 404/410-Erkennung (Produktseite entfernt) – Edge, den braucheklima nicht hat
This commit is contained in:
+8
-1
@@ -55,6 +55,10 @@ def parse(htmltext):
|
|||||||
async def fetch_one(client, src):
|
async def fetch_one(client, src):
|
||||||
try:
|
try:
|
||||||
r= await client.get(src["url"], headers=UA, timeout=20, follow_redirects=True)
|
r= await client.get(src["url"], headers=UA, timeout=20, follow_redirects=True)
|
||||||
|
if r.status_code in (404,410):
|
||||||
|
# Produktseite verschwunden -> als entfernt kennzeichnen (das kann braucheklima nicht)
|
||||||
|
return {"chain":src["chain"],"kind":src["kind"],"productId":src["productId"],"name":src["name"],
|
||||||
|
"url":src["url"],"price":None,"availability":"discontinued","status":r.status_code}
|
||||||
if r.status_code!=200:
|
if r.status_code!=200:
|
||||||
return {**src,"price":None,"availability":"unknown","status":r.status_code}
|
return {**src,"price":None,"availability":"unknown","status":r.status_code}
|
||||||
price, avail = parse(r.text)
|
price, avail = parse(r.text)
|
||||||
@@ -83,13 +87,16 @@ async def crawl():
|
|||||||
p=prev.get((o["chain"],o["productId"]))
|
p=prev.get((o["chain"],o["productId"]))
|
||||||
if p and p.get("availability")=="out_of_stock" and o.get("availability") in ("in_stock","online_available"):
|
if p and p.get("availability")=="out_of_stock" and o.get("availability") in ("in_stock","online_available"):
|
||||||
hist["events"].append({"chain":o["chain"],"productId":o["productId"],"name":o["name"],"at":now})
|
hist["events"].append({"chain":o["chain"],"productId":o["productId"],"name":o["name"],"at":now})
|
||||||
|
if p and p.get("availability")!="discontinued" and o.get("availability")=="discontinued":
|
||||||
|
hist["events"].append({"chain":o["chain"],"productId":o["productId"],"name":o["name"],"at":now,"type":"discontinued"})
|
||||||
hist["events"]=hist["events"][-200:]
|
hist["events"]=hist["events"][-200:]
|
||||||
# Preis-Aggregat je Produkt
|
# Preis-Aggregat je Produkt
|
||||||
byp={}
|
byp={}
|
||||||
for o in results:
|
for o in results:
|
||||||
if o.get("price"): byp.setdefault(o["productId"],[]).append(o["price"])
|
if o.get("price"): byp.setdefault(o["productId"],[]).append(o["price"])
|
||||||
agg={k:{"min":min(v),"max":max(v),"n":len(v)} for k,v in byp.items()}
|
agg={k:{"min":min(v),"max":max(v),"n":len(v)} for k,v in byp.items()}
|
||||||
snap={"generatedAt":now,"source":"kuehlfinder-livecrawler","intervalMin":INTERVAL_MIN,
|
gone=[o for o in results if o.get("availability")=="discontinued"]
|
||||||
|
snap={"generatedAt":now,"source":"kuehlfinder-livecrawler","intervalMin":INTERVAL_MIN,"discontinued":len(gone),
|
||||||
"offers":results,"priceByProduct":agg,"feed":hist["events"][-20:][::-1]}
|
"offers":results,"priceByProduct":agg,"feed":hist["events"][-20:][::-1]}
|
||||||
json.dump(snap, open(LIVE,"w"), ensure_ascii=False)
|
json.dump(snap, open(LIVE,"w"), ensure_ascii=False)
|
||||||
json.dump(hist, open(HIST,"w"), ensure_ascii=False)
|
json.dump(hist, open(HIST,"w"), ensure_ascii=False)
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
|
|||||||
.osrc b{font-size:.82rem}.osrc .num{font-size:.92rem}.osrc small{font-size:.71rem;color:var(--slate)}
|
.osrc b{font-size:.82rem}.osrc .num{font-size:.92rem}.osrc small{font-size:.71rem;color:var(--slate)}
|
||||||
.osrc.wucher{border-color:var(--danger);background:var(--danger-bg)}.osrc.wucher .num{color:var(--danger)}
|
.osrc.wucher{border-color:var(--danger);background:var(--danger-bg)}.osrc.wucher .num{color:var(--danger)}
|
||||||
.trust{font-size:.68rem;color:var(--slate)}
|
.trust{font-size:.68rem;color:var(--slate)}
|
||||||
|
.livebadge{display:inline-flex;align-items:center;gap:.4rem;font-size:.8rem;font-weight:600;color:var(--ok-dot);background:var(--ok-bg);border:1px solid var(--ok-dot);border-radius:999px;padding:.35rem .7rem;align-self:center}
|
||||||
.tablewrap{margin-top:.8rem;padding:0;overflow:hidden}
|
.tablewrap{margin-top:.8rem;padding:0;overflow:hidden}
|
||||||
.mtab{width:100%;border-collapse:collapse;font-size:.9rem}
|
.mtab{width:100%;border-collapse:collapse;font-size:.9rem}
|
||||||
.mtab thead th{text-align:left;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;color:var(--slate);font-weight:600;padding:.7rem .8rem;border-bottom:1px solid var(--line);background:var(--card);white-space:nowrap}
|
.mtab thead th{text-align:left;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;color:var(--slate);font-weight:600;padding:.7rem .8rem;border-bottom:1px solid var(--line);background:var(--card);white-space:nowrap}
|
||||||
@@ -142,6 +143,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
|
|||||||
<script>
|
<script>
|
||||||
import maplibregl from 'maplibre-gl';
|
import maplibregl from 'maplibre-gl';
|
||||||
const FLAG={DE:'🇩🇪',AT:'🇦🇹',CH:'🇨🇭',NL:'🇳🇱',PL:'🇵🇱',DK:'🇩🇰'};
|
const FLAG={DE:'🇩🇪',AT:'🇦🇹',CH:'🇨🇭',NL:'🇳🇱',PL:'🇵🇱',DK:'🇩🇰'};
|
||||||
|
const LIVE_URL='http://vnqwgi4gq520hs1p4bsgi4w6.46.224.49.66.sslip.io';
|
||||||
const fmt=(n,c)=>n==null?'–':new Intl.NumberFormat('de-DE',{style:'currency',currency:c||'EUR',maximumFractionDigits:0}).format(n);
|
const fmt=(n,c)=>n==null?'–':new Intl.NumberFormat('de-DE',{style:'currency',currency:c||'EUR',maximumFractionDigits:0}).format(n);
|
||||||
const $=s=>document.querySelector(s),$$=s=>Array.from(document.querySelectorAll(s));
|
const $=s=>document.querySelector(s),$$=s=>Array.from(document.querySelectorAll(s));
|
||||||
let DATA=null,center=null,shown=120;
|
let DATA=null,center=null,shown=120;
|
||||||
@@ -211,6 +213,21 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
|
|||||||
$$('#countrychips .chip').forEach(b=>b.addEventListener('click',()=>{const c=b.dataset.cc;state.countries.has(c)?(state.countries.delete(c),b.classList.remove('on')):(state.countries.add(c),b.classList.add('on'));render();}));
|
$$('#countrychips .chip').forEach(b=>b.addEventListener('click',()=>{const c=b.dataset.cc;state.countries.has(c)?(state.countries.delete(c),b.classList.remove('on')):(state.countries.add(c),b.classList.add('on'));render();}));
|
||||||
if(state.model!=='all')$('#model').value=state.model;
|
if(state.model!=='all')$('#model').value=state.model;
|
||||||
if(state.q)$('#q').value=state.q;
|
if(state.q)$('#q').value=state.q;
|
||||||
|
loadLive();
|
||||||
|
}
|
||||||
|
function loadLive(){
|
||||||
|
fetch(LIVE_URL+'/api/live').then(r=>r.json()).then(d=>{
|
||||||
|
if(!d||!d.offers||!d.offers.length)return;
|
||||||
|
const when=d.generatedAt?new Date(d.generatedAt):null;
|
||||||
|
const mins=when?Math.max(1,Math.round((Date.now()-when)/60000)):'?';
|
||||||
|
const items=d.offers.filter(o=>o.price).map(o=>{
|
||||||
|
const av=o.availability==='in_stock'?'im Markt':(o.availability==='online_available'?'online':(o.availability==='discontinued'?'⚠ Seite entfernt (404)':'vergriffen'));
|
||||||
|
const cls=(o.availability==='out_of_stock')?'':'';
|
||||||
|
return `<a class="osrc ${o.availability==='discontinued'?'wucher':''}" href="${o.url}" target="_blank" rel="sponsored nofollow noopener"><b>${o.chain} · LIVE</b><span class="num">${fmt(o.price)}</span><small>${o.name} · ${av}</small></a>`;
|
||||||
|
}).join('');
|
||||||
|
const bar=$('#onlinebar');
|
||||||
|
bar.insertAdjacentHTML('afterbegin', `<span class="livebadge" title="eigener Live-Crawler"><span class="dot ok live"></span> LIVE gescrapt · Stand vor ${mins} Min</span>`+items);
|
||||||
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
map.on('load',()=>{
|
map.on('load',()=>{
|
||||||
map.addSource('all',{type:'geojson',data:{type:'FeatureCollection',features:[]},cluster:true,clusterRadius:46,clusterMaxZoom:9});
|
map.addSource('all',{type:'geojson',data:{type:'FeatureCollection',features:[]},cluster:true,clusterRadius:46,clusterMaxZoom:9});
|
||||||
|
|||||||
Reference in New Issue
Block a user