Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve rezeptwelt.de recipe parsing #1295

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 83 additions & 7 deletions recipe_scrapers/rezeptwelt.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
from bs4 import Tag
import re

from ._abstract import AbstractScraper
from ._exceptions import SchemaOrgException, StaticValueException
from ._utils import normalize_string
from ._utils import normalize_string, get_minutes
from ._grouping_utils import IngredientGroup

# fiter to find non-empty tags
nonempty = re.compile(r".+")


def has_css_class(tag, cssclass):
classes = tag.get("class")
if not classes:
return False
if isinstance(classes, list):
return cssclass in classes
return classes == cssclass


class Rezeptwelt(AbstractScraper):
Expand All @@ -9,19 +25,69 @@ def host(cls):
return "rezeptwelt.de"

def site_name(self):
raise StaticValueException(return_value="Rezeptwelt")
return "Thermomix Rezeptwelt"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "Thermomix Rezeptwelt"
raise StaticValueException(return_value="Thermomix Rezeptwelt")

I admit this is a slightly unusual pattern that we use; it is used so that the interface of the library can indicate whether values were retrieved from the source HTML or whether they are static/constant values returned by the code.


def author(self):
return normalize_string(self.soup.find("span", {"id": "viewRecipeAuthor"}).text)
tag = self.soup.find("div", itemprop="author")
if tag:
return normalize_string(tag.get_text())
tag = self.soup.find("span", {"id": "viewRecipeAuthor"})
return normalize_string(tag.get_text())
Comment on lines +31 to +35
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some observations here:

  • The retrieval from an itemprop="author" attribute is essentially schema.org metadata retrieval; we have an existing helper method to implement that, so let's re-use them here.
  • The information contained in the viewRecipeAuthor element seems more-specific than the schema metadata, which is sometimes generic. So let's prefer viewRecipeAuthor when mentioned.

What this leads me to when adapting the code locally is:

Suggested change
tag = self.soup.find("div", itemprop="author")
if tag:
return normalize_string(tag.get_text())
tag = self.soup.find("span", {"id": "viewRecipeAuthor"})
return normalize_string(tag.get_text())
name_from_schema = self.schema.author()
name_from_hyperlink = None
tag = self.soup.find("span", {"id": "viewRecipeAuthor"})
if tag:
name_from_hyperlink = tag.get_text()
return normalize_string(name_from_hyperlink or name_from_schema)

Note: the word von in some of the test data seems redundant, so we can remove that (these changes affect that).


def ingredients(self) -> list[str]:
results = []
for ingredient_group in self.ingredient_groups():
results.extend(ingredient_group.ingredients)
return results

def ingredient_groups(self) -> list[IngredientGroup]:
ingredient_groups = []
group = None
ingredients = None
section = self.soup.find(id="ingredient-section")
# iterate over all tags in the ingredient section
# for each <p class="h5"> start a new group
# for each <tag itemprop="recipeIngredient"> add a new ingredient
for child in section.descendants:
if isinstance(child, Tag):
if child.name == "p" and has_css_class(child, "h5"):
if ingredients:
# save previous group
ingredient_groups.append(IngredientGroup(purpose=group, ingredients=ingredients))
# group might be an empty string, but that is ok
group = child.text.strip()
ingredients = []
elif child.get("itemprop", "") == "recipeIngredient":
ingredients.append(child.text)
if ingredients:
# group can be None if there is only one main group for all ingredients
ingredient_groups.append(IngredientGroup(purpose=group, ingredients=ingredients))
return ingredient_groups

def instructions(self):
container = self.soup.find("div", id="preparationSteps").find(
"span", itemprop="text"
)
instructions = [
normalize_string(paragraph.text) for paragraph in container.find_all("p")
]
return "\n".join(filter(None, instructions))
instructions = []
for p in container.find_all("p"):
text = p.get_text().strip()
if text:
instructions.append(text)
if not instructions:
# instructions are divided by "<br>"
for text in str(container).replace("<br/>", "\n").replace("\r", "").splitlines():
text = normalize_string(text.strip())
if text:
instructions.append(text)
# add optional tips to instructions
container = self.soup.find("div", attrs={"class": "tips"})
for p in container.find_all("p"):
if p and p.string:
for text in str(p).replace("<br/>", "\n").replace("\r", "").splitlines():
text = normalize_string(text.strip())
if text:
instructions.append(text)
return "\n".join(instructions)

def cuisine(self):
try:
Expand All @@ -36,3 +102,13 @@ def description(self):

