Repository: hhursev/recipe-scrapers
Pull request #982 (2024-01-11 00:44:46)

Adds equipment as possible output
Inspired by #958 this adds support for equipment as a possible output and adds coverage to 17 sites. I'm sure more sites can also provide this information.

I did not add 'equipment' to the generated test cases when making a new scraper as its rare that its available. I plan to update the documentation sometime this week!

jayaddison: (also: I think this will be a great feature to provide!)
jknndy: > This could be nitpicky, but I'd prefer if the equipment lists were _not_ sorted, but remain in the order that they were listed in the recipe (with duplicates removed, though).

for some reason some of the tests were failing without some sort of sorting feature. The tests were returning in a different order each time. `forktospoon` being an example, what do you think of using a format like this where it ensures the items retain the original order while still checking for duplicates?
<details>
  <summary>Sites that have sorting</summary>

  ```
recipe_scrapers/amazingribs.py
recipe_scrapers/bakingsense.py
recipe_scrapers/bowlofdelicious.py
recipe_scrapers/budgetbytes.py
recipe_scrapers/downshiftology.py
recipe_scrapers/elavegan.py
recipe_scrapers/herseyland.py
recipe_scrapers/joyfoodsunshine.py
recipe_scrapers/momswithcrockpots.py
  ```
</details>

```python
    def equipment(self):
        seen = set()
        return [
            equip.get_text()
            for equip in self.soup.find_all("div", class_="wprm-recipe-equipment-name")
            if equip.get_text()
            and (equip.get_text() not in seen and not seen.add(equip.get_text()))
        ]
```



jayaddison: > what do you think of using a format like this where it ensures the items retain the original order while still checking for duplicates?

