├── .gitignore
├── Program_Architecture.pdf
├── README.md
├── bodhi_alarcon.txt
├── david_wallach.txt
├── junhao_li.txt
├── main.py
└── templates
└── formtest.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pages
2 | *.json
3 | *.pyc
4 |
--------------------------------------------------------------------------------
/Program_Architecture.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dwallach1/RecipeTransformer/42fc6243aa4006b0edc3aaf7a1c83da9912c2669/Program_Architecture.pdf
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RecipeTransformer
2 |
3 | This python module takes in a URL pointing to a recipe from AllRecipes.com. It then promps the user to identify any and all supported
4 | transformations they would like to make to the recipe. These transformations can be any of the following
5 |
6 | * To and from vegetarian
7 | * To and from vegan
8 | * To and from healthy
9 | * To and from pescatarian
10 | * To Style of cuisine (i.e. to Thai, Mexican, Italian, etc.)
11 | * Cooking method (i.e. from bake to stirfry)
12 | * DIY to easy
13 |
14 |
15 | The program then outputs a JSON representation of the new ingredients and changes made to the original recipe to accomplish these transformations.
16 |
17 | For grading purposes, here is what we completed:
18 |
19 |
20 | - [x] Ingredient name
21 | - [x] Quantity
22 | - [x] Measurement (cup, teaspoon, pinch, etc.)
23 | - [x] Descriptor (e.g. fresh, extra-virgin) (OPTIONAL)
24 | - [x] Preparation (e.g. finely chopped) (OPTIONAL)
25 | - [x] Tools – pans, graters, whisks, etc.
26 | - [x] Primary cooking method (e.g. sauté, broil, boil, poach, etc.)
27 | - [x] Other cooking methods used (e.g. chop, grate, stir, shake, mince, crush, squeeze, etc.) (OPTIONAL)
28 | - [x] Steps – parse the directions into a series of steps that each consist of ingredients, tools, methods, and times (OPTIONAL)
29 | - [x] To and from vegetarian (REQUIRED)
30 | - [x] To and from healthy (REQUIRED)
31 | - [x] Style of cuisine (AT LEAST ONE REQUIRED)
32 | - [x] Any input cuisine (OPTIONAL)
33 | - [x] Another Style of cuisine (OPTIONAL)
34 | - [x] to and from Pescatatian (OPTIONAL)
35 | - [x] DIY to easy (OPTIONAL)
36 | - [x] GUI to run the program (OPTIONAL)
37 |
38 |
39 |
40 |
41 | # Usage
42 |
43 | ```python
44 | # set URL variable to point to a AllRecipes.com url
45 | URL = 'http://allrecipes.com/recipe/234667/chef-johns-creamy-mushroom-pasta/?internalSource=rotd&referringId=95&referringContentType=recipe%20hub'
46 |
47 | # parse the URL to get relevant information
48 | recipe_attrs = parse_url(URL) # parse_url returns a dict with data to populate a Recipe object
49 | recipe = Recipe(**recipe_attrs) # instantiate the Recipe object by unpacking dictionary
50 |
51 | # apply a transformation
52 | recipe.to_style('Mexican') # convert the Creamy Mushroom Pasta to be Mexican style
53 | print(recipe.to_JSON()) # print the new recipe
54 | print(recipe.original_recipe.to_JSON) # if you want to access the original recipe
55 |
56 | ```
57 |
58 | If you prefer a GUI interface, we have implemented a locally hosted webpage using web.py. To run it, simply add the --gui flag in the command line:
59 |
60 | `>> python main.py --gui`
61 |
62 | this will then print out a url to input into your webbrowser and from there you will be able to access all the functionality of this program and see the output in a much more friendly enviornment.
63 |
64 | # Classes
65 |
66 | * **Recipe Class** is the main class in which all the transformation methods are. It also holds a list of Ingredient objects and Instruction objects parsed from the input recipe's URL (from allrecipes.com). It also finds the cooking tools and cooking methods used in the recipe by parsing the Instruction objects once they are instatiated and built. The Recipe class gets built by a dictionary object returned from `parse_url(URL)` function which scrapes the URL from allrecipes.com and returns a dictionary with all the necessary information to build the Recipe object.
67 |
68 | The Recipe class gets instatiated with a dictionary with the following schema:
69 |
70 | {
71 |
72 | name: string
73 |
74 | preptime: int
75 |
76 | cooktime: int
77 |
78 | totaltime: int
79 |
80 | ingredients: list of strings
81 |
82 | instructions: list of strings
83 |
84 | calories: int
85 |
86 | carbs: int
87 |
88 | fat: int
89 |
90 | protien: int
91 |
92 | cholesterol: int
93 |
94 | sodium: int
95 |
96 | }
97 |
98 |
99 | and thus you can access information like `sodium` by calling recipe.sodium etc. The ingredients and instruction
100 | lists are instatiated and parsed in the Recipe's `__init__` method.
101 |
102 |
103 | * **Ingredient Class** is used to parse and store the Ingredients in a clean and easily accessible manner. An Instruction object takes in a string (the text of a bullet point from the recipe's url in the ingredients section) and parses out the name, quantity, measurement, descriptor, preperation, and type. It does this using NLTK's part of speech tagger as well as a word bank. The type is a single letter correlated to one of the following:
104 |
105 | * H --> Herbs / Spices
106 |
107 | * V --> Vegetable
108 |
109 | * M --> Meat
110 |
111 | * D --> Dairy
112 |
113 | * F --> Fruit
114 |
115 | * S --> Sauce
116 |
117 | * P --> Pescatarian (Seafood)
118 |
119 | * ? --> Misc.
120 |
121 |
122 |
123 | This is done by building out lists parsed from websites like wikipedia.com and naturalhealthtechniques.com that
124 | have long records for each category. By tagging each ingredient with a type, we are able to infer a lot more about
125 | the ingredient and it is integral to the Recipe's to_style method.
126 |
127 |
128 | * **Instruction Class** is used to parse and store the Instructions in a clean and easily accessible manner. The `__init__` method calls other methods within the class to find the cooking methods and tools used in that instruction as well as the amount of time the instruction takes. It is important to store this in a different object than the Recipe class so that when we change the recipes, we can update the specific instrucitons and details accordingly.
129 |
130 |
131 |
132 | # Methods
133 |
134 | The following are the methods of the Recipe class
135 |
136 | * **to_vegan()** - replaces non-vegan ingredients (meat, dairy, ect.) with vegan substitutes
137 | * **from_vegan()** - replaces vegan ingredients with non-vegan ingredients like meat, dairy, ect.
138 | * **to_vegetarian()** - replaces non-vegetarian ingredients (meat) with vegetarian substitutes such as tofu and such. Updates the instructions and times accordingly
139 | * **from_vegetarian()** - adds a random meat to the recipe and updates the instructions and times
140 | * **to_pescatarian()** - replaces meats with seafood and/or adds new seafood ingredients to the recipe
141 | * **from_pescatarian()** - replaces seafood with meat and/or adds new meat ingredients to the recipe
142 | * **to_style(style, threshold=1.0)** - takes in a parameter of type string `style` (i.e. 'Mexican', 'Thai') and converts the recipe to be more of the input style. The parameter `threshold` allows the user to control how much they want their recipe changed to the desired style. Threshold is a float from 0.0 to 1.0 with 0.0 being no changes and 1.0 being as many changes as possible.
143 | * **to_method(method)** - transforms the cooking method to be like that method. For example, if passed `'fry'` as the method paramter's value, then it will add flour and oil to the recipe if not already there and fry the meats and vegetables.
144 | * **to_easy()** - transforms the recipe from DIY to easy by making the ingredients less intenaive to get and prepare
145 | * **print_pretty()** - used to print the attributes of the recipe in an easy to read format
146 | * **to_JSON()** - used to export the recipe class to a JSON format
147 | * **compare_to_original()** - shows the additions and/or changes reflected in the current recipe from the recipe that the object was instatiated with
148 |
149 |
150 | # Dependencies
151 |
152 | * Used Python2.7 but works with Python3 as well
153 | * BeautifulSoup
154 | * NLTK
155 | * Requests
156 | * web.py (for GUI)
157 |
158 | # Program Architecture
159 |
160 | 
161 |
162 |
--------------------------------------------------------------------------------
/bodhi_alarcon.txt:
--------------------------------------------------------------------------------
1 | Bodhi Alarcon (bta566)
2 | Contributions:
3 | * GUI for the recipe transformer
4 | * additional testing
--------------------------------------------------------------------------------
/david_wallach.txt:
--------------------------------------------------------------------------------
1 | David Wallach (daw647)
2 | What I contributed:
3 | * parsing input URL
4 | * parsing wiki pages to build out lists
5 | * class structure / program architecture
6 | * to/from vegan
7 | * to/from vegetarian
8 | * to/from pescatarian
9 | * to style
10 | * to method
11 |
12 |
--------------------------------------------------------------------------------
/junhao_li.txt:
--------------------------------------------------------------------------------
1 | Junhao Li (jlo708)
2 | What I contributed:
3 | * initial to vegetarian (always replaced meats w/tofu and meat stocks w/vegetable broth)
4 | * initial from vegetarian (always added chicken)
5 | * to easy
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | """
2 | Recipe Transformer Project
3 |
4 | Given a URL to a recipe from AllRecipes.com, this program uses Natural Language Processing to transform any recipe, based on
5 | user's input, into any of the following categories:
6 | * To and from vegetarian
7 | * To and from vegan
8 | * To and from healthy
9 | * To and from pescatarian
10 | * To Style of cuisine (i.e. to Thai)
11 | * Cooking method (from bake to stir fry, for example)
12 |
13 |
14 | Authors:
15 | * David Wallach
16 | * Junhao Li
17 | * Bodhi Alarcon
18 |
19 |
20 | github repository: https://github.com/dwallach1/RecipeTransformer
21 |
22 | """
23 | import time
24 | import random
25 | import re
26 | import json
27 | import sys
28 | import argparse
29 | from urlparse import urlparse
30 | from collections import defaultdict, OrderedDict
31 | from operator import itemgetter
32 | import textwrap
33 | import copy
34 | import requests
35 | from bs4 import BeautifulSoup
36 | from nltk import word_tokenize, pos_tag
37 | import web
38 | from web import form
39 |
40 | DEBUG = False
41 |
42 |
43 | # simple regex used to find specific attributes in strings
44 | measure_regex = '(cup|spoon|fluid|ounce|pinch|gill|pint|quart|gallon|pound|drops|recipe|slices|pods|package|can|head|halves)'
45 | tool_indicator_regex = '(pan|skillet|pot|sheet|grate|whisk|griddle|bowl|oven|dish)'
46 | method_indicator_regex = '(boil|bake|baking|simmer|stir|roast|fry)'
47 | time_indicator_regex = '(min|hour)'
48 |
49 | # these are used as last resort measures to sort out names, descriptors, and preperation words that our system is having trouble parsing
50 | descriptor_regex = '(color|mini|container|skin|bone|halves|fine|parts|leftover|style|frying|breast)'
51 | preperation_regex = '(room|temperature|divided|sliced|dice|mince|chopped|quartered|cored|shredded|seperated|pieces)'
52 | names_regex = '(garlic|poppy|baking|sour|cream|broth|chicken|olive|mushroom|green|vegetable|bell)'
53 |
54 |
55 |
56 | # start of word banks
57 |
58 | healthy_substitutes = {
59 | # The way this dict is structures is so that the values are used to easily instatiate new Ingredient objects
60 | # the quantities are just for parsing, they are updated to be the amount used of the unhealthy version in the recipe
61 | # inside the Recipe to_healthy method
62 | 'oil': '2 tablespoons of Prune Puree',
63 | 'cheese': '3 tablespoons of Nutritional Yeast',
64 | 'pasta': '8 ounces of shredded zucchini',
65 | 'flour': '1 cup of whole-wheat flour',
66 | 'butter': '3 tablespoons of unsweetened applesauce',
67 | 'cream': '3 cups of greek yogurt',
68 | 'eggs': '3 egg whites',
69 | 'milk': '4 ounces of skim milk',
70 | 'potatoes': '4 handfuls of arugula',
71 | 'french fries': '4 handfuls of arugula',
72 | 'yogurt': '2 cups of low-fat cottage cheese'
73 |
74 | }
75 |
76 | dairy_substitutes = {
77 | # The way this dict is structures is so that the values are used to easily instatiate new Ingredient objects
78 | # the quantities are just for parsing, they are updated to be the amount used of the unhealthy version in the recipe
79 | # inside the Recipe to_healthy method
80 | 'butter': '2 tablespoons of olive oil',
81 | 'cheese': '3 tablespoons of yeast flakes',
82 | 'milk': '12 ounces of soy milk',
83 | 'sour cream': '2 avacados',
84 | 'cream': '2 cups of almond milk yogurt',
85 | 'ice cream': '2 cups of sorbet'
86 |
87 | }
88 |
89 | # do not need a dict because we switch based on 'type' attribute instead of 'name' attribute
90 | meat_substitutes = [
91 | '12 ounces of Tofu',
92 | '1 cup ICantBelieveItsNotMeat',
93 | '12 ounces of Tempeh',
94 | '12 ounces of grilled Seitan',
95 | '8 ounces of textured vegetable protien',
96 | '12 ounces of gluten-free vegan meat',
97 | '4 cups of Jackfruit',
98 | '3 large portobello mushrooms',
99 | '4 cups of lentils',
100 | '4 cups of legumes'
101 | ]
102 |
103 | # use Tuples to tag the substitutes
104 | unhealthy_substitutes = [
105 | ('1 pound of fried chicken', 'M'),
106 | ('4 pieces of milanesa', 'M'),
107 | ('3 fried eggplants', 'V'),
108 | ('10 fried pickles', 'V')
109 |
110 |
111 | ]
112 |
113 |
114 | # build list dynamically from wikipedia using the build_dynamic_lists() function -- used to tag the domain of an ingredient
115 | sauce_list = []
116 | vegetable_list = []
117 | herbs_spice_list = []
118 | dairy_list = []
119 | meat_list = []
120 | grain_list = []
121 | fruit_list = []
122 | seafood_list = []
123 |
124 |
125 | class Ingredient(object):
126 | """
127 | Represents an Ingredient in the recipe. Ingredients have assoiciated quantities, names, measurements,
128 | preperation methods (i.e. finley chopped), and descriptors (i.e. fresh, extra-virgin). Uses NLTK to tag each word's part
129 | of speach using the pos_tag fuction. From there the module handles extracting out the relavent information and storing it in the appropiate
130 | attributes of the object.
131 |
132 | For pos_tag:
133 | * ADJ adjective new, good, high, special, big, local
134 | * ADP adposition on, of, at, with, by, into, under
135 | * ADV adverb really, already, still, early, now
136 | * CONJ conjunction and, or, but, if, while, although
137 | * DET determiner, article the, a, some, most, every, no, which
138 | * NOUN noun year, home, costs, time, Africa
139 | * NUM numeral twenty-four, fourth, 1991, 14:24
140 | * PRT particle at, on, out, over per, that, up, with
141 | * PRON pronoun he, their, her, its, my, I, us
142 | * VERB verb
143 | * . punctuation marks . , ; !
144 | * X other ersatz, esprit, dunno, gr8, univeristy
145 | """
146 | def __init__(self, description):
147 | description_tagged = pos_tag(word_tokenize(description))
148 |
149 | # if DEBUG:
150 | # print ('tags: {}'.format(description_tagged))
151 |
152 | self.name = self.find_name(description_tagged)
153 | self.quantity = self.find_quantity(description) # do not use tagged description -- custom parsing for quantities
154 | self.measurement = self.find_measurement(description_tagged, description)
155 | self.descriptor = self.find_descriptor(description_tagged)
156 | self.preperation = self.find_preperation(description_tagged)
157 | self.type = self.find_type()
158 |
159 | if DEBUG:
160 | print ('parsing ingredient: {}'.format(description))
161 | print ('name: {}'.format(self.name))
162 | print ('quantity: {}'.format(self.quantity))
163 | print ('measurement: {}'.format(self.measurement))
164 | print ('descriptor: {}'.format(self.descriptor))
165 | print ('preperation: {}').format(self.preperation)
166 |
167 |
168 | def __str__(self):
169 | """
170 | String representation of an Ingredient instance
171 | """
172 | return self.name
173 |
174 |
175 | def __repr__(self):
176 | """
177 | How a Ingredient object is represented
178 | """
179 | return self.name
180 |
181 |
182 | def __eq__(self, other):
183 | """
184 | Defines what makes two Ingredient instances equal
185 | """
186 | if isinstance(self, other.__class__):
187 | return self.__dict__ == other.__dict__
188 | return False
189 |
190 |
191 | def find_name(self, description_tagged):
192 | """
193 | looks for name of the ingredient from the desciption. Finds the nouns that are not measurements
194 | """
195 | name = [d[0] for d in description_tagged if ((d[1] == 'NN' or d[1] == 'NNS' or d[1] == 'NNP')
196 | and not re.search(measure_regex, d[0], flags=re.I)
197 | and not re.search(descriptor_regex, d[0], flags=re.I)
198 | and not re.search(preperation_regex, d[0], flags=re.I))
199 | or re.search(names_regex, d[0], re.I)]
200 | if len(name) == 0:
201 | return description_tagged[-1][0]
202 | return ' '.join(name)
203 |
204 |
205 | def find_quantity(self, description):
206 | """
207 | looks for amount descriptors in the ingredient description.
208 | if none are apparent, it returns zero. Else it converts fractions to floats and
209 | aggregates measurement (i.e. 1 3/4 --> 1.75)
210 | """
211 | wholes = re.match(r'([0-9])\s', description)
212 | fractions = re.search(r'([0-9]\/[0-9])', description)
213 |
214 | if fractions:
215 | fractions = fractions.groups(0)[0]
216 | num = float(fractions[0])
217 | denom = float(fractions[-1])
218 | fractions = num / denom
219 |
220 | if wholes: wholes = int(wholes.groups(0)[0])
221 |
222 | total = float(wholes or 0.0) + float(fractions or 0.0)
223 |
224 | return total
225 |
226 |
227 | def find_measurement(self, description_tagged, description):
228 | """
229 | looks for measurements such as cups, teaspoons, etc.
230 | Uses measure_regex which is a compilation of possible measurements.
231 | """
232 | measurement = [d[0] for d in description_tagged if re.search(measure_regex, d[0], flags=re.I)]
233 | m = ' '.join(measurement)
234 | if re.search('package', m, flags=re.I):
235 | extra = description[description.find("(")+1:description.find(")")]
236 | return extra + ' package(s)'
237 | if re.search('can', m, flags=re.I):
238 | extra = description[description.find("(")+1:description.find(")")]
239 | return extra + ' can(s)'
240 |
241 | if description.find("(") > -1 and any(char.isdigit() for char in description):
242 | return description[description.find("(")+1:description.find(")")]
243 |
244 | return m
245 |
246 |
247 | def find_descriptor(self, description_tagged):
248 | """
249 | looks for descriptions such as fresh, extra-virgin by finding describing words such as
250 | adjectives
251 | """
252 | descriptors = [d[0] for d in description_tagged if (
253 | (d[1] == 'JJ' or d[1] == 'RB')
254 | and not re.search(measure_regex, d[0], flags=re.I)
255 | and not re.search(names_regex, d[0], flags=re.I)
256 | )
257 | or re.search(descriptor_regex, d[0], flags=re.I)]
258 | return descriptors
259 |
260 |
261 | def find_preperation(self, description_tagged):
262 | """
263 | find all preperations (finely, chopped) by finding action words such as verbs
264 | """
265 | preperations = [d[0] for d in description_tagged if (
266 | d[1] == 'VB' or d[1] == 'VBD'
267 | or re.search(preperation_regex, d[0], flags=re.I)
268 | )
269 | and not re.search(names_regex, d[0], flags=re.I)]
270 | for i, p in enumerate(preperations):
271 | if p == 'taste':
272 | preperations[i] = 'to taste'
273 | return preperations
274 |
275 |
276 | def find_type(self):
277 | """
278 | attempts to categorize ingredient for Recipe methods to work more smoothly and correctly
279 |
280 | * H --> Herbs / Spices
281 | * V --> Vegetable
282 | * M --> Meat
283 | * D --> Dairy
284 | * F --> Fruit
285 | * S --> Sauce
286 | * P --> Pescatarian (Seafood)
287 | * ? --> Misc.
288 |
289 | ordered by precedence of how telling the classification is --> bound to one classification
290 | """
291 |
292 | # special cases:
293 | if self.name.lower().find('sauce') >= 0: return 'S'
294 |
295 | # normal execution:
296 | types = ''
297 | if any(set(self.name.lower().split(' ')).intersection(set(example.lower().split(' '))) for example in meat_list) and len(types) == 0: types ='M'
298 | if any(set(self.name.lower().split(' ')).intersection(set(example.lower().split(' '))) for example in vegetable_list) and len(types) == 0: types = 'V'
299 | if any(set(self.name.lower().split(' ')).intersection(set(example.lower().split(' '))) for example in dairy_list) and len(types) == 0: types = 'D'
300 | if any(set(self.name.lower().split(' ')).intersection(set(example.lower().split(' '))) for example in grain_list) and len(types) == 0: types = 'G'
301 | if any(set(self.name.lower().split(' ')).intersection(set(example.lower().split(' '))) for example in sauce_list) and len(types) == 0: types = 'S'
302 | if any(set(self.name.lower().split(' ')).intersection(set(example.lower().split(' '))) for example in seafood_list) and len(types) == 0: types = 'P'
303 | if any(set(self.name.lower().split(' ')).intersection(set(example.lower().split(' '))) for example in herbs_spice_list) and len(types) == 0: types = 'H'
304 | if any(set(self.name.lower().split(' ')).intersection(set(example.lower().split(' '))) for example in fruit_list) and len(types) == 0: types = 'F'
305 | if len(types) == 0: types = '?'
306 | return types
307 |
308 |
309 | class Instruction(object):
310 | """
311 | Represents an instruction to produce the Recipe. Each instruction has a set of tools used for cooking and set of
312 | methods also used for cooking. There is a time field to denote the amount of time the instruction takes to complete.
313 | """
314 | def __init__(self, instruction):
315 | self.instruction = instruction
316 | self.instruction_words = word_tokenize(self.instruction)
317 | self.cooking_tools = self.find_tools(self.instruction_words)
318 | self.cooking_methods = self.find_methods(self.instruction_words)
319 | self.time = self.find_time(self.instruction_words)
320 | self.ingredients = None
321 |
322 |
323 | def find_tools(self, instruction_words):
324 | """
325 | looks for any and all cooking tools apparent in the instruction text by using the tool_indicator_regex
326 | variable
327 | """
328 | cooking_tools = []
329 | for word in instruction_words:
330 | if re.search(tool_indicator_regex, word, flags=re.I):
331 | cooking_tools.append(word)
332 | wordset = set(cooking_tools)
333 | return [item for item in wordset if item.istitle() or item.title() not in wordset]
334 |
335 |
336 | def find_methods(self, instruction_words):
337 | """
338 | looks for any and all cooking methods apparent in the instruction text by using the method_indicator_regex
339 | variable
340 | """
341 | cooking_methods = []
342 | for word in instruction_words:
343 | if re.search(method_indicator_regex, word, flags=re.I):
344 | cooking_methods.append(word)
345 | if re.search('preheat', word, re.I):
346 | cooking_methods.append('bake')
347 |
348 | wordset = set(cooking_methods)
349 | return [item for item in wordset if item.istitle() or item.title() not in wordset]
350 |
351 |
352 | def find_time(self, instruction):
353 | """
354 | looks for all time notations apparent in the instruction text by using the time_indicator_regex
355 | variable and aggregating the total times using typecasting
356 | """
357 | time = 0
358 | for i, word in enumerate(instruction):
359 | # if we are talking about degrees, then extracting numbers are not associated with time
360 | if word == 'degrees': return 0
361 |
362 | if re.search(time_indicator_regex, word, flags=re.I):
363 | try:
364 | if re.search('(hour)', word, flags=re.I):
365 | time += int(instruction[i-1]) * 60
366 | else:
367 | time += int(instruction[i-1])
368 | except:
369 | pass
370 | return time
371 |
372 |
373 | def update_instruction(self):
374 | """
375 | Uses the instances word list to update the objects instruction attribute
376 | """
377 | self.instruction = ' '.join(self.instruction_words)
378 |
379 |
380 | class Recipe(object):
381 | """
382 | Used to represent a recipe. Data for each recipe can be found
383 | on AllRecipes.com.
384 | """
385 | def __init__(self, **kwargs):
386 | for key, value in kwargs.items():
387 | setattr(self, key, value)
388 |
389 | self.text_instructions = self.instructions; # store the original instructions,
390 | # idea for right now is to modify the original instructions for transformations
391 | self.ingredients = [Ingredient(ing) for ing in self.ingredients] # store ingredients in Ingredient objects
392 | self.instructions = [Instruction(inst) for inst in self.instructions] # store instructions in Instruction objects
393 | self.cooking_tools, self.cooking_methods = self.parse_instructions() # get aggregate tools and methods apparent in all instructions
394 | self.update_instructions() # as part of the steps requirement, add the associated ingredients to each instruction step
395 |
396 |
397 | self.instructions = [i for i in self.instructions if len(i.instruction)]
398 | # save original copy to compare with the transformations
399 | self.original_recipe = copy.deepcopy(self)
400 |
401 |
402 | def parse_instructions(self):
403 | """
404 | Gathers aggregate data from all instructions to provide overall cooking tools and methods instead of
405 | per instruction basis
406 | """
407 | cooking_tools = []
408 | cooking_methods = []
409 | for inst in self.instructions:
410 | cooking_tools.extend(inst.cooking_tools)
411 | cooking_methods.extend(inst.cooking_methods)
412 | return list(set(cooking_tools)), list(set(cooking_methods))
413 |
414 |
415 | def update_instructions(self):
416 | """
417 | To convert instructions into steps, we need to store the associated ingredients as part an attribute for
418 | each instruction. This method does that update
419 | """
420 | for i, instruction in enumerate(self.instructions):
421 | ingredients = [ingredient.name for ingredient in self.ingredients if instruction.instruction.find(ingredient.name) >= 0]
422 | self.instructions[i].ingredients = list(set(ingredients))
423 |
424 |
425 | def to_JSON(self, original=False):
426 | """
427 | convert representation to easily parseable JSON format
428 | """
429 | data = OrderedDict()
430 | if original: self = self.original_recipe
431 | data['name'] = self.name
432 | data['url'] = self.url
433 | data['cooking tools'] = self.cooking_tools
434 | data['cooking method'] = self.cooking_methods
435 | ing_list = []
436 | for ingredient in self.ingredients:
437 | ing_attrs = {}
438 | for attr, value in ingredient.__dict__.iteritems():
439 | ing_attrs[attr] = value
440 | ing_list.append(ing_attrs)
441 |
442 | data['ingredients'] = ing_list
443 |
444 | inst_list = []
445 | for instruction in self.instructions:
446 | inst_attrs = {}
447 | include = True
448 | for attr, value in instruction.__dict__.iteritems():
449 | if attr == 'instruction_words': continue
450 | if attr == 'instruction' and len(value) == 0: include = False
451 | inst_attrs[attr] = value
452 | if include: inst_list.append(inst_attrs)
453 |
454 | data['steps'] = inst_list
455 | parsed = json.dumps(data, indent=4)
456 | return parsed
457 |
458 |
459 | def print_pretty(self):
460 | """
461 | print a human friendly version of the recipe
462 | """
463 | s = ""
464 | s += '\nIngredients List:'
465 | for ing in self.ingredients:
466 | # only add quantity, measurement, descriptor, and preperation if we have them
467 | quant = ''
468 | if ing.quantity != 0:
469 | quant = "{} ".format(round(ing.quantity, 2) if ing.quantity % 1 else int(ing.quantity))
470 |
471 | measure = ''
472 | if ing.measurement != "":
473 | measure = ing.measurement + ' '
474 |
475 | descr = ''
476 | if len(ing.descriptor) > 0:
477 | descr = ' '.join(ing.descriptor) + ' '
478 |
479 | prep = ''
480 | if len(ing.preperation) > 0:
481 | prep = ', ' + ' and '.join(ing.preperation)
482 |
483 | full_ing = '{}{}{}{}{}'.format(quant, measure, descr, ing.name, prep)
484 |
485 | s += full_ing
486 |
487 | s += '\nInstructions:'
488 | for i, t_inst in enumerate(self.text_instructions[:-1]):
489 | s += textwrap.fill('{}. {}'.format(i+1, t_inst), 80)
490 | return s
491 |
492 |
493 | def compare_to_original(self):
494 | """
495 | Compares the current recipe to the original recipe the object was instatiated with.
496 | If no changes were made, then they will be identical.
497 | """
498 | s = ""
499 | try:
500 | s += '\n-----------------------'
501 | s += '\nThe following changes were made to the original recipe: '
502 | if len(self.original_recipe.ingredients) < len(self.ingredients):
503 | for i in range(len(self.original_recipe.ingredients), len(self.ingredients)):
504 | s += '\n* added {}'.format(self.ingredients[i].name)
505 | else:
506 | for i in range(len(self.original_recipe.ingredients)):
507 | if self.original_recipe.ingredients[i].name != self.ingredients[i].name:
508 | s += '\n* {} ---> {}'.format(self.original_recipe.ingredients[i].name, self.ingredients[i].name)
509 | if len(self.original_recipe.instructions) < len(self.instructions):
510 | for i in range(len(self.original_recipe.instructions), len(self.instructions)):
511 | s += '\n* added {}'.format(self.instructions[i].instruction)
512 | else:
513 | for i in range(len(self.original_recipe.instructions)):
514 | if self.original_recipe.instructions[i].instruction != self.instructions[i].instruction:
515 | s += '\n* {}\n ---> {}'.format(self.original_recipe.instructions[i].instruction, self.instructions[i].instruction)
516 | s += '\n-----------------------'
517 | except:
518 | s += '\n-----------------------'
519 | return s
520 |
521 |
522 | def to_healthy(self):
523 | """
524 | Transforms the recipe to a more healthy version by removing and/or replacing unhealthy ingredients
525 | """
526 | for i, ingredient in enumerate(self.ingredients):
527 | if any(name in ingredient.name.split(' ') for name in healthy_substitutes.keys()):
528 | key = next(name for name in ingredient.name.split(' ') if name in healthy_substitutes.keys())
529 | healthy_sub = Ingredient(healthy_substitutes[key])
530 | healthy_sub.quantity = ingredient.quantity
531 | self.swap_ingredients(self.ingredients[i], healthy_sub)
532 |
533 | self.name = self.name + ' (healthy)'
534 |
535 |
536 | def from_healthy(self):
537 | """
538 | Transforms the recipe to a less healthy (more delicous) version by adding unhealthy ingredients and/or replacing
539 | healthy ingredients with unhealthy ingredients from the global unhealthy_substitutes list
540 | """
541 | for i, ingredient in enumerate(self.ingredients):
542 | if ingredient.type == 'V':
543 | candidates = filter(lambda sub: sub[1] == 'V', unhealthy_substitutes)
544 | new_ingredient = Ingredient(random.choice(candidates)[0])
545 | self.swap_ingredients(ingredient, new_ingredient)
546 | if ingredient.type == 'M':
547 | candidates = filter(lambda sub: sub[1] == 'M', unhealthy_substitutes)
548 | new_ingredient = Ingredient(random.choice(candidates)[0])
549 | self.swap_ingredients(ingredient, new_ingredient)
550 |
551 |
552 | self.name = self.name + ' (unhealthy)'
553 |
554 |
555 | def to_vegan(self):
556 | """
557 | Transforms the recipe to be vegan by removing and/or subsituting all ingredients that are not vegan
558 | """
559 | # start by making vegetarian
560 | self.to_vegetarian()
561 |
562 | # add a random dairy from the dairy_substitutes dictionary
563 | for i, ingredient in enumerate(self.ingredients):
564 | if ingredient.type == 'D':
565 | idx = random.randint(0, len(dairy_substitutes.keys()) - 1)
566 | dairy_sub = Ingredient(dairy_substitutes[dairy_substitutes.keys()[idx]])
567 | dairy_sub.quantity = ingredient.quantity
568 | self.swap_ingredients(self.ingredients[i], dairy_sub)
569 |
570 | # update the name of the recipe
571 | self.name = self.name.replace(' (vegetarian)', '') + ' (vegan)'
572 |
573 |
574 | def from_vegan(self):
575 | """
576 | Transforms the recipe to be non-vegan by adding ingredients that are not vegan
577 | """
578 | # start by adding random meat
579 | self.from_vegetarian()
580 |
581 | # find random dairy
582 | dairy = random.choice(dairy_list)
583 |
584 | # add it to the ingredients list
585 | self.ingredients.append(Ingredient('3 cups of {}'.format(dairy)))
586 |
587 | # create and add new instructions for making and inserting the dairy
588 |
589 | # update the name of the recipe
590 | self.name = self.name + ' (non-vegan)'
591 |
592 |
593 | def to_vegetarian(self):
594 | """
595 | Replaces meat or seafood ingredients with vegetarian alternatives. Uses a random integer generator to randomly choose
596 | which substitute from the meat_substitutes list to use.
597 | """
598 |
599 | for i, ingredient in enumerate(self.ingredients):
600 | if ingredient.type == 'M':
601 | meat_sub = Ingredient(random.choice(meat_substitutes))
602 | meat_sub.quantity = ingredient.quantity
603 | self.swap_ingredients(self.ingredients[i], meat_sub)
604 |
605 | self.name = self.name + ' (vegetarian)'
606 |
607 |
608 | def from_vegetarian(self):
609 | """
610 | Adds a random meat from the gloabl meat_list to the recipe, updates instructions and times
611 | accordingly
612 | """
613 |
614 | # find a random meat from the meat_list to add
615 | meat = random.choice(meat_list).encode('utf-8')
616 | self.ingredients.append(Ingredient('3 cups of boiled {}'.format(meat)))
617 |
618 |
619 | # update/add/build the necessary instructions
620 | boiling_meat = 'Place the {0} in a non-stick pan and fill the pan with water until the {0} are covered.'.format(meat) \
621 | + ' Simmer uncovered for 5 minutes.' \
622 | + ' Then, turn off the heat and cover for 15 minutes. Remove the {0} and set aside.'.format(meat)
623 | adding_meat = 'Shred the {0} by pulling the meat apart into thin slices by hand. Stir in the shredded {0}.'.format(meat)
624 |
625 | # Instatiate objects
626 | boiling_meat_instruction = Instruction(boiling_meat)
627 | adding_meat_instruction = Instruction(adding_meat)
628 |
629 |
630 | # add the instructions to the recipe
631 | self.instructions.insert(0, boiling_meat_instruction)
632 | self.instructions.insert(-1, adding_meat_instruction)
633 |
634 |
635 | def to_pescatarian(self):
636 | """
637 | Replaces meat with seafood ingredients. Uses a random integer generator to randomly choose
638 | which substitute from the seafood_list to use. If no meat, then augment the recipe with a random
639 | seafood
640 | """
641 | swapped = False
642 | for i, ingredient in enumerate(self.ingredients):
643 | if ingredient.type == 'M':
644 | seafood_sub = Ingredient('3 cups of {}'.format(random.choice(seafood_list)))
645 | seafood_sub.quantity = ingredient.quantity
646 | self.swap_ingredients(self.ingredients[i], seafood_sub)
647 | swapped = True
648 |
649 | if not swapped:
650 | # augment the recipe instead of swapping because no meats in the recipe
651 | seafood_ing = Ingredient('3 cups of {}'.format(random.choice(seafood_list)))
652 | self.ingredients.append(seafood_ing)
653 |
654 | grill_seafood = 'Place the {} in a non-stick pan and fill the pan with oil.'.format(seafood_ing.name) \
655 | + ' Grill both sides until charred, takes about 7 minutes.' \
656 | + ' Then, turn off the heat and cover for 15 minutes.'
657 | add_seafood = 'flip the {} onto the plate over the other ingredients.'.format(seafood_ing.name)
658 |
659 | # Instatiate objects
660 | grill_seafood_instruction = Instruction(grill_seafood)
661 | add_seafood_instruction = Instruction(add_seafood)
662 |
663 |
664 | # add the instructions to the recipe
665 | self.instructions.insert(0, grill_seafood_instruction)
666 | self.instructions.insert(-1, add_seafood_instruction)
667 |
668 | self.name = self.name + ' (pescatarian)'
669 |
670 |
671 | def from_pescatarian(self):
672 | """
673 | Replaces seafood with meat and vegetable ingredients. Uses a random integer generator to randomly choose
674 | which substitute from the meat_list to use. If no seafood, then augment the recipe with a random
675 | meat/dairy/grain
676 | """
677 | for i, ingredient in enumerate(self.ingredients):
678 | if ingredient.type == 'P':
679 | meat_sub = Ingredient(random.choice(meat_substitutes))
680 | meat_sub.quantity = ingredient.quantity
681 | self.swap_ingredients(self.ingredients[i], meat_sub)
682 |
683 | self.name = self.name + ' (non-pescatarian)'
684 |
685 |
686 | def to_style(self, style, threshold=1.0):
687 | """
688 | search all recipes for recipes pertaining to the 'style' parameter and builds frequency dictionary.
689 | Then adds/removes/augemnets ingredients to make it more like the 'style' of cuisine.
690 | """
691 |
692 | url = 'https://www.allrecipes.com/search/results/?wt={}&sort=re'.format(style)
693 |
694 | # retrieve data from url
695 | result = requests.get(url, timeout=10)
696 | c = result.content
697 |
698 | # store in BeautifulSoup object to parse HTML DOM
699 | soup = BeautifulSoup(c, "lxml")
700 |
701 | # find all urls that point to recipe pages
702 | style_recipes = [urlparse(url['href']) for url in soup.find_all('a', href=True)] # find all urls in HTML DOM
703 | style_recipes = [r.geturl() for r in style_recipes if r.path[1:8] == 'recipe/'] # filter out noise urls
704 | style_recipes = list(set(style_recipes)) # don't double count urls
705 |
706 | # parse the urls and create new Recipe objects
707 | style_recipes = [Recipe(**parse_url(recipe)) for recipe in style_recipes] # instantiate all recipe objects for each found recipe
708 | # print ('found {} recipes cooked {} style'.format(len(style_recipes), style))
709 |
710 | # unpack all ingredients in total set of new recipes of type 'style'
711 | ingredients_ = [recipe.ingredients for recipe in style_recipes]
712 | ingredients = []
713 | for ingredient in ingredients_:
714 | ingredients.extend(ingredient)
715 |
716 | # hold reference to just the ingredient names for frequency distrobutions
717 | ingredient_names = [ingredient.name for ingredient in ingredients]
718 |
719 | # hold reference to ingredients from original recipe
720 | current_ingredient_names = [ingredient.name for ingredient in self.ingredients]
721 | # print ('current ingredients from original recipe are {}'.format(current_ingredient_names))
722 |
723 | # extract only the names and not the freqs -- will be sorted in decreasing order
724 | key_new_ingredients = [freq[0] for freq in self.freq_dist(ingredient_names)]
725 | # remove the ingredients that are already in there
726 | key_new_ingredients = [ingredient for ingredient in key_new_ingredients if not(ingredient in current_ingredient_names)][:10]
727 | # print ('key ingredients from {} recipes found are {}'.format(style, key_new_ingredients))
728 |
729 |
730 | # get the whole ingredient objects -- this is to change actions accorgingly
731 | # e.g. if we switch from pinches of salt to lemon, we need to change pinches to squeezes
732 | ingredient_changes = [ingredient for ingredient in ingredients if (ingredient.name in key_new_ingredients) and not(ingredient.name in current_ingredient_names)]
733 |
734 | # clear up some memory
735 | del ingredients_
736 | del ingredients
737 | del ingredient_names
738 | del style_recipes
739 | del soup
740 |
741 |
742 | tmp = []
743 | new = []
744 | for ingredient in ingredient_changes:
745 | if ingredient.name in tmp: continue
746 | tmp.append(ingredient.name)
747 | new.append(ingredient)
748 |
749 | ingredient_changes = copy.deepcopy(new)
750 |
751 | # no longer needed --> temporary use
752 | del new
753 | del tmp
754 |
755 |
756 | # Find out most common ingredients from all recipes of type 'style' -- then decide which to switch and/or add to current recipe
757 | try: most_common_sauce = next(ingredient for ingredient in ingredient_changes if ingredient.type == 'S')
758 | except StopIteration: most_comon_sauce = None
759 | try: most_common_meat = next(ingredient for ingredient in ingredient_changes if ingredient.type == 'M')
760 | except StopIteration: most_common_meat = None
761 | try: most_common_vegetable = next(ingredient for ingredient in ingredient_changes if ingredient.type == 'V')
762 | except StopIteration: most_common_vegetable = None
763 | try: most_common_grain = next(ingredient for ingredient in ingredient_changes if ingredient.type == 'G')
764 | except StopIteration: most_common_grain = None
765 | try: most_common_dairy = next(ingredient for ingredient in ingredient_changes if ingredient.type == 'D')
766 | except StopIteration: most_common_dairy = None
767 | try: most_common_herb = next(ingredient for ingredient in ingredient_changes if ingredient.type == 'H')
768 | except StopIteration: most_common_herb = None
769 | try: most_common_fruit = next(ingredient for ingredient in ingredient_changes if ingredient.type == 'F')
770 | except StopIteration: most_common_fruit = None
771 |
772 |
773 | # switch the ingredients
774 | most_commons = filter(lambda mc: mc != None,
775 | [most_common_meat, most_common_vegetable, most_common_sauce, most_common_grain,
776 | most_common_herb, most_common_dairy, most_common_fruit])
777 |
778 | try: most_commons = most_commons[:int(7*threshold)]
779 | except: pass # this means we didnt find enough to choose -- just keep whole list b/c under threshold anyways
780 |
781 | # print ('most commons {}'.format([m.name for m in most_commons]))
782 |
783 |
784 | for new_ingredient in most_commons:
785 | try: current_ingredient = next(ingredient for ingredient in self.ingredients if ingredient.type == new_ingredient.type)
786 | except StopIteration: continue
787 | self.swap_ingredients(current_ingredient, new_ingredient)
788 |
789 | # update name
790 | self.name = self.name + ' (' + style + ')'
791 |
792 |
793 | def to_method(self, method):
794 | """
795 | Transforms the recipe into using a method. The supported methods are
796 |
797 | * fry (i.e. fried chicken)
798 | * stirfry
799 | * bake
800 |
801 | If the method parameter is passed an unsupported value, an error message will be displayed and no
802 | transformation will be made to the recipe object.
803 | """
804 | supported_methods = ['fry', 'stir-fry', 'bake']
805 | if not method in supported_methods:
806 | print ('Error in to_method call. {} method is not yet supported.\n \
807 | please look at documentation for supported methods'.format(method))
808 | return
809 |
810 |
811 | replacements = {
812 | 'to_fry': [('preheated', ''), ('preheat', ''), ('oven', ''), ('degree', ''), ('baking sheet', 'skillet'), ('bake', 'fry'), ('baked', 'fried')],
813 | 'to_bake': [('skillet', 'baking sheet'), ('fry', 'place in oven'), ('drain', 'dry'), ('paper towel', ''), ('dry', 'remove from oven'), ('pot', 'baking sheet'), ('boil', 'crisp'), ('water', 'tin foil')],
814 | 'to_stir-fry': [('preheated', ''), ('preheat', ''), ('oven', ''), ('degree', ''), ('baking sheet', 'skillet'), ('bake', 'cook'), ('baked', 'cooked'), ('fry', 'cook until crisp'), ('drain', 'pour over rice and vegetables'), ('paper towel', '')]
815 | }
816 |
817 |
818 | #
819 | #
820 | # Fry
821 | #
822 | #
823 |
824 | if method == 'fry':
825 | # vegetable and meat booleans
826 | V, M = False, False
827 |
828 | # add flour if there is no flour in the recipe already
829 | flour = [ingredient for ingredient in self.ingredients if 'flour' in ingredient.name.lower().split(' ')]
830 | if not len(flour):
831 | self.ingredients.append(Ingredient('1 1/2 cups of flour'))
832 |
833 |
834 | # add oil if there is no oil in the recipe already
835 | oil = [ingredient for ingredient in self.ingredients if 'oil' in ingredient.name.lower().split(' ')]
836 | if not len(flour):
837 | self.ingredients.append(Ingredient('2 quarts of vegetable oil'))
838 |
839 |
840 | # find if there are vegetables
841 | vegetables = [ingredient for ingredient in self.ingredients if ingredient.type == 'V']
842 |
843 | # find if there are meats
844 | meats = [ingredient for ingredient in self.ingredients if ingredient.type == 'M']
845 |
846 | if not len(meats):
847 | if not len(vegetables):
848 | # add meat if there is no meat or vegetables in the recipe
849 | meat = Ingredient('10 ounces of {}'.format(random.choice(meat_list)))
850 | self.ingredients.append(meat)
851 | M = True
852 | else:
853 | V = True
854 | else:
855 | M = True
856 | V = bool(len(vegetables))
857 | meat = meats[0]
858 |
859 |
860 | if re.search('Preheat oven to', self.instructions[0].instruction):
861 | self.instructions = self.instructions[1:]
862 |
863 | # remove / update all instructions that correlated to previous cooking methods
864 | other_method_regex = '(' + '|'.join(w[0] for w in replacements['to_fry']) + ')'
865 | for i, instruction in enumerate(self.instructions):
866 | inst = ' ' + instruction.instruction.lower()
867 | if re.search(other_method_regex, inst, flags=re.I):
868 | other_words = replacements['to_fry']
869 | for word in other_words:
870 | if inst.find(word[0]):
871 | inst = inst.replace(word[0], word[1])
872 | self.instructions[i] = Instruction(inst.strip()) # update instruction object in memory --> make permanent
873 |
874 |
875 |
876 | # add necessary instructions
877 | instruction_meat = 'In a large skillet, heat oil over medium heat. Salt and pepper {0} to taste, then roll in flour to coat. \
878 | Place {0} in skillet and fry on medium heat until one side is golden brown, \
879 | then turn and brown other side until {0} is no longer pink inside and its juices run clear.'.format(meat.name)
880 |
881 | # if len(vegetables):
882 | # instruction_vegetables = 'In a large skillet, heat oil over medium heat. Salt and pepper {0} to taste, then roll in flour to coat. \
883 | # Place {0} in skillet and fry on medium heat until crispy.'.format(' and '.join([v.name for v in vegetables]))
884 |
885 | # # add vegetable instruction if vegetables are in recipe
886 | # self.instructions.insert(-1, Instruction(instruction_vegetables.strip()))
887 |
888 | # frying adds meat to the recipe regardless,
889 | self.instructions.insert(-1, Instruction(instruction_meat.strip()))
890 |
891 | # update cooking tools and methods
892 | self.cooking_tools = ['skillet']
893 | self.cooking_methods = ['fry']
894 |
895 | if not re.search('serve', self.instructions[-1].instruction):
896 | self.instructions.append(Instruction('Remove {} from the skillet. Dry on paper towels and serve!'.format(meat.name)))
897 |
898 |
899 |
900 | #
901 | #
902 | # Stir-fry
903 | #
904 | #
905 |
906 | elif method == 'stir-fry':
907 |
908 | # make sure there are vegetables
909 | vegetables = [ingredient for ingredient in self.ingredients if ingredient.type == 'V']
910 |
911 | if len(vegetables) < 5:
912 | for _ in range(5 - len(vegetables)):
913 | try: self.ingredients.append(Ingredient('{} cups of {}'.format(random.randint(1,4), random.choice(vegetable_list))))
914 | except: pass
915 | # update after the additions
916 | vegetables = [ingredient for ingredient in self.ingredients if ingredient.type == 'V']
917 |
918 | # add sesame oil and soy sauce to stirfry the vegetables
919 | self.ingredients.append(Ingredient('1 tablespoon of sesame oil'))
920 | self.ingredients.append(Ingredient('2 tablespoons of soy sauce'))
921 |
922 | # add rice
923 | self.ingredients.append(Ingredient('1 1/2 cups of uncooked rice'))
924 |
925 | if re.search('Preheat oven to', self.instructions[0].instruction):
926 | self.instructions = self.instructions[1:]
927 |
928 | # remove / update all instructions that correlated to previous cooking methods
929 | other_method_regex = '(' + '|'.join(w[0] for w in replacements['to_stir-fry']) + ')'
930 | for i, instruction in enumerate(self.instructions):
931 | inst = ' ' + instruction.instruction.lower()
932 | if re.search(other_method_regex, inst, flags=re.I):
933 | other_words = replacements['to_stir-fry']
934 | for word in other_words:
935 | if inst.find(word[0]):
936 | inst = inst.replace(word[0], word[1])
937 | self.instructions[i] = Instruction(inst.strip()) # update instruction object in memory --> make permanent
938 |
939 |
940 | instruction_rice = 'Heat 4 quarts of water to a boil and then place the rice in and let it cook for 8 minutes'
941 |
942 | instruction_vegetables = 'Heat 1 tablespoon sesame oil in a large skillet over medium-high heat. Cook and \
943 | stir {} until just tender, about 5 minutes. \
944 | Remove vegetables from skillet and keep warm.'.format(' and '.join([v.name for v in vegetables]))
945 |
946 | # update cooking method and tools
947 | self.cooking_methods = ['stir-fry']
948 | self.cooking_tools = ['skillet']
949 |
950 | # update instructions
951 | self.instructions.insert(-1, Instruction(instruction_vegetables.strip()))
952 | self.instructions.insert(-1, Instruction(instruction_rice.strip()))
953 |
954 | #
955 | #
956 | # Baking
957 | #
958 | #
959 |
960 | # otherwise it must be 'bake' due to process of elimination
961 | else:
962 |
963 | begin_instruction = Instruction('Preheat oven to 350 degrees F (175 degrees C). Grease a 9x13-inch baking dish.')
964 | bake_instruction = Instruction('Bake in the preheated oven until the ensemble is crisp, about 30 minutes. Remove from the oven and drizzle with sauce.')
965 |
966 | # update cooking methods and tools
967 | self.cooking_methods = ['Bake']
968 | current_tools = self.cooking_tools
969 | self.cooking_tools = ['pan', 'oven', 'dish', 'bowl']
970 |
971 | bake_instruction_idx = 1
972 |
973 | for instruction in self.instructions:
974 | words = instruction.instruction_words
975 | for i, w in enumerate(words):
976 |
977 | # do all stir-fry recipes use skillets?
978 | if re.search('skillet', w, flags=re.I):
979 | instruction.instruction_words[i] = 'pan'
980 | bake_instruction_idx = i
981 |
982 |
983 |
984 | # remove / update all instructions that correlated to previous cooking methods
985 | other_method_regex = '(' + '|'.join(w[0] for w in replacements['to_bake']) + ')'
986 | for i, instruction in enumerate(self.instructions):
987 | inst = ' ' + instruction.instruction.lower()
988 | if re.search(other_method_regex, inst, flags=re.I):
989 | other_words = replacements['to_bake']
990 | for word in other_words:
991 | if inst.find(word[0]):
992 | inst = inst.replace(word[0], word[1])
993 | self.instructions[i] = Instruction(inst.strip()) # update instruction object in memory --> make permanent
994 |
995 |
996 |
997 | # update proper instructions
998 | self.instructions.insert(0, begin_instruction)
999 |
1000 | if not re.search('serve', self.instructions[-1].instruction, flags=re.I):
1001 | self.instructions.insert(bake_instruction_idx, bake_instruction)
1002 |
1003 |
1004 | self.update_instructions()
1005 | self.name = self.name + ' (' + method + ')'
1006 |
1007 |
1008 | def to_easy(self):
1009 | """
1010 | makes recipes easier by replacing freshly made ingredients with store-bought,
1011 | disallowing finely done ingredients, and only allowing one type of chees
1012 | """
1013 | for i, ingredient in enumerate(self.ingredients):
1014 | if 'freshly' in ingredient.descriptor:
1015 | ingredient.descriptor.remove('freshly')
1016 | ingredient.descriptor.append('store-bought')
1017 | if 'finely' in ingredient.descriptor:
1018 | ingredient.descriptor.remove('finely')
1019 | self.ingredients[i] = ingredient
1020 |
1021 | cheeses = [ing for ing in self.ingredients if re.search('cheese', ing.name)]
1022 | if len(cheeses) > 1:
1023 | for i in range(1, len(cheeses)):
1024 | first_cheese = copy.deepcopy(cheeses[0])
1025 | first_cheese.measurement = cheeses[i].measurement
1026 | first_cheese.quantity = cheeses[i].quantity
1027 | print cheeses[0].quantity
1028 | self.swap_ingredients(cheeses[i], first_cheese)
1029 |
1030 |
1031 | def freq_dist(self, data):
1032 | """
1033 | builds a frequncy distrobution dictionary sorted by the most commonly occuring words
1034 | """
1035 | freqs = defaultdict(lambda: 0)
1036 | for d in data:
1037 | freqs[d] += 1
1038 | return sorted(freqs.items(), key=itemgetter(1), reverse=True)
1039 |
1040 |
1041 | def swap_ingredients(self, current_ingredient, new_ingredient):
1042 | """
1043 | replaces the current_ingredient with the new_ingredient.
1044 | Updates the associated instructions, times, and ingredients.
1045 | """
1046 | # (1) switch the ingredients in self.ingredients list
1047 | for i, ingredient in enumerate(self.ingredients):
1048 | if ingredient.name == current_ingredient.name:
1049 | self.ingredients[i] = new_ingredient
1050 |
1051 | # (2) update the instructions that mention it
1052 | name_length = len(current_ingredient.name.split(' '))
1053 | for i, instruction in enumerate(self.instructions):
1054 | for j in range(len(instruction.instruction_words) - name_length):
1055 | if current_ingredient.name == ' '.join(instruction.instruction_words[j:j+name_length]):
1056 | self.instructions[i].instruction_words[j] = new_ingredient.name
1057 |
1058 | # get rid of any extra words
1059 | for k in range(1, name_length):
1060 | self.instructions[i].instruction_words[j+k] == ''
1061 | self.instructions[i].update_instruction()
1062 |
1063 |
1064 | def remove_non_numerics(string): return re.sub('[^0-9]', '', string)
1065 |
1066 |
1067 | def parse_url(url):
1068 | """
1069 | reads the url and creates a recipe object.
1070 | Urls are expected to be from AllRecipes.com
1071 |
1072 |
1073 | Builds a dictionary that is passed into a Recipe object's init function and unpacked.
1074 | The dictionary is set up as
1075 |
1076 | {
1077 | name: string
1078 | preptime: int
1079 | cooktime: int
1080 | totaltime: int
1081 | ingredients: list of strings
1082 | instructions: list of strings
1083 | calories: int
1084 | carbs: int
1085 | fat: int
1086 | protien: int
1087 | cholesterol: int
1088 | sodium: int
1089 |
1090 | }
1091 | """
1092 | # retrieve data from url
1093 | result = requests.get(url, timeout=10)
1094 | c = result.content
1095 |
1096 | # store in BeautifulSoup object to parse HTML DOM
1097 | soup = BeautifulSoup(c, "lxml")
1098 |
1099 |
1100 | # find name
1101 | name = soup.find('h1', {'itemprop': 'name'}).text
1102 |
1103 | # find relavent time information
1104 | # some recipes are missing some of the times
1105 | try: preptime = remove_non_numerics(soup.find('time', {'itemprop': 'prepTime'}).text)
1106 | except: preptime = 0
1107 | try: cooktime = remove_non_numerics(soup.find('time', {'itemprop': 'cookTime'}).text)
1108 | except: cooktime = 0
1109 | try: totaltime = remove_non_numerics(soup.find('time', {'itemprop': 'totalTime'}).text)
1110 | except: totaltime = 0
1111 |
1112 | # find ingredients
1113 | ingredients = [i.text for i in soup.find_all('span', {'class': 'recipe-ingred_txt added'})]
1114 |
1115 | # find instructions
1116 | instructions = [i.text for i in soup.find_all('span', {'class': 'recipe-directions__list--item'})]
1117 |
1118 |
1119 | # nutrition facts
1120 | calories = remove_non_numerics(soup.find('span', {'itemprop': 'calories'}).text)
1121 | carbs = soup.find('span', {'itemprop': 'carbohydrateContent'}).text # measured in grams
1122 | fat = soup.find('span', {'itemprop': 'fatContent'}).text # measured in grams
1123 | protien = soup.find('span', {'itemprop': 'proteinContent'}).text # measured in grams
1124 | cholesterol = soup.find('span', {'itemprop': 'cholesterolContent'}).text # measured in miligrams
1125 | sodium = soup.find('span', {'itemprop': 'sodiumContent'}).text # measured in grams
1126 |
1127 |
1128 | if DEBUG:
1129 | print ('recipe is called {}'.format(name))
1130 | print ('prep time is {} minutes, cook time is {} minutes and total time is {} minutes'.format(preptime, cooktime, totaltime))
1131 | print ('it has {} ingredients'.format(len(ingredients)))
1132 | print ('it has {} instructions'.format(len(instructions)))
1133 | print ('it has {} calories, {} g of carbs, {} g of fat, {} g of protien, {} mg of cholesterol, {} mg of sodium'.format(calories, carbs, fat, protien, cholesterol, sodium))
1134 |
1135 |
1136 | return {
1137 | 'name': name,
1138 | 'preptime': preptime,
1139 | 'cooktime': cooktime,
1140 | 'totaltime': totaltime,
1141 | 'ingredients': ingredients,
1142 | 'instructions': instructions,
1143 | 'calories': calories,
1144 | 'carbs': carbs,
1145 | 'fat': fat,
1146 | 'protien': protien,
1147 | 'cholesterol': cholesterol,
1148 | 'sodium': sodium,
1149 | 'url': url
1150 | }
1151 |
1152 |
1153 | def build_dynamic_lists():
1154 | """
1155 | fills the lists of known foods from websites -- used to tag ingredients
1156 | """
1157 | global vegetable_list
1158 | global sauce_list
1159 | global herbs_spice_list
1160 | global dairy_list
1161 | global meat_list
1162 | global grain_list
1163 | global fruit_list
1164 | global seafood_list
1165 |
1166 | # build vegetable list
1167 | url = 'https://simple.wikipedia.org/wiki/List_of_vegetables'
1168 | result = requests.get(url, timeout=10)
1169 | c = result.content
1170 |
1171 | # store in BeautifulSoup object to parse HTML DOM
1172 | soup = BeautifulSoup(c, "lxml")
1173 |
1174 | lis = [li.text.strip() for li in soup.find_all('li')]
1175 | lis_clean = []
1176 | for li in lis:
1177 | if li == 'Lists of vegetables': break
1178 | if len(li) == 1: continue
1179 | if re.search('\d', li): continue
1180 | if re.search('\n', li): continue
1181 | lis_clean.append(li.lower())
1182 | vegetable_list = lis_clean
1183 |
1184 |
1185 | # build herbs and spices list
1186 | url = 'https://en.wikipedia.org/wiki/List_of_culinary_herbs_and_spices'
1187 | result = requests.get(url, timeout=10)
1188 | c = result.content
1189 |
1190 | # store in BeautifulSoup object to parse HTML DOM
1191 | soup = BeautifulSoup(c, "lxml")
1192 |
1193 | lis = [li.text.strip() for li in soup.find_all('li')][3:]
1194 | lis_clean = []
1195 | for li in lis:
1196 | if len(li) == 1: continue
1197 | if re.search('\d', li): continue
1198 | if re.search('\n', li): continue
1199 | if li == 'Category': break
1200 | lis_clean.append(li.lower())
1201 | herbs_spice_list = lis_clean
1202 |
1203 |
1204 | # build sauces list
1205 | url = 'https://en.wikipedia.org/wiki/List_of_sauces'
1206 | result = requests.get(url, timeout=10)
1207 | c = result.content
1208 |
1209 | # store in BeautifulSoup object to parse HTML DOM
1210 | soup = BeautifulSoup(c, "lxml")
1211 |
1212 | lis = [li.text.strip() for li in soup.find_all('li')]
1213 | lis_clean = []
1214 | for li in lis:
1215 | if len(li) == 1: continue
1216 | if re.search('\d', li): continue
1217 | if re.search('\n', li): continue
1218 | if li == 'Category': break
1219 | lis_clean.append(li.lower())
1220 | sauce_list = lis_clean
1221 |
1222 |
1223 | # build meat list
1224 | url = 'http://naturalhealthtechniques.com/list-of-meats-and-poultry/'
1225 | result = requests.get(url, timeout=10)
1226 | c = result.content
1227 |
1228 | # store in BeautifulSoup object to parse HTML DOM
1229 | soup = BeautifulSoup(c, "lxml")
1230 |
1231 | div = soup.find('div', {'class': 'entry-content'})
1232 | lis = [li.text.strip() for li in div.find_all('li')]
1233 | lis_clean = []
1234 | for li in lis:
1235 | if len(li) == 1: continue
1236 | if re.search('\d', li): continue
1237 | if re.search('\n', li): continue
1238 | lis_clean.append(li.lower())
1239 | meat_list = lis_clean
1240 |
1241 | # build seafood_list and also extend to the meat_list
1242 | url = 'http://naturalhealthtechniques.com/list-of-fish-and-seafood/'
1243 | result = requests.get(url, timeout=10)
1244 | c = result.content
1245 |
1246 | # store in BeautifulSoup object to parse HTML DOM
1247 | soup = BeautifulSoup(c, "lxml")
1248 |
1249 | div = soup.find('div', {'class': 'entry-content'})
1250 | lis = [li.text.strip() for li in div.find_all('li')]
1251 | lis_clean = []
1252 | for li in lis:
1253 | if len(li) == 1: continue
1254 | if re.search('\d', li): continue
1255 | if re.search('\n', li): continue
1256 | lis_clean.append(li.lower())
1257 | meat_list.extend(lis_clean)
1258 | seafood_list = lis_clean
1259 |
1260 | # build dairy list
1261 | url = 'http://naturalhealthtechniques.com/list-of-cheese-dairy-products/'
1262 | result = requests.get(url, timeout=10)
1263 | c = result.content
1264 |
1265 | # store in BeautifulSoup object to parse HTML DOM
1266 | soup = BeautifulSoup(c, "lxml")
1267 |
1268 | div = soup.find('div', {'class': 'entry-content'})
1269 | lis = [li.text.strip() for li in div.find_all('li')]
1270 | lis_clean = []
1271 | for li in lis:
1272 | if len(li) == 1: continue
1273 | if re.search('\d', li): continue
1274 | if re.search('\n', li): continue
1275 | lis_clean.append(li.lower())
1276 | dairy_list = lis_clean
1277 |
1278 |
1279 | # build grains list
1280 | url = 'http://naturalhealthtechniques.com/list-of-grains-cereals-pastas-flours/'
1281 | result = requests.get(url, timeout=10)
1282 | c = result.content
1283 |
1284 | # store in BeautifulSoup object to parse HTML DOM
1285 | soup = BeautifulSoup(c, "lxml")
1286 |
1287 | div = soup.find('div', {'class': 'entry-content'})
1288 | lis = [li.text.strip() for li in div.find_all('li')]
1289 | lis_clean = []
1290 | for li in lis:
1291 | if len(li) == 1: continue
1292 | if re.search('\d', li): continue
1293 | if re.search('\n', li): continue
1294 | lis_clean.append(li.lower())
1295 | grain_list = lis_clean
1296 |
1297 |
1298 | # build grains list
1299 | url = 'http://naturalhealthtechniques.com/list-of-fruits/'
1300 | result = requests.get(url, timeout=10)
1301 | c = result.content
1302 |
1303 | # store in BeautifulSoup object to parse HTML DOM
1304 | soup = BeautifulSoup(c, "lxml")
1305 |
1306 | div = soup.find('div', {'class': 'entry-content'})
1307 | lis = [li.text.strip() for li in div.find_all('li')]
1308 | lis_clean = []
1309 | for li in lis:
1310 | if len(li) == 1: continue
1311 | if re.search('\d', li): continue
1312 | if re.search('\n', li): continue
1313 | lis_clean.append(li.lower())
1314 | fruit_list = lis_clean
1315 |
1316 |
1317 | def timeit(method):
1318 | def timed(*args, **kw):
1319 | ts = time.time()
1320 | result = method(*args, **kw)
1321 | te = time.time()
1322 | if 'log_time' in kw:
1323 | name = kw.get('log_name', method.__name__.upper())
1324 | kw['log_time'][name] = int((te - ts))
1325 | else:
1326 | print '%r %2.2f s' % \
1327 | (method.__name__, (te - ts))
1328 | return result
1329 | return timed
1330 |
1331 |
1332 | @timeit
1333 | def main():
1334 | """
1335 | main function -- runs all initalization and any methods user wants
1336 | """
1337 |
1338 | # parse websites to build global lists -- used for Ingredient type tagging
1339 | build_dynamic_lists()
1340 |
1341 | URL = 'http://allrecipes.com/recipe/234667/chef-johns-creamy-mushroom-pasta/?internalSource=rotd&referringId=95&referringContentType=recipe%20hub'
1342 | # URL = 'http://allrecipes.com/recipe/21014/good-old-fashioned-pancakes/?internalSource=hub%20recipe&referringId=1&referringContentType=recipe%20hub'
1343 | # URL = 'https://www.allrecipes.com/recipe/60598/vegetarian-korma/?internalSource=hub%20recipe&referringId=1138&referringContentType=recipe%20hub'
1344 | # URL = 'https://www.allrecipes.com/recipe/8836/fried-chicken/?internalSource=hub%20recipe&referringContentType=search%20results&clickId=cardslot%202'
1345 | # URL = 'https://www.allrecipes.com/recipe/52005/tender-italian-baked-chicken/?internalSource=staff%20pick&referringId=201&referringContentType=recipe%20hub'
1346 |
1347 | # URLS = [
1348 | # 'https://www.allrecipes.com/recipe/213717/chakchouka-shakshouka/?internalSource=hub%20recipe&referringContentType=search%20results&clickId=cardslot%201',
1349 | # 'https://www.allrecipes.com/recipe/216756/baked-ham-and-cheese-party-sandwiches/?internalSource=hub%20recipe&referringContentType=search%20results&clickId=cardslot%205',
1350 | # 'https://www.allrecipes.com/recipe/234592/buffalo-chicken-stuffed-shells/',
1351 | # 'https://www.allrecipes.com/recipe/23109/rainbow-citrus-cake/',
1352 | # 'https://www.allrecipes.com/recipe/219910/homemade-cream-filled-sponge-cakes/',
1353 | # 'https://www.allrecipes.com/recipe/16700/salsa-chicken/?internalSource=hub%20recipe&referringId=1947&referringContentType=recipe%20hub',
1354 | # 'https://www.allrecipes.com/recipe/109190/smooth-sweet-tea/',
1355 | # 'https://www.allrecipes.com/recipe/220943/chef-johns-buttermilk-biscuits/',
1356 | # 'https://www.allrecipes.com/recipe/24501/tangy-honey-glazed-ham/?internalSource=hub%20recipe&referringId=15876&referringContentType=recipe%20hub',
1357 | # 'https://www.allrecipes.com/recipe/247204/red-split-lentils-masoor-dal/?internalSource=staff%20pick&referringId=233&referringContentType=recipe%20hub'
1358 | # 'https://www.allrecipes.com/recipe/233856/mauigirls-smoked-salmon-stuffed-pea-pods/?internalSource=staff%20pick&referringId=416&referringContentType=recipe%20hub',
1359 | # 'https://www.allrecipes.com/recipe/169305/sopapilla-cheesecake-pie/?internalSource=hub%20recipe&referringId=728&referringContentType=recipe%20hub',
1360 | # 'https://www.allrecipes.com/recipe/85389/gourmet-mushroom-risotto/?internalSource=hub%20recipe&referringId=723&referringContentType=recipe%20hub',
1361 | # 'https://www.allrecipes.com/recipe/138020/st-patricks-colcannon/?internalSource=staff%20pick&referringId=197&referringContentType=recipe%20hub',
1362 | # 'https://www.allrecipes.com/recipe/18241/candied-carrots/?internalSource=hub%20recipe&referringId=194&referringContentType=recipe%20hub',
1363 | # 'https://www.allrecipes.com/recipe/18870/roast-leg-of-lamb-with-rosemary/?internalSource=hub%20recipe&referringId=194&referringContentType=recipe%20hub',
1364 | # 'https://www.allrecipes.com/recipe/8270/sams-famous-carrot-cake/?internalSource=hub%20recipe&referringId=188&referringContentType=recipe%20hub',
1365 | # 'https://www.allrecipes.com/recipe/13717/grandmas-green-bean-casserole/?internalSource=hub%20recipe&referringId=188&referringContentType=recipe%20hub'
1366 |
1367 | # ]
1368 |
1369 | # for url in URLS:
1370 | # recipe_attrs = parse_url(url)
1371 | # recipe = Recipe(**recipe_attrs)
1372 | # print(recipe.to_JSON())
1373 |
1374 | recipe_attrs = parse_url(URL)
1375 | recipe = Recipe(**recipe_attrs)
1376 | print(recipe.to_JSON())
1377 | # recipe.to_vegan()
1378 | # recipe.from_vegan()
1379 |
1380 | # recipe.to_vegan()
1381 | # recipe.from_vegan()
1382 | # recipe.to_vegetarian()
1383 | # recipe.from_vegetarian()
1384 | # recipe.to_pescatarian()
1385 | # recipe.from_pescatarian()
1386 | # recipe.to_healthy()
1387 | # recipe.from_healthy()
1388 | # recipe.to_style('Thai')
1389 | recipe.to_style('Mexican')
1390 | # recipe.to_method('bake')
1391 | # recipe.to_easy();
1392 | print(recipe.to_JSON())
1393 | print(recipe.compare_to_original())
1394 | # recipe.to_method('fry')
1395 | # # recipe.print_pretty()
1396 |
1397 |
1398 |
1399 | #============================================================================
1400 | # Start webPy environment
1401 | #============================================================================
1402 |
1403 |
1404 | def main_gui(url, method, parameter):
1405 |
1406 | # parse websites to build global lists -- used for Ingredient type tagging
1407 | build_dynamic_lists()
1408 |
1409 |
1410 | URL = url
1411 | recipe_attrs = parse_url(URL)
1412 |
1413 | recipe = Recipe(**recipe_attrs)
1414 | s = ""
1415 | s += recipe.to_JSON()
1416 |
1417 | if method == 'to_vegan':
1418 | recipe.to_vegan()
1419 | elif method == 'from_vegan':
1420 | recipe.from_vegan()
1421 | elif method == 'to_vegetarian':
1422 | recipe.to_vegetarian()
1423 | elif method == 'from_vegetarian':
1424 | recipe.from_vegetarian()
1425 | elif method == 'to_pescatarian':
1426 | recipe.to_pescatarian()
1427 | elif method == 'from_pescatarian':
1428 | recipe.from_pescatarian()
1429 | elif method == 'to_healthy':
1430 | recipe.to_healthy()
1431 | elif method == 'from_healthy':
1432 | recipe.from_healthy()
1433 | elif method == 'to_style(Thai)':
1434 | recipe.to_style('Thai')
1435 | elif method == 'to_style(parameter)':
1436 | if not parameter:
1437 | return "This method requires a parameter"
1438 | recipe.to_style(parameter)
1439 | elif method == 'to_method(parameter)':
1440 | if not parameter:
1441 | return "This method requires a parameter"
1442 | recipe.to_method(parameter)
1443 |
1444 |
1445 |
1446 | s += recipe.to_JSON()
1447 | s += recipe.compare_to_original()
1448 |
1449 | return s
1450 |
1451 | render = web.template.render('templates/')
1452 |
1453 | urls = ('/', 'index')
1454 | class RecipeApp(web.application):
1455 | def run(self, port=8080, *middleware):
1456 | func = self.wsgifunc(*middleware)
1457 | return web.httpserver.runsimple(func, ('0.0.0.0', port))
1458 |
1459 | myform = form.Form(
1460 | form.Textbox("url",
1461 | form.notnull),
1462 | form.Dropdown('transformation', ['to_vegan', 'from_vegan', 'to_vegetarian', 'from_vegetarian', 'to_pescatarian', 'from_pescatarian', 'to_healthy', 'from_healthy', 'to_style(parameter)', 'to_method(parameter)']),
1463 | form.Textbox("parameter (optional)"))
1464 |
1465 |
1466 |
1467 | class index:
1468 | def GET(self):
1469 | form = myform()
1470 | # make sure you create a copy of the form by calling it (line above)
1471 | # Otherwise changes will appear globally
1472 | return render.formtest(form)
1473 |
1474 | def POST(self):
1475 | form = myform()
1476 | if not form.validates():
1477 | return render.formtest(form)
1478 | else:
1479 | return main_gui(form.d.url, form['transformation'].value, form['parameter (optional)'].value)
1480 |
1481 |
1482 |
1483 | if __name__ == "__main__":
1484 | parser = argparse.ArgumentParser()
1485 | parser.add_argument("--gui", help="run application on locally hosted webpage", action="store_true")
1486 |
1487 | args = parser.parse_args()
1488 | if args.gui:
1489 | sys.argv[1] = ''
1490 | web.internalerror = web.debugerror
1491 | app = RecipeApp(urls, globals())
1492 | app.run()
1493 | else:
1494 | main()
1495 |
--------------------------------------------------------------------------------
/templates/formtest.py:
--------------------------------------------------------------------------------
1 | $def with (form)
2 |
3 |
4 |