#!/usr/bin/env python3 """ Template: Chefkoch Nightly Scraper (async Playwright) Scrapes recipe metadata from Chefkoch.de via headless browser. Intended to be customized per search query and run as background job. Prerequisite: playwright install chromium """ import asyncio, json, re from datetime import datetime from pathlib import Path from playwright.async_api import async_playwright # ─── Config ───────────────────────────────────────────────────────────────── QUERY = "asiatisch einfach familie" MAX_RESULTS = 10 OUTPUT_DIR = Path.home() / "Documents/Essensplanung" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) OUTPUT = OUTPUT_DIR / f"scrape_{re.sub(r'[^a-z0-9]', '_', QUERY.lower())}_{datetime.now().strftime('%Y%m%d')}.json" # ─── Playwright ───────────────────────────────────────────────────────────── async def accept_cookies(page): try: await page.click("button[title='Zustimmen']", timeout=2000) except: pass async def scrape_page(browser, query, max_results): results = [] url = f"https://www.chefkoch.de/rs/s0/{query.replace(' ', '%20')}/Rezepte.html" page = await browser.new_page() try: await page.goto(url, wait_until="networkidle", timeout=30000) await accept_cookies(page) # Scrolle bis genug Karten geladen for _ in range(15): cards = await page.query_selector_all(".ds-recipe-card") if len(cards) >= max_results * 2: break await page.evaluate("window.scrollBy(0, 800)") await asyncio.sleep(1.5) # Extrahiere Links links = await page.eval_on_selector_all( "a.ds-recipe-card__title-link", "els => [...new Set(els.map(e => e.href))]" ) print(f" {len(links)} Links gefunden") # Lade Details for link in list(dict.fromkeys(links))[:max_results]: detail = await get_recipe_detail(browser, link) if detail: results.append(detail) print(f" + {detail['name'][:40]}") await asyncio.sleep(0.5) finally: await page.close() return results async def get_recipe_detail(browser, url): page = await browser.new_page() try: await page.goto(url, wait_until="networkidle", timeout=15000) await accept_cookies(page) # Name name = await page.eval_on_selector("h1", "e => e?.innerText.trim() || 'Unbekannt'") # Zutaten ings = [] rows = await page.query_selector_all("table.ingredients tbody tr") for row in rows[:15]: try: amt = await row.eval_on_selector("td.amount", "e => e.innerText.trim()") item = await row.eval_on_selector("td.name", "e => e.innerText.trim()") if item: ings.append({"amount": amt, "item": item}) except: pass # Zeit time = await page.eval_on_selector( ".ds-recipe-meta__icon-text, .bi-recipe-meta-duration", "e => e?.innerText.trim() || '?'" ) return { "name": name, "url": url, "ingredients": ings, "prep_time_text": time, } except Exception as e: print(f" ⚠️ Fehler: {e}") return None finally: await page.close() # ─── Main ─────────────────────────────────────────────────────────────────── async def main(): print(f"🔍 Scrape: '{QUERY}' → {MAX_RESULTS}x") async with async_playwright() as p: browser = await p.chromium.launch(headless=True) results = await scrape_page(browser, QUERY, MAX_RESULTS) await browser.close() OUTPUT.write_text(json.dumps(results, indent=2, ensure_ascii=False), encoding="utf-8") print(f"💾 {len(results)} Rezepte → {OUTPUT}") if __name__ == "__main__": asyncio.run(main())