I'm not so keen on that approach, for two reasons:

  * It's a fairly complex construction - it's smart, but it doesn't feel Pythonic to me.
  * Similarly, placing a side-effect (the `seen.add` call) into a [comprehension](https://docs.python.org/3/reference/expressions.html#comprehensions) seems like a code smell.

I think this could be a situation where creating a utility/helper method would get us to the neatest result.
jknndy: I plan to take a look at this tomorrow, should have a fix before the weekend
jknndy: Done! Been on the road for work a lot of the month so took a while to get to it. Added a utility all the clean up and implemented it in all the sites i listed last time.

Let me know if this is what you had in mind

Patch:
diff --git a/README.rst b/README.rst
index 2f7e90d3..d6c25f7f 100644
--- a/README.rst
+++ b/README.rst
@@ -56,8 +56,9 @@ then:
     scraper.yields()
     scraper.to_json()
     scraper.links()
-    scraper.nutrients()  # if available
-    scraper.canonical_url()  # also not always available
+    scraper.nutrients()  # not always available
+    scraper.canonical_url()  # not always available
+    scraper.equipment()  # not always available
 
 You also have an option to scrape html-like content
 
diff --git a/docs/in-depth-guide-scraper-functions.md b/docs/in-depth-guide-scraper-functions.md
index 82ed2be0..22249648 100644
--- a/docs/in-depth-guide-scraper-functions.md
+++ b/docs/in-depth-guide-scraper-functions.md
@@ -304,3 +304,12 @@ scraper.ratings()
 ### `reviews() -> List[Dict[str, str]]`
 
 Returns a `list` of reviews about the recipe from the website. Each review is a `dict` containing the reviewer's name (`str`) and their review (`str`).
+
+### `equipment() -> List[str] | None`
+
+Returns a list of cooking equipment needed for provided recipe.
+
+```py
+>>> scraper.equipment()
+['Mixing Bowl', 'Whisk', 'Baking Tray']
+```
diff --git a/recipe_scrapers/_abstract.py b/recipe_scrapers/_abstract.py
index 2bac3d5d..4d31d987 100644
--- a/recipe_scrapers/_abstract.py
+++ b/recipe_scrapers/_abstract.py
@@ -53,7 +53,7 @@ class AbstractScraper:
 
         # attach the plugins as instructed in settings.PLUGINS
         if not hasattr(self.__class__, "plugins_initialized"):
-            for name, func in inspect.getmembers(self, inspect.ismethod):
+            for name, _ in inspect.getmembers(self, inspect.ismethod):
                 current_method = getattr(self.__class__, name)
                 for plugin in reversed(settings.PLUGINS):
                     if plugin.should_run(self.host(), name):
@@ -164,6 +164,9 @@ class AbstractScraper:
     def reviews(self):
         raise NotImplementedError("This should be implemented.")
 
+    def equipment(self):
+        raise NotImplementedError("This should be implemented.")
+
     def links(self):
         invalid_href = {"#", ""}
         links_html = self.soup.findAll("a", href=True)
diff --git a/recipe_scrapers/_utils.py b/recipe_scrapers/_utils.py
index 6434c23b..4b9a3e20 100644
--- a/recipe_scrapers/_utils.py
+++ b/recipe_scrapers/_utils.py
@@ -171,6 +171,17 @@ def get_yields(element):
     return "{} serving{}".format(matched, "" if int(matched) == 1 else "s")
 
 
+def get_equipment(equipment_items):
+    # Removes duplicates from results and sorts them in order they appear on site.
+    seen = set()
+    unique_equipment = []
+    for item in equipment_items:
+        if item not in seen:
+            seen.add(item)
+            unique_equipment.append(item)
+    return unique_equipment
+
+
 def normalize_string(string):
     # Convert all named and numeric character references (e.g. &gt;, &#62;)
     unescaped_string = html.unescape(string)
diff --git a/recipe_scrapers/addapinch.py b/recipe_scrapers/addapinch.py
index 54277d49..e1d74c87 100644
--- a/recipe_scrapers/addapinch.py
+++ b/recipe_scrapers/addapinch.py
@@ -49,3 +49,14 @@ class AddAPinch(AbstractScraper):
 
     def description(self):
         return self.schema.description()
+
+    def equipment(self):
+        return list(
+            dict.fromkeys(
+                (equip.find("a").get_text())
+                for equip in self.soup.find_all(
+                    "div", class_="wprm-recipe-equipment-name"
+                )
+                if equip.find("a") and equip.find("a").get_text()
+            )
+        )
diff --git a/recipe_scrapers/altonbrown.py b/recipe_scrapers/altonbrown.py
index 8828ed67..d5629560 100644
--- a/recipe_scrapers/altonbrown.py
+++ b/recipe_scrapers/altonbrown.py
@@ -48,3 +48,13 @@ class AltonBrown(AbstractScraper):
 
     def description(self):
         return self.schema.description()
+
+    def equipment(self):
+        return list(
+            dict.fromkeys(
+                (equip.get_text())
+                for equip in self.soup.find_all(
+                    "div", class_="wprm-recipe-equipment-name"
+                )
+            )
+        )
diff --git a/recipe_scrapers/amazingribs.py b/recipe_scrapers/amazingribs.py
index 7c9c7713..065d86a8 100644
--- a/recipe_scrapers/amazingribs.py
+++ b/recipe_scrapers/amazingribs.py
@@ -2,6 +2,7 @@
 
 from ._abstract import AbstractScraper
 from ._grouping_utils import group_ingredients
+from ._utils import get_equipment, normalize_string
 
 
 class AmazingRibs(AbstractScraper):
@@ -34,3 +35,10 @@ class AmazingRibs(AbstractScraper):
 
     def instructions(self):
         return self.schema.instructions()
+
+    def equipment(self):
+        equipment_items = [
+            normalize_string(e.get_text())
+            for e in self.soup.find_all("div", class_="wprm-recipe-equipment-name")
+        ]
+        return sorted(get_equipment(equipment_items))
diff --git a/recipe_scrapers/bakingsense.py b/recipe_scrapers/bakingsense.py
index 8725f513..8609c770 100644
--- a/recipe_scrapers/bakingsense.py
+++ b/recipe_scrapers/bakingsense.py
@@ -1,6 +1,7 @@
 # mypy: disallow_untyped_defs=False
 from ._abstract import AbstractScraper
 from ._grouping_utils import group_ingredients
+from ._utils import get_equipment
 
 
 class BakingSense(AbstractScraper):
@@ -45,3 +46,11 @@ class BakingSense(AbstractScraper):
 
     def description(self):
         return self.schema.description()
+
+    def equipment(self):
+        equipment_items = [
+            equip.get_text()
+            for equip in self.soup.find_all("div", class_="wprm-recipe-equipment-name")
+            if equip.get_text()
+        ]
+        return get_equipment(equipment_items)
diff --git a/recipe_scrapers/bowlofdelicious.py b/recipe_scrapers/bowlofdelicious.py
index 5f4bc0c7..06f6f078 100644
--- a/recipe_scrapers/bowlofdelicious.py
+++ b/recipe_scrapers/bowlofdelicious.py
@@ -1,5 +1,6 @@
 # mypy: disallow_untyped_defs=False
 from ._abstract import AbstractScraper
+from ._utils import get_equipment
 
 
 class BowlOfDelicious(AbstractScraper):
@@ -30,3 +31,12 @@ class BowlOfDelicious(AbstractScraper):
 
     def ratings(self):
         return self.schema.ratings()
+
+    def equipment(self):
+        equipment_items = [
+            link.get_text()
+            for link in self.soup.select(
+                "div.wprm-recipe-equipment-name a.wprm-recipe-equipment-link"
+            )
+        ]
+        return get_equipment(equipment_items)
diff --git a/recipe_scrapers/budgetbytes.py b/recipe_scrapers/budgetbytes.py
index d9126b93..741318b9 100644
--- a/recipe_scrapers/budgetbytes.py
+++ b/recipe_scrapers/budgetbytes.py
@@ -1,6 +1,7 @@
 # mypy: disallow_untyped_defs=False
 from ._abstract import AbstractScraper
 from ._grouping_utils import group_ingredients
+from ._utils import get_equipment
 
 
 class BudgetBytes(AbstractScraper):
@@ -36,3 +37,12 @@ class BudgetBytes(AbstractScraper):
 
     def ratings(self):
         return self.schema.ratings()
+
+    def equipment(self):
+        equipment_items = [
+            link.get_text()
+            for link in self.soup.select(
+                "div.wprm-recipe-equipment-name a.wprm-recipe-equipment-link"
+            )
+        ]
+        return get_equipment(equipment_items)
diff --git a/recipe_scrapers/downshiftology.py b/recipe_scrapers/downshiftology.py
index 54fe33da..b2db6013 100644
--- a/recipe_scrapers/downshiftology.py
+++ b/recipe_scrapers/downshiftology.py
@@ -1,5 +1,6 @@
 # mypy: disallow_untyped_defs=False
 from ._abstract import AbstractScraper
+from ._utils import get_equipment
 
 
 class Downshiftology(AbstractScraper):
@@ -30,3 +31,11 @@ class Downshiftology(AbstractScraper):
 
     def ratings(self):
         return self.schema.ratings()
+
+    def equipment(self):
+        equipment_items = [
+            item.find("a", class_="wprm-recipe-equipment-link").get_text()
+            for item in self.soup.find_all("div", class_="wprm-recipe-equipment-name")
+            if item.find("a", class_="wprm-recipe-equipment-link")
+        ]
+        return get_equipment(equipment_items)
diff --git a/recipe_scrapers/elavegan.py b/recipe_scrapers/elavegan.py
index 5819a2fa..a42f1960 100644
--- a/recipe_scrapers/elavegan.py
+++ b/recipe_scrapers/elavegan.py
@@ -2,6 +2,7 @@
 
 from ._abstract import AbstractScraper
 from ._grouping_utils import group_ingredients
+from ._utils import get_equipment
 
 
 class ElaVegan(AbstractScraper):
@@ -49,3 +50,14 @@ class ElaVegan(AbstractScraper):
 
     def description(self):
         return self.schema.description()
+
+    def equipment(self):
+        equipment_items = [
+            item.find("div", class_="wprm-recipe-equipment-name")
+            .get_text()
+            .rstrip("*")
+            .strip()
+            for item in self.soup.find_all("div", class_="wprm-recipe-equipment-item")
+            if item.find("div", class_="wprm-recipe-equipment-name")
+        ]
+        return get_equipment(equipment_items)
diff --git a/recipe_scrapers/fattoincasadabenedetta.py b/recipe_scrapers/fattoincasadabenedetta.py
index 8ab2fa39..a04f743d 100644
--- a/recipe_scrapers/fattoincasadabenedetta.py
+++ b/recipe_scrapers/fattoincasadabenedetta.py
@@ -46,3 +46,10 @@ class FattoInCasaDaBenedetta(AbstractScraper):
 
     def description(self):
         return self.schema.description()
+
+    def equipment(self):
+        return list(
+            dict.fromkeys(
+                (item.get_text()) for item in self.soup.find_all("p", class_="name")
+            )
+        )
diff --git a/recipe_scrapers/forktospoon.py b/recipe_scrapers/forktospoon.py
index db554915..a7a04095 100644
--- a/recipe_scrapers/forktospoon.py
+++ b/recipe_scrapers/forktospoon.py
@@ -40,3 +40,12 @@ class ForkToSpoon(AbstractScraper):
 
     def description(self):
         return self.schema.description()
+
+    def equipment(self):
+        seen = set()
+        return [
+            equip.get_text()
+            for equip in self.soup.find_all("div", class_="wprm-recipe-equipment-name")
+            if equip.get_text()
+            and (equip.get_text() not in seen and not seen.add(equip.get_text()))
+        ]
diff --git a/recipe_scrapers/herseyland.py b/recipe_scrapers/herseyland.py
index 5d5696c9..2f910f8e 100644
--- a/recipe_scrapers/herseyland.py
+++ b/recipe_scrapers/herseyland.py
@@ -1,6 +1,7 @@
 # mypy: allow-untyped-defs
 
 from ._abstract import AbstractScraper
+from ._utils import get_equipment
 
 
 class HerseyLand(AbstractScraper):
@@ -37,3 +38,11 @@ class HerseyLand(AbstractScraper):
 
     def description(self):
         return self.schema.description()
+
+    def equipment(self):
+        equipment_items = [
+            item.find("span", class_="h6").get_text()
+            for item in self.soup.find_all("div", class_="equipment-item")
+            if item.find("span", class_="h6")
+        ]
+        return get_equipment(equipment_items)
diff --git a/recipe_scrapers/joyfoodsunshine.py b/recipe_scrapers/joyfoodsunshine.py
index d4b829c3..7833c42a 100644
--- a/recipe_scrapers/joyfoodsunshine.py
+++ b/recipe_scrapers/joyfoodsunshine.py
@@ -1,6 +1,6 @@
 # mypy: disallow_untyped_defs=False
 from ._abstract import AbstractScraper
-from ._utils import normalize_string
+from ._utils import get_equipment, normalize_string
 
 
 class Joyfoodsunshine(AbstractScraper):
@@ -33,3 +33,11 @@ class Joyfoodsunshine(AbstractScraper):
 
     def ratings(self):
         return self.schema.ratings()
+
+    def equipment(self):
+        equipment_items = [
+            item.find("a", class_="wprm-recipe-equipment-link").get_text()
+            for item in self.soup.find_all("div", class_="wprm-recipe-equipment-name")
+            if item.find("a", class_="wprm-recipe-equipment-link")
+        ]
+        return get_equipment(equipment_items)
diff --git a/recipe_scrapers/lifestyleofafoodie.py b/recipe_scrapers/lifestyleofafoodie.py
index 720f6c17..4bdec6d8 100644
--- a/recipe_scrapers/lifestyleofafoodie.py
+++ b/recipe_scrapers/lifestyleofafoodie.py
@@ -49,3 +49,13 @@ class LifestyleOfAFoodie(AbstractScraper):
             "div.wprm-recipe-ingredient-group h4",
             "ul.wprm-recipe-ingredients li",
         )
