150 lines
4.2 KiB
Python
150 lines
4.2 KiB
Python
import random
|
||
import re
|
||
from html import unescape
|
||
from html.parser import HTMLParser
|
||
from urllib.parse import urljoin
|
||
from urllib.request import Request, urlopen
|
||
|
||
|
||
CHEFKOCH_TODAY_URL = "https://www.chefkoch.de/rezepte/was-koche-ich-heute/"
|
||
|
||
FALLBACK_RECIPES = [
|
||
{
|
||
"title": "Cremige Pasta mit Tomaten",
|
||
"url": "https://www.chefkoch.de/",
|
||
"image_url": "",
|
||
"meta": "ca. 25 Minuten",
|
||
},
|
||
{
|
||
"title": "Kartoffel-Brokkoli-Auflauf",
|
||
"url": "https://www.chefkoch.de/",
|
||
"image_url": "",
|
||
"meta": "vegetarisch",
|
||
},
|
||
{
|
||
"title": "Schnelle Reispfanne",
|
||
"url": "https://www.chefkoch.de/",
|
||
"image_url": "",
|
||
"meta": "einfach",
|
||
},
|
||
{
|
||
"title": "Wraps mit Hähnchen",
|
||
"url": "https://www.chefkoch.de/",
|
||
"image_url": "",
|
||
"meta": "schnell",
|
||
},
|
||
{
|
||
"title": "Gemüse-Curry",
|
||
"url": "https://www.chefkoch.de/",
|
||
"image_url": "",
|
||
"meta": "würzig",
|
||
},
|
||
{
|
||
"title": "Ofenkartoffeln mit Dip",
|
||
"url": "https://www.chefkoch.de/",
|
||
"image_url": "",
|
||
"meta": "günstig",
|
||
},
|
||
{
|
||
"title": "Gnocchi-Pfanne",
|
||
"url": "https://www.chefkoch.de/",
|
||
"image_url": "",
|
||
"meta": "15 Minuten",
|
||
},
|
||
{
|
||
"title": "Pizza-Toast",
|
||
"url": "https://www.chefkoch.de/",
|
||
"image_url": "",
|
||
"meta": "MVP-Klassiker",
|
||
},
|
||
]
|
||
|
||
|
||
class ChefkochTodayParser(HTMLParser):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.recipes = []
|
||
self._current_link = None
|
||
self._current_text = []
|
||
self._current_image_url = ""
|
||
|
||
def handle_starttag(self, tag, attrs):
|
||
attrs = dict(attrs)
|
||
if tag == "a":
|
||
href = attrs.get("href", "")
|
||
if re.search(r"/rezepte/\d+/.+\.html", href):
|
||
self._current_link = urljoin(CHEFKOCH_TODAY_URL, href)
|
||
self._current_text = []
|
||
self._current_image_url = ""
|
||
elif tag == "img" and self._current_link:
|
||
self._current_image_url = attrs.get("src") or attrs.get("data-src") or ""
|
||
|
||
def handle_data(self, data):
|
||
if self._current_link:
|
||
self._current_text.append(data)
|
||
|
||
def handle_endtag(self, tag):
|
||
if tag != "a" or not self._current_link:
|
||
return
|
||
|
||
title, meta = parse_recipe_text(" ".join(self._current_text))
|
||
if title and not any(recipe["url"] == self._current_link for recipe in self.recipes):
|
||
self.recipes.append(
|
||
{
|
||
"title": title,
|
||
"url": self._current_link,
|
||
"image_url": self._current_image_url,
|
||
"meta": meta,
|
||
}
|
||
)
|
||
|
||
self._current_link = None
|
||
self._current_text = []
|
||
self._current_image_url = ""
|
||
|
||
|
||
def parse_recipe_text(text):
|
||
text = unescape(re.sub(r"\s+", " ", text)).strip()
|
||
text = text.replace("rating_star", "").strip()
|
||
|
||
rating_match = re.match(r"(?P<rating>\d+[,.]\d)\s*/?\s*5?\s*\((?P<count>\d+)\)\s*(?P<title>.+)", text)
|
||
if rating_match:
|
||
rating = rating_match.group("rating")
|
||
count = rating_match.group("count")
|
||
title = clean_recipe_title(rating_match.group("title"))
|
||
return title, f"{rating}/5 ({count})"
|
||
|
||
return clean_recipe_title(text), ""
|
||
|
||
|
||
def clean_recipe_title(title):
|
||
title = re.sub(r"\s+von\s+essen\s*&\s*trinken.*$", "", title, flags=re.IGNORECASE)
|
||
title = re.sub(r"[^\w\sÄÖÜäöüß.,&'’:/()\-]", "", title)
|
||
return re.sub(r"\s+", " ", title).strip()
|
||
|
||
|
||
def fetch_chefkoch_today_recipes():
|
||
cache_buster = random.randint(100000, 999999)
|
||
request = Request(
|
||
f"{CHEFKOCH_TODAY_URL}?rf={cache_buster}",
|
||
headers={
|
||
"User-Agent": "Mozilla/5.0 (compatible; RezeptFinder/1.0)",
|
||
"Cache-Control": "no-cache",
|
||
},
|
||
)
|
||
with urlopen(request, timeout=8) as response:
|
||
html = response.read().decode("utf-8", errors="replace")
|
||
|
||
parser = ChefkochTodayParser()
|
||
parser.feed(html)
|
||
return parser.recipes[:8]
|
||
|
||
|
||
def get_recipes():
|
||
try:
|
||
recipes = fetch_chefkoch_today_recipes()
|
||
except Exception:
|
||
return FALLBACK_RECIPES
|
||
|
||
return recipes or FALLBACK_RECIPES
|