Repository: hhursev/recipe-scrapers
Pull request #37 (2018-04-25 18:52:41)

Retrieve rating
Get rating  and reviews from epicurious recipe.

hhursev: Hey @Froskekongen,

Sorry for the radio silence on my end on that one! TBH, I’ve completely forgotten about this PR. (Partially because I was not 100% confident how to react on it)

I definitely like the idea of having `reviews` and `ratings` methods added. However, the thing about not accepting this PR is that I want the project to stay consistent. Meaning, if we are to add `reviews` and `ratings` for one site, let’s do it for all of them.

Here things get tricky as not all recipe sites have reviews and ratings options. And here is the part that I was unsure about on how to proceed.

However as time passes, I realize I am inclining towards adding `.ratings` and `.reviews` 👍 

Can you please add `.reviews` and `.ratings` to the `AbstractScraper` (raising NotImplementedError) base class. I will slowly backfill the existing Sites we support and this will go into version `3.0.0` once finished.

Thank you for taking your time and contributing! 🍺 It’s appreciated.
marcolussetti: Once the abstract class is in, I'll start working on adding this to existing sites as well :)
Froskekongen: @hhursev: I added the abstract methods now. Ratings and reviews will be a really interesting addition.

Patch:
diff --git a/recipe_scrapers/_abstract.py b/recipe_scrapers/_abstract.py
index f41159e6..73122ea5 100644
--- a/recipe_scrapers/_abstract.py
+++ b/recipe_scrapers/_abstract.py
@@ -87,6 +87,12 @@ class AbstractScraper():
     def instructions(self):
         raise NotImplementedError("This should be implemented.")
 
+    def ratings(self):
+        raise NotImplementedError("This should be implemented.")
+
+    def reviews(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/epicurious.py b/recipe_scrapers/epicurious.py
index 04135bf0..e4ed4c7c 100644
--- a/recipe_scrapers/epicurious.py
+++ b/recipe_scrapers/epicurious.py
@@ -1,8 +1,10 @@
 from ._abstract import AbstractScraper
 from ._utils import normalize_string
+import re
 
 
 class Epicurious(AbstractScraper):
+    fork_rating_re = re.compile('/(\d)_forks.png')
 
     @classmethod
     def host(self):
@@ -38,3 +40,29 @@ class Epicurious(AbstractScraper):
             normalize_string(instruction.get_text())
             for instruction in instructions
         ])
+
+    def ratings(self):
+        rating = self.soup.find('span', {'class': 'rating'})
+        rating = rating.get_text().split('/')[0] if rating is not None else None
+        rating = float(rating) if rating is not None else None
+        return rating
+
+    def reviews(self):
+        reviews = self.soup.findAll('', {'class': "most-recent"})
+        ratings = [rev.find('img', {'class': "fork-rating"}) for rev in reviews]
+        temp = []
+        for rating in ratings:
+            if 'src' in rating.attrs:
+                txt = rating.attrs['src']
+            else:
+                txt = ''
+            rating = self.fork_rating_re.search(txt)
+            rating = rating.group(1) if rating is not None else '0'
+            rating = int(rating) if rating != '0' else None
+            temp.append(rating)
+        ratings = temp
+        review_texts = [rev.find('div', {'class': "review-text"}) for rev in reviews]
+        reviews = [rev.get_text().strip('/ flag if inappropriate') for rev in review_texts]
+        result = [{'review_text': rt, "rating": rating}
+                  for rt, rating in zip(reviews, ratings)]
+        return result


Test patch:
diff --git a/recipe_scrapers/tests/test_epicurious.py b/recipe_scrapers/tests/test_epicurious.py
index cc828d06..a90c572b 100644
--- a/recipe_scrapers/tests/test_epicurious.py
+++ b/recipe_scrapers/tests/test_epicurious.py
@@ -52,3 +52,14 @@ class TestEpicurious(unittest.TestCase):
             'Cook noodles according to package directions. During the last minute of cooking, add escarole. Drain and rinse under cold water.\nToss noodles, escarole, and chili sauce in a large bowl until coated; season with salt. Divide noodles between bowls. Slice scallions into 1" pieces and place on top of noodles along with some tofu crumbles, radishes, and peanuts.',
             self.harvester_class.instructions()
         )
+
+    def test_ratings(self):
+        return self.assertGreaterEqual(self.harvester_class.ratings(), 0.99)
+
+    def test_reviews(self):
+        print(self.harvester_class.reviews()[0]['review_text'])
+        self.assertCountEqual(
+            "This was yummy, the tofu especially.  The sauce for the tofu really made the dish.  In light of that, I'd like to try this again with rice noodles instead of ramen and chicken instead of tofu.  I didn't really dig the fresh ramen as much as I thought I would (too eggy).  I'm not sure chicken will work quite so well as the tofu but it's worth a shot. lisamichellek from Seattle, WA",
+            self.harvester_class.reviews()[0]['review_text'])
+
+        self.assertEqual(self.harvester_class.reviews()[0]['rating'], 4)