+
+    def equipment(self):
+        return list(
+            dict.fromkeys(
+                (item.get_text())
+                for item in self.soup.find_all(
+                    "div", class_="wprm-recipe-equipment-name"
+                )
+            )
+        )
diff --git a/recipe_scrapers/ministryofcurry.py b/recipe_scrapers/ministryofcurry.py
index 7a7bf8b4..acf8d0c7 100644
--- a/recipe_scrapers/ministryofcurry.py
+++ b/recipe_scrapers/ministryofcurry.py
@@ -37,3 +37,13 @@ class MinistryOfCurry(AbstractScraper):
 
     def description(self):
         return self.schema.description()
+
+    def equipment(self):
+        return list(
+            {
+                (item.get_text())
+                for item in self.soup.find_all(
+                    "div", class_="wprm-recipe-equipment-name"
+                )
+            }
+        )
diff --git a/recipe_scrapers/momswithcrockpots.py b/recipe_scrapers/momswithcrockpots.py
index 0393dce0..63a56c12 100644
--- a/recipe_scrapers/momswithcrockpots.py
+++ b/recipe_scrapers/momswithcrockpots.py
@@ -1,5 +1,6 @@
 # mypy: disallow_untyped_defs=False
 from ._abstract import AbstractScraper
+from ._utils import get_equipment
 
 
 class MomsWithCrockPots(AbstractScraper):
