Recipe Management System

This is my all-in-one solution to digitizing recipe books and nutrition guides. The central concept is to isolate all of the information entry into one tool, and then to offer the ability to export relevant information to various formats. This ties in nicely with my Food Inventory Management System, because the core tool is CRONOMETER, and the ingredients can be added to the system via barcode scanning. Since CRONOMETER knows about all ingredients (they are tagged with a barcode in their comment), additional scripts can query the inventory database to find out which ingredients are available. Simply create a custom recipe in CRONOMETER. Then, in the comments field, add the step by step instructions - separate steps with a period. Then, just run the script (soon to appear below)! This solution takes the CRONOMETER custom recipe format and turns it into a set of useful recipe cards. It's simply some CSS and a bit of XML parsing. For example, some of my custom recipes are available at Wilson's Recipes.

Details

A Python script is used to search through all XML files in the CRONOMETER custom XML directory. For each file it finds with a "recipe" tag, it then extracts the information about the constitutent ingredients - quantity and CRONOMETER ID. Resolving this ID is the tricky part. Mainly, IDs come from two locations: pre-programmed USDA ingredients, and custom ingredients. If the ingredient is custom, it locates that XML file in the same directory and pulls the name; if it's USDA, I've created a USDA name-ID correlation file that it searches to obtain the name. The rest is just gravy. The script produces a RecipeML file. I needed to add the id tag in order to get linking to work properly when the XML is rendered as HTML. In addition, it converts this XML file to a format slightly better for web viewing, mostly by linking it to a CSS file. You can use mine (below) to get a webpage that looks like a set of recipe cards. Static linking to individual recipes is possible using a hash and the title of the recipe, minus any non-alphanumeric characters. This is encoded in the XML as an id under the title tag.

The extension possibilities are great. For example, you could write a script to prompt you with a few recipes - it grabs the time, then queries your inventory for ingredients you have in stock, cross-examines the RecipeML file, and tells you what you can make. If you're hosting an event, you could also write a script to pull a few selected recipes from the master file to share around and make sure there are no allergy conflicts. If you're on a certain type of diet, it's fairly easy to extend the portion of the script that creates the XML to nutrition information - CRONOMETER knows virtually all of the nutritional information of the recipe, so it is possible to quickly see the resultant dietary statistics of a given recipe - something most recipe books cannot boast.

Code

You can find the CSS file to nicely display the XML file here:

wbrenna.ca/wilson/recipes/recipes.css

Python Cronometer-RecipeML Script


#!/usr/bin/python 

import bisect
import glob
import os
import xml.etree.ElementTree as ET
from subprocess import call

recipeFilesDir = 'someCronometerFoodsDirectory/'

if os.path.isfile("cronoRecipes.xml"):
	os.rename("cronoRecipes.xml", "cronoRecipes.xml.bak")
if os.path.isfile("cronoRecipesFormatted.xml"):
	os.rename("cronoRecipesFormatted.xml", "cronoRecipesFormatted.xml.bak")

usda_map_list = [i.strip().split('\t') for i in open("usda_map.txt").readlines()]
keys = [r[0] for r in usda_map_list]
print usda_map_list[0]
print usda_map_list[bisect.bisect_left(keys,'01017')][1]

recipeRoot = ET.Element("recipeml")
recipeRoot.set("version","0.5")
recipeRoot.set("generator","CronoWilson 1.0")
recipeRoot.set("xml:lang","en")
recipeMenu = ET.SubElement(recipeRoot,"menu")
recipeHead = ET.SubElement(recipeMenu,"head")
recipeTitle = ET.SubElement(recipeHead,"title")
recipeTitle.text = "My Cronometer Recipes"
recipeDesc = ET.SubElement(recipeMenu,"description")
recipeDesc.text = "Converted recipes from a Cronometer Database."

#Now we just need to read the Cronometer file. Use XML reader for this:
for files in glob.glob(recipeFilesDir + "*.xml"):
	print "Processing " + files
	tree = ET.parse(files)
	root = tree.getroot()

	if root.tag == 'recipe':
#		print root.tag + " found!"
		print root.attrib['name']
		recipeRecipe = ET.SubElement(recipeMenu,"recipe")
		recipeHead2 = ET.SubElement(recipeRecipe,"head")
		recipeTitle2 = ET.SubElement(recipeHead2,"title",id=''.join(e for e in root.attrib['name'] if e.isalnum()))
		recipeTitle2.text = root.attrib['name']
		recipeIng = ET.SubElement(recipeRecipe,"ingredients")
		recipeDirections = ""
		for child in root:
			if child.tag == 'serving':
				#print "Ingredient found. Parsing..."
				ingredientname = ""
				amount = ""
				for attribkey in child.attrib.keys():
					if attribkey == 'name':
#						print "Name found! It's " + child.attrib[attribkey]
						ingredientname = child.attrib[attribkey]
					elif attribkey == 'source':
						if child.attrib[attribkey] == 'USDA':
#							print "USDA database. Ref " + child.attrib['food']
#							print "This ingredient is " + usda_map_list[bisect.bisect_left(keys,child.attrib['food'])][1]
							ingredientname = usda_map_list[bisect.bisect_left(keys,child.attrib['food'])][1]
						elif child.attrib[attribkey] == 'My Foods':
							tmptree = ET.parse(recipeFilesDir + child.attrib['food'] + '.xml')
							tmproot = tmptree.getroot()
							ingredientname = tmproot.attrib['name']
					elif attribkey == 'grams':
						amount = child.attrib[attribkey]

				if ingredientname == "":
					continue;
				elif amount == "":
					continue;
				else:
					print "Name is " + ingredientname + " and amount is " + amount
					recipeIngred = ET.SubElement(recipeIng,"ing")
					recipeAmt = ET.SubElement(recipeIngred,"amt")
					recipeQty = ET.SubElement(recipeAmt,"qty")
					recipeQty.text = amount
					recipeUnit = ET.SubElement(recipeAmt,"unit")
					recipeUnit.text = "grams (g)"
					recipeI = ET.SubElement(recipeIng,"item")
					recipeI.text = ingredientname
					
			elif child.tag == 'comments':
				print "Instructions are given."
				recipeDirections = child.text

		recipeInstructions = ET.SubElement(recipeRecipe,"directions")
		if recipeDirections is None:
			recipeInstructions.text = ""
		elif recipeDirections != "":
			for j in recipeDirections.split('. '):
				recipeStep = ET.SubElement(recipeInstructions,"step")
				recipeStep.text = j
		else:
			recipeInstructions.text = ""


recipeTree = ET.ElementTree(recipeRoot)
recipeTree.write("cronoRecipes.xml")
					
call(["./formatScript.sh"])

Bash Cronometer-RecipeML Support Script


#!/bin/bash
xmllint --format cronoRecipes.xml > cronoRecipesFormatted.xml

echo '<?xml version="1.0"?>
<!DOCTYPE recipeml PUBLIC "-//FormatData//DTD RecipeML 0.5//EN"
"http://www.formatdata.com/recipeml/recipeml.dtd">
<?xml-stylesheet href="recipes.css" type="text/css"?>' > myRecipes.xml
tail --lines=+2 cronoRecipesFormatted.xml >> myRecipes.xml