-
Notifications
You must be signed in to change notification settings - Fork 559
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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): | ||||||||||||||||||||||||||||
|
@@ -9,19 +25,69 @@ def host(cls): | |||||||||||||||||||||||||||
return "rezeptwelt.de" | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
def site_name(self): | ||||||||||||||||||||||||||||
raise StaticValueException(return_value="Rezeptwelt") | ||||||||||||||||||||||||||||
return "Thermomix Rezeptwelt" | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some observations here:
What this leads me to when adapting the code locally is:
Suggested change
Note: the word |
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
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: | ||||||||||||||||||||||||||||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BeautifulSoup (
Suggested change
|
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
def reviews(self): | ||||||||||||||||||||||||||||
return None |
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" | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.