@@ -24,3 +25,11 @@ class MomsWithCrockPots(AbstractScraper):
 
     def ratings(self):
         return self.schema.ratings()
+
+    def equipment(self):
+        equipment_items = [
+            item.find("a", class_="wprm-recipe-equipment-link").get_text()
+            for item in self.soup.find_all("div", class_="wprm-recipe-equipment-name")
+            if item.find("a", class_="wprm-recipe-equipment-link")
+        ]
+        return get_equipment(equipment_items)
diff --git a/recipe_scrapers/mykitchen101.py b/recipe_scrapers/mykitchen101.py
index ac438fa4..cc3729d9 100644
--- a/recipe_scrapers/mykitchen101.py
+++ b/recipe_scrapers/mykitchen101.py
@@ -41,3 +41,13 @@ class MyKitchen101(AbstractScraper):
                 if instruction.get_text()[:1].isdigit()
             ]
         )
+
+    def equipment(self):
+        return list(
+            {
+                normalize_string("".join(item.stripped_strings).split("(")[0].strip())
+                for item in self.soup.find_all(
+                    "div", class_="wprm-recipe-equipment-name"
+                )
+            }
+        )
diff --git a/recipe_scrapers/mykitchen101en.py b/recipe_scrapers/mykitchen101en.py
index 5a09e453..8e3037e0 100644
--- a/recipe_scrapers/mykitchen101en.py
+++ b/recipe_scrapers/mykitchen101en.py
@@ -24,3 +24,13 @@ class MyKitchen101en(AbstractScraper):
 
     def instructions(self):
         return self.schema.instructions()