def language(self):
return self.soup.find("meta", {"property": "og:locale"})["content"]

def prep_time(self):
tag = self.soup.find(itemprop="performTime", content=nonempty)
return get_minutes(tag['content']) if tag else None

def equipment(self):
return [tag['content'] for tag in self.soup.find_all("meta", itemprop="tool", content=nonempty)]
Comment on lines +106 to +111
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BeautifulSoup (bs4 / self.soup) allows non-empty content filtering by passing a boolean True value, so I think we can simplify these methods slightly:

Suggested change
def prep_time(self):
tag = self.soup.find(itemprop="performTime", content=nonempty)
return get_minutes(tag['content']) if tag else None
def equipment(self):
return [tag['content'] for tag in self.soup.find_all("meta", itemprop="tool", content=nonempty)]
def prep_time(self):
tag = self.soup.find(itemprop="performTime", content=True)
return get_minutes(tag['content']) if tag else None
def equipment(self):
return [tag['content'] for tag in self.soup.find_all("meta", itemprop="tool", content=True)]


def reviews(self):
return None
38 changes: 35 additions & 3 deletions tests/test_data/rezeptwelt.de/rezeptwelt.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"author": "Kräuterwiese",
"canonical_url": "rezeptwelt.de",
"site_name": "Rezeptwelt",
"site_name": "Thermomix Rezeptwelt",
"host": "rezeptwelt.de",
"language": "de_DE",
"title": "Italienischer Nudelsalat",
Expand All @@ -21,23 +21,55 @@
"1 TL Salz",
"2 Prisen Pfeffer"
],
"ingredient_groups": [
{
"ingredients": [
"250 g Nudeln z.B. Fusilli,Penne",
"1000 g Wasser",
"1 EL Gemüsebrühe",
"100 g Tomaten getrocknet, ohne Öl",
"500 g Kirschtomaten halbiert",
"1 Kugel Mozarella, halbiert",
"8 Stück schwarze Oliven, entsteint",
"100 g Rucola/Rauke",
"2 Zehen Knoblauch",
"1 Bund Basilkum (ohne Stiele)",
"45 g Weißweinessig",
"40 g Olivenöl",
"1 TL Salz",
"2 Prisen Pfeffer"
],
"purpose": ""
}
],
"instructions": "Salat\n1. Wasser und Gemüsebrühe in den Mixtopf geben und 8Min./100°/Stufe 1 aufkochen\n2. Nudeln zugeben, je nach Packungsanweisung ca. 8-10Min./100°/\"Linkslauf\" /Stufe \"Sanftrührstufe\" garen, Nudeln in den Gareinsatz abgießen und Garflüssigkeit dabei auffangen. Nudeln in eine große Schüssel umfüllen und etwas abkühlen lassen.\n3. getrocknete Tomaten in den Mixtopf geben, 5Sek./\"Linkslauf deaktiviert\" /Stufe 4 zerkleinern und zu den Nudeln geben.\n4.Mozarella in den Mixtopf geben, 2 Sek./Stufe 4 zerkleinern und zu den Nudeln geben.\n5. Alle weiteren Salatzutaten (Rucola, schwarze Oliven und Kirschtomaten) in die große Schüssel zugeben und vermischen\nDressing:\n6. Knoblauch und Basilikum in den Mixtopf geben 4 Sek./Stufe 7 zerkleinern.\n7. Übrige Zutaten und 120 g Garflüssigkeit zugeben und 15Sek./Stufe 3-4 verrühren, zum Salat geben und vermischen.\nEin Sattmacher-Rezept.\nDieser Salat enthält nicht nur alle Farben der italienischen Flagge, er schmeckt auch typisch mediterran !\nWW geeignet.",
"instructions_list": [
"Salat",
"1. Wasser und Gemüsebrühe in den Mixtopf geben und 8Min./100°/Stufe 1 aufkochen",
"2. Nudeln zugeben, je nach Packungsanweisung ca. 8-10Min./100°/\"Linkslauf\" /Stufe \"Sanftrührstufe\" garen, Nudeln in den Gareinsatz abgießen und Garflüssigkeit dabei auffangen. Nudeln in eine große Schüssel umfüllen und etwas abkühlen lassen.",
"2. Nudeln zugeben, je nach Packungsanweisung ca. 8-10Min./100°/\"Linkslauf\" /Stufe \"Sanftrührstufe\" garen, Nudeln in den Gareinsatz abgießen und Garflüssigkeit dabei auffangen. Nudeln in eine große Schüssel umfüllen und etwas abkühlen lassen.",
"3. getrocknete Tomaten in den Mixtopf geben, 5Sek./\"Linkslauf deaktiviert\" /Stufe 4 zerkleinern und zu den Nudeln geben.",
"4.Mozarella in den Mixtopf geben, 2 Sek./Stufe 4 zerkleinern und zu den Nudeln geben.",
"5. Alle weiteren Salatzutaten (Rucola, schwarze Oliven und Kirschtomaten) in die große Schüssel zugeben und vermischen",
"Dressing:",
"6. Knoblauch und Basilikum in den Mixtopf geben 4 Sek./Stufe 7 zerkleinern.",
"7. Übrige Zutaten und 120 g Garflüssigkeit zugeben und 15Sek./Stufe 3-4 verrühren, zum Salat geben und vermischen."
"7. Übrige Zutaten und 120 g Garflüssigkeit zugeben und 15Sek./Stufe 3-4 verrühren, zum Salat geben und vermischen.",
"Ein Sattmacher-Rezept.",
"Dieser Salat enthält nicht nur alle Farben der italienischen Flagge, er schmeckt auch typisch mediterran !",
"WW geeignet."
],
"category": "Vorspeisen/Salate",
"yields": "6 servings",
"description": "Italienischer Nudelsalat, ein Rezept der Kategorie Vorspeisen/Salate.",
"total_time": 50,
"prep_time": 50,
"cuisine": null,
"ratings": 4.68,
"ratings_count": 358,
"equipment": [
"Spatel",
"2. Mixtopf TM6"
],
"reviews": null,
"nutrients": {},
"image": "https://de.rc-cdn.community.thermomix.com/recipeimage/wbtt7xp3-9544c-831497-cfcd2-6bis4hp6/d60b5483-c12d-4c2a-871d-05748e5aa06c/main/italienischer-nudelsalat.jpg"
}
96 changes: 96 additions & 0 deletions tests/test_data/rezeptwelt.de/rezeptwelt_2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"author": "von tilasaniy",
"canonical_url": "rezeptwelt.de",
"site_name": "Thermomix Rezeptwelt",
"host": "rezeptwelt.de",
"language": "de_DE",
"title": "Wirsing-Kartoffel-Hack-Auflauf",
"ingredients": [
"800 g Kartoffeln, gewürfelt",
"1 Stück kleiner Wirsing, in Streifen geschnitten",
"800 g Wasser",
"1 geh. TL Suppengrundstock (selbstgemacht), oder Gemüsebrühe",
"600 g Hackfleisch, (wir nehmen Rind)",
"4 EL Tomatenketchup",
"4 EL Tomatenmark",
" Pfeffer/Kräuter-Salz/Muskat",
"500 g Schupfnudeln aus dem Kühlregal, (bei Bedarf - Auflauf reicht dann für 6 Personen!)",
"50 g Butter",
"80 g Mehl",
"300 g Milch",
"300 g Garflüssigkeit, (vom Gemüse garen)",
"1 TL Salz",
" Öl für die Form",
"200 g Käse gerieben, zum Überbacken"
],
"ingredient_groups": [
{
"ingredients": [
"800 g Kartoffeln, gewürfelt",
"1 Stück kleiner Wirsing, in Streifen geschnitten",
"800 g Wasser",
"1 geh. TL Suppengrundstock (selbstgemacht), oder Gemüsebrühe",
"600 g Hackfleisch, (wir nehmen Rind)",
"4 EL Tomatenketchup",
"4 EL Tomatenmark",
" Pfeffer/Kräuter-Salz/Muskat",
"500 g Schupfnudeln aus dem Kühlregal, (bei Bedarf - Auflauf reicht dann für 6 Personen!)"
],
"purpose": "Auflauf"
},
{
"ingredients": [
"50 g Butter",
"80 g Mehl",
"300 g Milch",
"300 g Garflüssigkeit, (vom Gemüse garen)",
"1 TL Salz"
],
"purpose": "Béchamelsoße"
},
{
"ingredients": [
" Öl für die Form",
"200 g Käse gerieben, zum Überbacken"
],
"purpose": "Ausserdem"
}
],
"instructions": "Die Kartoffeln in das Garkörbchen geben. Den Wirsing in Streifen geschnitten in den Varoma geben.\nWasser und Suppengrundstock in den Mixtopf geben, Garkörbchen einhängen, Varoma aufsetzen und alles 25 Min./Varoma/Stufe 1 kochen.\nHackfleischmasse\nWährend das Gemüse kocht: Hackfleisch in der Pfanne krümelig braten. Wenn es durch ist, das Ketchup und das Tomatenmark sowie die Gewürze zugeben und nochmals kurz braten.\nBechamelsosse\nBackofen auf 200 °C Ober-/Unterhitze vorheizen.\nWenn der Wirsing fertig ist, Varoma und Garkörbchen zu Seite stellen und die Garflüssigkeit aus dem Mixtopf auffangen (300 g).\nButter im Mixtopf 2 Min./100°C/Stufe 1 schmelzen, Mehl dazu geben und 1 Min./100°C/Stufe 2 anschwitzen.\nMilch und Garflüssigkeit hinzufügen und 7 Min./100°C/Stufe 2 einkochen. Danach mit dem Salz würzen und nochmals kurz 10 Sek./Stufe 5 verrühren.\nIn die geölte Auflaufform erst etwas Bechamelsoße (1/3), dann die Schupfnudeln (bei Bedarf) und die Kartoffeln geben. Den Wirsing einfüllen, 1/3 Soße dazu, dann die Hackfleischmasse darauf und die restliche Bechamelsoße drüber verteilen. Mit geriebenem Käse bestreuen.\nIm vorgeheizten Backofen bei 200 °C Ober-/Unterhitze etwa 25 - 30 Minuten backen.\nDieses Rezept ist eine Variation vom \"Wirsing-Auflauf\" von littlecloud51277. Vielen Dank für die Anregung!",
"instructions_list": [
"Die Kartoffeln in das Garkörbchen geben. Den Wirsing in Streifen geschnitten in den Varoma geben.",
"Wasser und Suppengrundstock in den Mixtopf geben, Garkörbchen einhängen, Varoma aufsetzen und alles 25 Min./Varoma/Stufe 1 kochen.",
"Hackfleischmasse",
"Während das Gemüse kocht: Hackfleisch in der Pfanne krümelig braten. Wenn es durch ist, das Ketchup und das Tomatenmark sowie die Gewürze zugeben und nochmals kurz braten.",
"Bechamelsosse",
"Backofen auf 200 °C Ober-/Unterhitze vorheizen.",
"Wenn der Wirsing fertig ist, Varoma und Garkörbchen zu Seite stellen und die Garflüssigkeit aus dem Mixtopf auffangen (300 g).",
"Butter im Mixtopf 2 Min./100°C/Stufe 1 schmelzen, Mehl dazu geben und 1 Min./100°C/Stufe 2 anschwitzen.",
"Milch und Garflüssigkeit hinzufügen und 7 Min./100°C/Stufe 2 einkochen. Danach mit dem Salz würzen und nochmals kurz 10 Sek./Stufe 5 verrühren.",
"In die geölte Auflaufform erst etwas Bechamelsoße (1/3), dann die Schupfnudeln (bei Bedarf) und die Kartoffeln geben. Den Wirsing einfüllen, 1/3 Soße dazu, dann die Hackfleischmasse darauf und die restliche Bechamelsoße drüber verteilen. Mit geriebenem Käse bestreuen.",
"Im vorgeheizten Backofen bei 200 °C Ober-/Unterhitze etwa 25 - 30 Minuten backen.",
"Dieses Rezept ist eine Variation vom \"Wirsing-Auflauf\" von littlecloud51277. Vielen Dank für die Anregung!"
],
"category": "Hauptgerichte mit Fleisch",
"yields": "5 servings",
"description": "Wirsing-Kartoffel-Hack-Auflauf, ein Rezept der Kategorie Hauptgerichte mit Fleisch. Mehr Thermomix® Rezepte auf www.rezeptwelt.de",
"total_time": 70,
"cook_time": null,
"prep_time": 40,
"cuisine": "Europäisch",
"ratings": 4.69,
"ratings_count": 94,
"equipment": [
"Spatel",
"Auflaufform Anna",
"Spülbürste Set",
"2. Mixtopf TM6"
],
"reviews": null,
"nutrients": {},
"image": "https://de.rc-cdn.community.thermomix.com/recipeimage/vwpr9gab-d4f55-857620-cfcd2-f55fe74i/2f7c6e32-d54b-49df-839e-e7cb02a20eaf/original/wirsing-kartoffel-hack-auflauf.jpg",
"keywords": [
"Europäisch",
"europaisch"
]
}
Loading