fix: KöPi - exakte marktguru Selektoren, saubere Anzeige
This commit is contained in:
@@ -50,58 +50,25 @@ async function scrapeMarktguru() {
|
|||||||
await new Promise(r => setTimeout(r, 4000));
|
await new Promise(r => setTimeout(r, 4000));
|
||||||
|
|
||||||
const offers = await page.evaluate(() => {
|
const offers = await page.evaluate(() => {
|
||||||
const results = [];
|
// Exakte Selektoren basierend auf marktguru HTML-Struktur
|
||||||
// Angebots-Karten selektieren
|
const cards = document.querySelectorAll('li.offer-list-item');
|
||||||
const cards = document.querySelectorAll('.offer-item, [class*="offer-item"], .search-offer, [class*="searchOffer"]');
|
return Array.from(cards).map(card => {
|
||||||
|
const name = card.querySelector('h3')?.textContent?.trim() || '';
|
||||||
// Fallback: alle Container die Preis + Händler enthalten
|
const brand = card.querySelector('dd.brand a')?.textContent?.trim() || '';
|
||||||
const containers = cards.length > 0 ? cards :
|
const price = card.querySelector('span.price')?.textContent?.trim() || '';
|
||||||
document.querySelectorAll('[class*="offer"]:not([class*="more"]):not([class*="list"]):not([class*="section"])');
|
const oldPrice = card.querySelector('.price-bubble .old-price, .crossed')?.textContent?.trim() || '';
|
||||||
|
const retailer = card.querySelector('dd.retailer-name a')?.textContent?.trim() || '';
|
||||||
containers.forEach(card => {
|
const dateRange= card.querySelector('dd.valid')?.textContent?.trim() || '';
|
||||||
const text = card.innerText || card.textContent || '';
|
const daysLeft = card.querySelector('dd.time-left span')?.textContent?.trim() || '';
|
||||||
if (!text.includes('König') && !text.includes('Pilsen')) return;
|
const daysText = card.querySelector('dd.time-left')?.textContent?.trim() || '';
|
||||||
|
const validity = daysLeft ? `Noch ${daysLeft} Tag${daysLeft==='1'?'':'e'}` :
|
||||||
const img = card.querySelector('img');
|
card.querySelector('dt.time-left')?.textContent?.includes('Brandneu') ? 'Neu' : '';
|
||||||
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
|
const descEl = card.querySelector('.info div, .info');
|
||||||
|
const description = descEl?.textContent?.trim() || '';
|
||||||
// Felder aus Text extrahieren
|
const img = card.querySelector('img.offer-list-item-img');
|
||||||
let name='', brand='', price='', retailer='', validity='', dateRange='', description='', oldPrice='';
|
const badge = card.querySelector('.badge, .discount-badge, [class*="badge"]')?.textContent?.trim() || '';
|
||||||
|
return { name, brand, price, oldPrice, retailer, dateRange, validity, description, badge, image: img?.src || null };
|
||||||
lines.forEach((line, i) => {
|
|
||||||
if (line.match(/^€\s*\d/) && !price) price = line;
|
|
||||||
if (line.match(/^\d+[,\.]\d+\s*€/) && !price) price = line;
|
|
||||||
if (line.includes('Händler:')) retailer = lines[i+1] || line.replace('Händler:','').trim();
|
|
||||||
if (line.includes('Marke:')) brand = lines[i+1] || line.replace('Marke:','').trim();
|
|
||||||
if (line.includes('Gültig:') || line.includes('gültig:')) dateRange = lines[i+1] || line.replace(/G.ltig:/i,'').trim();
|
|
||||||
if (line.includes('Noch') && line.includes('Tag')) validity = line;
|
|
||||||
if (line.includes('/ l') || line.includes('/l') || line.includes('Kasten') || line.includes('Flasche')) description = line;
|
|
||||||
if (line.includes('Brandneu')) validity = 'Neu';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Name ist meist die erste Zeile
|
|
||||||
name = lines[0] || '';
|
|
||||||
|
|
||||||
// Direkte DOM-Selektion als Alternative
|
|
||||||
const priceEl = card.querySelector('[class*="price"],[class*="Price"]');
|
|
||||||
const retailEl = card.querySelector('[class*="retailer"],[class*="Retailer"],[class*="store"],[class*="Store"]');
|
|
||||||
const nameEl = card.querySelector('[class*="title"],[class*="name"],h3,h2');
|
|
||||||
const brandEl = card.querySelector('[class*="brand"],[class*="Brand"]');
|
|
||||||
const descEl = card.querySelector('[class*="desc"],[class*="info"],[class*="detail"]');
|
|
||||||
|
|
||||||
if (priceEl) price = priceEl.textContent.trim();
|
|
||||||
if (retailEl) retailer = retailEl.textContent.trim();
|
|
||||||
if (nameEl) name = nameEl.textContent.trim();
|
|
||||||
if (brandEl) brand = brandEl.textContent.trim();
|
|
||||||
if (descEl) description = descEl.textContent.trim();
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
name, brand, price, oldPrice, retailer, validity, dateRange, description,
|
|
||||||
image: img?.src || null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return results;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filtern auf Ziel-Händler + nur König Pilsener
|
// Filtern auf Ziel-Händler + nur König Pilsener
|
||||||
|
|||||||
@@ -35,17 +35,23 @@ function OfferCard({ offer }) {
|
|||||||
display:'flex', flexDirection:'column',
|
display:'flex', flexDirection:'column',
|
||||||
}}>
|
}}>
|
||||||
{/* Produktbild */}
|
{/* Produktbild */}
|
||||||
<div style={{ height:150, background:'#fff', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, overflow:'hidden' }}>
|
<div style={{ height:160, background:'#fff', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, overflow:'hidden', position:'relative' }}>
|
||||||
{offer.image
|
{offer.image
|
||||||
? <img src={`/api/tools/koepi/img?url=${encodeURIComponent(offer.image)}`}
|
? <img src={`/api/tools/koepi/img?url=${encodeURIComponent(offer.image)}`}
|
||||||
alt={offer.name} style={{ maxHeight:'100%', maxWidth:'100%', objectFit:'contain' }}
|
alt={offer.name} style={{ maxHeight:'100%', maxWidth:'100%', objectFit:'contain' }}
|
||||||
onError={e => { e.target.parentElement.innerHTML = '<div style="color:#555;font-size:40px">🍺</div>'; }}
|
onError={e => { e.target.style.display='none'; }}
|
||||||
/>
|
/>
|
||||||
: <div style={{ fontSize:40 }}>🍺</div>
|
: null
|
||||||
}
|
}
|
||||||
|
{offer.badge && (
|
||||||
|
<div style={{ position:'absolute', top:6, left:6, background:'#e2001a', color:'#fff',
|
||||||
|
borderRadius:4, padding:'2px 6px', fontSize:9, fontFamily:'monospace', fontWeight:700 }}>
|
||||||
|
{offer.badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ padding:'12px 14px', flex:1, display:'flex', flexDirection:'column', gap:6 }}>
|
<div style={{ padding:'12px 14px', flex:1, display:'flex', flexDirection:'column', gap:5 }}>
|
||||||
{/* Händler Badge */}
|
{/* Händler Badge */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display:'inline-block', alignSelf:'flex-start',
|
display:'inline-block', alignSelf:'flex-start',
|
||||||
@@ -54,17 +60,19 @@ function OfferCard({ offer }) {
|
|||||||
color, fontFamily:'monospace', fontSize:10, fontWeight:700,
|
color, fontFamily:'monospace', fontSize:10, fontWeight:700,
|
||||||
}}>{offer.retailer}</div>
|
}}>{offer.retailer}</div>
|
||||||
|
|
||||||
{/* Name */}
|
{/* Name + Marke */}
|
||||||
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:13, fontWeight:600, lineHeight:1.3 }}>
|
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:13, fontWeight:600, lineHeight:1.3 }}>
|
||||||
{offer.name}
|
König Pilsener
|
||||||
{offer.brand && offer.brand !== offer.name && (
|
|
||||||
<span style={{ color:'rgba(255,255,255,0.4)', fontSize:10, marginLeft:6 }}>{offer.brand}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{offer.name && offer.name.toLowerCase() !== 'könig pilsener' && (
|
||||||
|
<div style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:11 }}>
|
||||||
|
{offer.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Beschreibung/Flaschengröße */}
|
{/* Beschreibung/Flaschengröße */}
|
||||||
{offer.description && (
|
{offer.description && (
|
||||||
<div style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:10, lineHeight:1.4 }}>
|
<div style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:10, lineHeight:1.5 }}>
|
||||||
{offer.description}
|
{offer.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -82,15 +90,15 @@ function OfferCard({ offer }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gültigkeit */}
|
{/* Gültigkeit */}
|
||||||
<div style={{ marginTop:'auto', paddingTop:6, borderTop:'1px solid rgba(255,255,255,0.06)' }}>
|
<div style={{ marginTop:'auto', paddingTop:6, borderTop:'1px solid rgba(255,255,255,0.06)', display:'flex', flexDirection:'column', gap:3 }}>
|
||||||
{offer.validity && (
|
{offer.validity && (
|
||||||
<div style={{ color: offer.validity.includes('Neu') ? '#4ecdc4' : '#f59e0b', fontFamily:'monospace', fontSize:10 }}>
|
<div style={{ color:'#f59e0b', fontFamily:'monospace', fontSize:10 }}>
|
||||||
⏰ {offer.validity}
|
⏰ {offer.validity}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{offer.dateRange && (
|
{offer.dateRange && (
|
||||||
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10, marginTop:2 }}>
|
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10 }}>
|
||||||
📅 {offer.dateRange}
|
📅 Gültig: {offer.dateRange}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user