+
+    def equipment(self):
+        return list(
+            {
+                ("".join(item.stripped_strings).split("(")[0].strip())
+                for item in self.soup.find_all(
+                    "div", class_="wprm-recipe-equipment-name"
+                )
+            }
+        )


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index 2ed51a30..08e4b812 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -31,6 +31,7 @@ OPTIONAL_TESTS = [
     "prep_time",
     "ratings",
     "reviews",
+    "equipment",
 ]
 
 
diff --git a/tests/library/test_utils.py b/tests/library/test_utils.py
index f561d0d2..92b1a22e 100644
--- a/tests/library/test_utils.py
+++ b/tests/library/test_utils.py
@@ -105,6 +105,7 @@ class TestUtils(unittest.TestCase):
                 "cook_time",
                 "cuisine",
                 "description",
+                "equipment",
                 "host",
                 "image",
                 "ingredient_groups",
diff --git a/tests/test_data/addapinch.com/addapinch_2.json b/tests/test_data/addapinch.com/addapinch_2.json
index dd922a75..3734f0a7 100644
--- a/tests/test_data/addapinch.com/addapinch_2.json
+++ b/tests/test_data/addapinch.com/addapinch_2.json
@@ -75,5 +75,9 @@
   "site_name": "Add a Pinch",
   "title": "Citrus Scones Recipe with Orange Glaze",
   "total_time": 30,
-  "yields": "8 servings"
+  "yields": "8 servings",
+  "equipment": [
+    "Pastry Blender",
+    "Grater/ Zester"
+  ]
 }
diff --git a/tests/test_data/altonbrown.com/altonbrown_1.json b/tests/test_data/altonbrown.com/altonbrown_1.json
index d744bf39..7384f6e2 100644
--- a/tests/test_data/altonbrown.com/altonbrown_1.json
+++ b/tests/test_data/altonbrown.com/altonbrown_1.json
@@ -49,5 +49,10 @@
   "site_name": "Alton Brown",
   "title": "A Far, Far Better Cake",
   "total_time": 210,
-  "yields": "2 servings"
+  "yields": "2 servings",
+  "equipment": [
+    "Digital kitchen scale",
+    "Digital instant-read thermometer",
+    "2 (9-inch) round aluminum cake pans"
+  ]
 }
diff --git a/tests/test_data/altonbrown.com/altonbrown_2.json b/tests/test_data/altonbrown.com/altonbrown_2.json
index 6660c611..da7f790f 100644
--- a/tests/test_data/altonbrown.com/altonbrown_2.json
+++ b/tests/test_data/altonbrown.com/altonbrown_2.json
@@ -73,5 +73,10 @@
   "site_name": "Alton Brown",
   "title": "Filipino-Style Pork Skewers with Vinegar Sauce",
   "total_time": 280,
-  "yields": "12 servings"
+  "yields": "12 servings",
+  "equipment": [
+    "1-gallon zip top bag",
+    "Charcoal grill (See note)",
+    "12 8-inch wooden skewers, smoked in water (See other note)"
+  ]
 }
diff --git a/tests/test_data/amazingribs.com/amazingribs_1.json b/tests/test_data/amazingribs.com/amazingribs_1.json
index 7fb4264a..244dde55 100644
--- a/tests/test_data/amazingribs.com/amazingribs_1.json
+++ b/tests/test_data/amazingribs.com/amazingribs_1.json
@@ -53,5 +53,9 @@
   "site_name": "Meathead's AmazingRibs.com",
   "title": "Texas Hot Guts Recipe",
   "total_time": 165,
-  "yields": "8 servings"
+  "yields": "8 servings",
+  "equipment": [
+    "sausage grinder",
+    "sausage stuffer"
+  ]
 }
diff --git a/tests/test_data/amazingribs.com/amazingribs_2.json b/tests/test_data/amazingribs.com/amazingribs_2.json
index 7a17ebb4..0fd82e38 100644
--- a/tests/test_data/amazingribs.com/amazingribs_2.json
+++ b/tests/test_data/amazingribs.com/amazingribs_2.json
@@ -47,5 +47,8 @@
   "site_name": "Meathead's AmazingRibs.com",
   "title": "Brisket Burnt Ends Recipe",
   "total_time": 610,
-  "yields": "8 servings"
+  "yields": "8 servings",
+  "equipment": [
+    "4 cups wood chunks or chips (about 8 ounces (226 g))"
+  ]
 }
diff --git a/tests/test_data/baking-sense.com/bakingsense_1.json b/tests/test_data/baking-sense.com/bakingsense_1.json
index 20542b85..c93f4696 100644
--- a/tests/test_data/baking-sense.com/bakingsense_1.json
+++ b/tests/test_data/baking-sense.com/bakingsense_1.json
@@ -80,5 +80,12 @@
   "site_name": "Baking Sense®",
   "title": "Sourdough Bundt Cake with Buttermilk Glaze",
   "total_time": 60,
-  "yields": "16 servings"
+  "yields": "16 servings",
+  "equipment": [
+    "My Book",
+    "KA Stand Mixer",
+    "Bundt Pan",
+    "Cooling Rack",
+    "Half Sheet Pans"
+  ]
 }
diff --git a/tests/test_data/baking-sense.com/bakingsense_2.json b/tests/test_data/baking-sense.com/bakingsense_2.json
index e6f18dbf..31b4c770 100644
--- a/tests/test_data/baking-sense.com/bakingsense_2.json
+++ b/tests/test_data/baking-sense.com/bakingsense_2.json
@@ -76,5 +76,14 @@
   "site_name": "Baking SenseÂ®",
   "title": "Chocolate Cake with Strawberry Mousse Recipe",
   "total_time": 250,
-  "yields": "12 servings"
+  "yields": "12 servings",
+  "equipment": [
+    "My Book",
+    "9″ Cake Pan",
+    "KA Stand Mixer",
+    "Food Processor",
+    "Revolving Cake Stand",
+    "Cake Boards",
+    "Sieve"
+  ]
 }
diff --git a/tests/test_data/bowlofdelicious.com/bowlofdelicious.json b/tests/test_data/bowlofdelicious.com/bowlofdelicious.json
index faf90f13..de9e23cb 100644
--- a/tests/test_data/bowlofdelicious.com/bowlofdelicious.json
+++ b/tests/test_data/bowlofdelicious.com/bowlofdelicious.json
@@ -56,5 +56,8 @@
   "site_name": "Bowl of Delicious",
   "title": "Six-Minute Seared Ahi Tuna Steaks",
   "total_time": 6,
-  "yields": "2 servings"
+  "yields": "2 servings",
+  "equipment": [
+    "Nonstick Skillet"
+  ]
 }
diff --git a/tests/test_data/budgetbytes.com/budgetbytes.json b/tests/test_data/budgetbytes.com/budgetbytes.json
index 03b35e7f..a2c3b6c8 100644
--- a/tests/test_data/budgetbytes.com/budgetbytes.json
+++ b/tests/test_data/budgetbytes.com/budgetbytes.json
@@ -51,5 +51,8 @@
   "site_name": "Budget Bytes",
   "title": "Smash Burgers",
   "total_time": 40,
-  "yields": "4 servings"
+  "yields": "4 servings",
+  "equipment": [
+    "Cast Iron Skillet"
+  ]
 }
diff --git a/tests/test_data/downshiftology.com/downshiftology.json b/tests/test_data/downshiftology.com/downshiftology.json
index 78c55e75..eb4515ee 100644
--- a/tests/test_data/downshiftology.com/downshiftology.json
+++ b/tests/test_data/downshiftology.com/downshiftology.json
@@ -67,5 +67,9 @@
   "site_name": "Downshiftology",
   "title": "Greek Chicken Kabobs",
   "total_time": 55,
-  "yields": "6 servings"
+  "yields": "6 servings",
+  "equipment": [
+    "Metal Skewers",
+    "Lodge Indoor Grill"
+  ]
 }
diff --git a/tests/test_data/elavegan.com/elavegan_2.json b/tests/test_data/elavegan.com/elavegan_2.json
index 53b00d35..1b1b4317 100644
--- a/tests/test_data/elavegan.com/elavegan_2.json
+++ b/tests/test_data/elavegan.com/elavegan_2.json
@@ -89,5 +89,9 @@
   "site_name": "Elavegan",
   "title": "Vegan Coffee Cake",
   "total_time": 55,
-  "yields": "9 servings"
+  "yields": "9 servings",
+  "equipment": [
+    "Kitchen scale",
+    "8x8 pan"
+  ]
 }
diff --git a/tests/test_data/fattoincasadabenedetta.it/fattoincasadabenedetta.json b/tests/test_data/fattoincasadabenedetta.it/fattoincasadabenedetta.json
index 56c05561..01263bb0 100644
--- a/tests/test_data/fattoincasadabenedetta.it/fattoincasadabenedetta.json
+++ b/tests/test_data/fattoincasadabenedetta.it/fattoincasadabenedetta.json
@@ -58,5 +58,10 @@
   "site_name": "Fatto in casa da Benedetta",
   "title": "Torta fredda al limone",
   "total_time": 45.0,
-  "yields": "8 servings"
+  "yields": "8 servings",
+  "equipment": [
+    "Casseruola 16cm con coperchio",
+    "Frusta in silicone 32cm",
+    "Sac à poche"
+  ]
 }
diff --git a/tests/test_data/forktospoon.com/forktospoon.json b/tests/test_data/forktospoon.com/forktospoon.json
index e92f9f66..adb4115a 100644
--- a/tests/test_data/forktospoon.com/forktospoon.json
+++ b/tests/test_data/forktospoon.com/forktospoon.json
@@ -61,5 +61,10 @@
   "site_name": "Fork To Spoon",
   "title": "Air Fryer Parmesan Tomatoes",
   "total_time": 20,
-  "yields": "6 servings"
+  "yields": "6 servings",
+  "equipment": [
+    "Air Fryer",
+    "Cooking Spray",
+    "Parchment Paper"
+  ]
 }
diff --git a/tests/test_data/hersheyland.com/herseyland.json b/tests/test_data/hersheyland.com/herseyland.json
index c6c02a57..242455eb 100644
--- a/tests/test_data/hersheyland.com/herseyland.json
+++ b/tests/test_data/hersheyland.com/herseyland.json
@@ -62,5 +62,13 @@
   "site_name": null,
   "title": "HERSHEY'S \"Perfectly Chocolate\" Chocolate Cake Recipe | Hersheyland",
   "total_time": 45,
-  "yields": "1 cake"
+  "yields": "1 cake",
+  "equipment": [
+    "Round Baking Pans",
+    "Measuring Cups and Spoons",
+    "Mixing Bowl",
+    "Mixer",
+    "Oven",
+    "Wire Rack"
+  ]
 }
diff --git a/tests/test_data/joyfoodsunshine.com/joyfoodsunshine.json b/tests/test_data/joyfoodsunshine.com/joyfoodsunshine.json
index efd70999..df13fd0f 100644
--- a/tests/test_data/joyfoodsunshine.com/joyfoodsunshine.json
+++ b/tests/test_data/joyfoodsunshine.com/joyfoodsunshine.json
@@ -59,5 +59,14 @@
   "site_name": "JoyFoodSunshine",
   "title": "Easy Homemade Pizza Dough",
   "total_time": 30,
-  "yields": "14 servings"
+  "yields": "14 servings",
+  "equipment": [
+    "glass batter bowl",
+    "measuring spoons",
+    "measuring cups",
+    "rolling pin",
+    "pizza stone",
+    "Pizza Peel",
+    "Pizza scissors"
+  ]
 }
diff --git a/tests/test_data/lifestyleofafoodie.com/lifestyleofafoodie_2.json b/tests/test_data/lifestyleofafoodie.com/lifestyleofafoodie_2.json
index 813e09cb..338070da 100644
--- a/tests/test_data/lifestyleofafoodie.com/lifestyleofafoodie_2.json
+++ b/tests/test_data/lifestyleofafoodie.com/lifestyleofafoodie_2.json
@@ -38,5 +38,8 @@
   "site_name": "Lifestyle of a Foodie",
   "title": "Crispy air fryer frozen french fries",
   "total_time": 13,
-  "yields": "4 servings"
+  "yields": "4 servings",
+  "equipment": [
+    "Ninja air fryer"
+  ]
 }
diff --git a/tests/test_data/ministryofcurry.com/ministryofcurry.json b/tests/test_data/ministryofcurry.com/ministryofcurry.json
index 4ed41275..1f732533 100644
--- a/tests/test_data/ministryofcurry.com/ministryofcurry.json
+++ b/tests/test_data/ministryofcurry.com/ministryofcurry.json
@@ -76,5 +76,8 @@
   "site_name": "Ministry of Curry",
   "title": "Slow Cooker EASY Chicken Tikka Masala",
   "total_time": 270,
-  "yields": "6 servings"
+  "yields": "6 servings",
+  "equipment": [
+    "Slow Cooker"
+  ]
 }
diff --git a/tests/test_data/momswithcrockpots.com/momswithcrockpots.json b/tests/test_data/momswithcrockpots.com/momswithcrockpots.json
index 533df140..92e2832a 100644
--- a/tests/test_data/momswithcrockpots.com/momswithcrockpots.json
+++ b/tests/test_data/momswithcrockpots.com/momswithcrockpots.json
@@ -56,5 +56,8 @@
   "site_name": "Moms with Crockpots",
   "title": "Crockpot Macaroni & Cheese",
   "total_time": 225,
-  "yields": "6 servings"
+  "yields": "6 servings",
+  "equipment": [
+    "6 Qt Crockpot"
+  ]
 }
diff --git a/tests/test_data/mykitchen101.com/mykitchen101.testhtml b/tests/test_data/mykitchen101.com/mykitchen101.testhtml
index 541f9bc2..afcb9cb7 100644
--- a/tests/test_data/mykitchen101.com/mykitchen101.testhtml
+++ b/tests/test_data/mykitchen101.com/mykitchen101.testhtml
@@ -839,4 +839,4 @@ var windowOpen;
 <!-- Dynamic page generated in 0.292 seconds. -->
 <!-- Cached page generated by WP-Super-Cache on 2021-07-03 05:24:56 -->
 
-<!-- super cache -->
+<!-- super cache -->
diff --git a/tests/test_data/mykitchen101en.com/mykitchen101en.json b/tests/test_data/mykitchen101en.com/mykitchen101en.json
index 3d18898b..a3b119a8 100644
--- a/tests/test_data/mykitchen101en.com/mykitchen101en.json
+++ b/tests/test_data/mykitchen101en.com/mykitchen101en.json
@@ -64,5 +64,8 @@
   "site_name": "MyKitchen101en.com",
   "title": "Baked Mini Egg Sponge Cakes",
   "total_time": 55,
-  "yields": "30 servings"
+  "yields": "30 servings",
+  "equipment": [
+    "2 mini muffin pans"
+  ]
 }