基于规则引擎与加权随机的智能周菜单生成系统设计与实现
1. 项目概述:从“今天吃什么”到一周菜单的自动化生成
“今天吃什么?”这大概是每个家庭、每个独居者,甚至每个需要自己解决三餐的人,每天都要面对的灵魂拷问。点外卖,久了钱包和健康都吃不消;自己做饭,光是构思菜单、搭配营养、计算分量、采购食材,就足以消耗掉下班后仅存的精力。更别提要规划一周的饮食,既要考虑口味变化,又要兼顾营养均衡,还要避免食材浪费,这简直是一项系统工程。
kwokronny/week-menu-generate这个项目,就是瞄准了这个普遍存在的痛点。它不是一个简单的菜谱集合,而是一个智能化的周度菜单生成器。其核心目标,是帮助用户自动化地解决“吃什么”的决策难题,并延伸至购物清单的生成,从而将我们从繁琐的饮食规划中解放出来。想象一下,每周日晚上,你只需要运行一下这个程序,它就能根据你的口味偏好、饮食限制(如素食、无麸质)、营养目标,甚至冰箱里剩余的食材,为你生成一份从周一到周日的完整菜单,并附带一份可以直接拿去超市采购的详细清单。这不仅仅是省心,更是一种生活方式的优化。
这个项目适合所有需要自己规划饮食的人:忙碌的上班族、注重健康的健身人士、需要为全家准备餐食的家庭主妇/主夫,甚至是想要尝试新菜式但缺乏灵感的美食爱好者。它的价值在于将主观、感性的饮食选择,转化为一个可定制、可重复、数据驱动的理性过程。接下来,我将深入拆解这个项目的设计思路、技术实现以及在实际应用中可能遇到的坑,希望能为你构建或使用类似工具提供一份详尽的参考。
2. 项目整体设计与核心思路拆解
2.1 需求场景与核心功能定义
要构建一个有用的周菜单生成器,我们首先要明确用户的核心场景和期望的功能。经过分析,主要场景可以归纳为以下几类:
- 省时省力型:用户最核心的需求是“别让我想”。他们希望输入尽可能少的条件(比如“不吃辣”、“想要减肥餐”),就能得到一份可以直接执行的菜单。
- 营养管理型:用户可能有具体的健康目标,如增肌、减脂、控制血糖。他们需要菜单能粗略计算并平衡蛋白质、碳水、脂肪的摄入,甚至考虑微量元素。
- 清库存/节约型:用户希望优先使用家里已有的食材,避免浪费。生成器需要能接受“现有食材”作为输入,并围绕这些食材来设计菜单。
- 探索尝新型:用户吃腻了家常菜,希望系统能定期引入一些新菜式,但又不能太复杂或食材太偏门。
基于这些场景,一个完整的week-menu-generate系统应该具备以下核心功能模块:
- 菜谱数据库:这是系统的基石。需要包含足够多、结构化良好的菜谱数据。每条菜谱至少应包含:菜名、所需食材(及分量)、烹饪步骤、预估耗时、菜系分类、口味标签(辣、甜等)、难度等级、以及粗略的营养成分(可选)。
- 用户偏好画像:系统需要记录用户的口味喜好、饮食禁忌(过敏、宗教、素食等)、烹饪设备限制(如无烤箱)、以及营养目标(如每日热量区间)。
- 智能生成引擎:这是大脑。它根据用户画像,从菜谱数据库中筛选和组合菜谱,形成一日三餐(或两餐)的搭配,并确保一周内不重复、营养均衡、烹饪时间合理分布(比如工作日安排快手菜)。
- 购物清单整合:根据生成的周菜单,自动汇总所有需要的食材,合并同类项,并计算出总采购量。理想情况下,还能按超市货架分区(果蔬区、冷藏区、干货区)进行归类。
- 手动调整与反馈:生成的结果不可能100%满意,必须允许用户手动替换某一天的某一餐。并且,用户对某道菜的评分(“太难吃了”或“太棒了”)应能反馈给系统,用于优化未来的推荐。
2.2 技术架构选型与考量
这样一个项目,技术栈的选择可以很轻量,也可以很复杂,取决于你想要的功能深度。
1. 数据层:菜谱数据从哪里来?这是第一个难题。自己手动录入几百上千道菜谱是不现实的。通常有几种方案:
- 爬取公开菜谱网站:这是最直接的方式,但涉及法律和伦理风险(版权、反爬虫策略)。如果仅为个人学习使用,需要非常谨慎,并严格遵守网站的
robots.txt协议。技术上可以用 Python 的requests+BeautifulSoup或Scrapy框架。 - 使用开源或付费的菜谱 API:国外有一些如
Spoonacular API,功能强大,包含营养数据,但通常是付费的。国内这类成熟的API较少。 - 共建社区数据库:做一个允许用户提交菜谱的UGC系统,但这属于另一个庞大的项目,冷启动困难。
- 使用本地结构化文件:对于最小可行产品(MVP),完全可以自己整理一个几十上百道家常菜的 JSON 或 YAML 文件作为种子数据库。这虽然数据量小,但完全可控,没有外部依赖。
实操心得:对于个人项目或学习原型,强烈建议从本地JSON文件开始。你可以先精心设计50道你常做、爱吃的菜谱的结构化数据。这能让你快速搭建起核心逻辑,避免在数据获取上陷入泥潭。等核心引擎跑通后,再考虑如何优雅地扩充数据。
2. 核心逻辑层:生成算法如何设计?这是项目的灵魂。一个简单的生成器可能只是随机选择。但一个智能的生成器需要考虑多重约束:
- 过滤:根据用户禁忌(如“不吃猪肉”)过滤掉所有相关菜谱。
- 评分:为用户偏好(如“喜欢川菜”)的菜谱加分。
- 多样性:确保同一周内,主要食材(鸡、牛、鱼、豆腐)和菜系尽量轮换。
- 均衡性:尽量让午餐和晚餐有荤有素,有干有稀。早餐可以相对固定或简单。
- 时间成本:将烹饪时间作为权重,工作日优先分配“快手菜”(<30分钟)。
这本质上是一个带约束的优化问题。对于初学者,不必一开始就上复杂的机器学习推荐算法。一个非常有效且易于理解的策略是**“规则引擎+加权随机”**:
- 定义一系列规则(如“周一至周五晚餐烹饪时间<40分钟”、“每天至少有一道绿叶蔬菜”、“鱼类每周出现2-3次”)。
- 为每个候选菜谱根据符合的规则数量、用户偏好匹配度计算一个综合权重。
- 每天从高权重的菜谱池中,按权重进行随机选择(而非总是选最高分),这样既能保证质量,又保留了惊喜感和多样性。
3. 呈现与交互层:如何交付给用户?
- 命令行界面(CLI):最快捷的方式。生成一个格式好看的文本菜单和购物清单,打印到控制台或输出到
.txt/.md文件。适合技术用户和自动化脚本调用。 - Web 界面:更友好的方式。可以用 Flask、Django(Python)或 Express(Node.js)搭建一个简单的网页,用户通过表单设置偏好,点击生成,网页上以表格形式展示美观的周菜单,并提供一键导出购物清单为PDF或文本。
- 移动端/消息机器人:更高阶的集成。可以做成 Telegram/微信机器人,每天定时推送当日菜单,或者接受语音指令“重新生成周三的晚餐”。
技术选型建议:Python是这个项目的绝佳选择。它在数据处理(Pandas)、简单爬虫、Web后端(Flask/FastAPI)和快速原型开发上都有巨大优势。核心生成逻辑用纯Python实现,数据用JSON存储,初期用CLI交互,是阻力最小的路径。
3. 核心模块解析与实操要点
3.1 构建结构化的菜谱数据库
菜谱数据的结构设计直接决定了后续所有功能的复杂度和可行性。一个设计良好的菜谱对象应该包含以下字段:
{ “id”: “mapo_tofu_001”, “name”: “麻婆豆腐”, “cuisine”: [“川菜”], “tags”: [“辣”, “下饭菜”, “豆制品”, “快手菜”], “meal_type”: [“午餐”, “晚餐”], “difficulty”: “中等”, “prep_time”: 15, // 准备时间,分钟 “cook_time”: 10, // 烹饪时间,分钟 “servings”: 2, // 份量 “ingredients”: [ // 食材列表,这是核心! {“name”: “嫩豆腐”, “quantity”: 1, “unit”: “盒”, “category”: “豆制品”}, {“name”: “猪肉末”, “quantity”: 100, “unit”: “克”, “category”: “肉类”}, {“name”: “郫县豆瓣酱”, “quantity”: 1.5, “unit”: “汤匙”, “category”: “调味品”}, {“name”: “蒜”, “quantity”: 3, “unit”: “瓣”, “category”: “蔬菜”}, {“name”: “小葱”, “quantity”: 2, “unit”: “根”, “category”: “蔬菜”} ], “steps”: [“豆腐切块焯水”, “炒香肉末和豆瓣酱...”, “加水烧开下豆腐...”, “勾芡撒葱花”], “nutrition”: { // 营养信息,可粗略估算或留空 “calories”: 350, “protein”: 20, “carbs”: 15, “fat”: 25 } }关键点解析:
- 食材的量化:
quantity和unit必须标准化。尽量使用“克”、“毫升”、“个”、“汤匙”这类通用单位,避免“适量”、“少许”。这是后续自动计算购物量的基础。 - 食材分类:
category字段非常重要,它直接服务于购物清单的归类(“肉类”、“蔬菜”、“调味品”、“主食”)。 - 标签系统:
tags和cuisine是用户偏好匹配的关键。标签要尽可能丰富和准确,例如“辣”、“酸”、“烤箱菜”、“便当菜”、“高蛋白”。 - 时间成本:将
prep_time和cook_time分开,有助于更精确地评估一道菜的实际耗时。
实操步骤:创建你的种子数据库
- 新建一个
recipes.json文件。 - 列出你最常做的20-30道菜。
- 为每一道菜,按照上述结构,查找或估算其食材用量和烹饪时间,并填好标签。
- 这个过程虽然枯燥,但一劳永逸。你可以边吃边整理,或者利用周末时间完成。
注意事项:在定义食材
name时,要统一命名。比如“番茄”、“西红柿”、“西红柿(番茄)”在系统中会被视为三种不同的食材,导致购物清单无法合并。建议建立一个ingredient_aliases.json文件来管理别名映射,例如{“番茄”: [“西红柿”, “tomato”]},在生成购物清单前进行归一化处理。
3.2 用户偏好系统的设计
用户偏好不能是一个简单的配置文件,而应该是一个可以学习和调整的模型。初期可以设计为一个JSON配置文件:
{ “user_id”: “default”, “disliked_ingredients”: [“香菜”, “内脏”], “allergies”: [“海鲜”], “dietary_restrictions”: [“素食”], // 可选:素食、纯素、无麸质等 “preferred_cuisines”: [“川菜”, “家常菜”], “preferred_tags”: [“快手菜”, “下饭菜”], “avoid_tags”: [“油炸”, “烘焙”], // 太费事或不健康 “cooking_equipment”: [“炒锅”, “电饭煲”], // 没有烤箱就过滤掉需要烤箱的菜 “nutrition_goal”: { “max_daily_calories”: 2000, “min_daily_protein”: 60 // 克 }, “default_servings”: 2 // 默认制作几人份 }高级考量:反馈循环系统应该记录用户对生成菜谱的交互行为:
- 显式反馈:用户对某道菜评分(1-5星)或标记“不再显示”。
- 隐式反馈:用户手动替换了某道菜,被替换掉的菜谱可以视为一次负反馈;而用户连续几周都保留的菜谱,可以视为正反馈。 这些反馈数据可以用来动态调整菜谱的权重或用户偏好标签,让系统越用越“懂你”。
3.3 智能生成引擎的实现逻辑
这是最核心的代码部分。我们可以将其分解为几个步骤,用伪代码表示核心逻辑:
def generate_weekly_menu(user_prefs, recipe_db, days=7): weekly_menu = [] used_recipe_ids = set() # 用于避免重复 primary_ingredient_tracker = {} # 跟踪主要食材出现次数,如 {“鸡”: 2, “鱼”: 1} for day in range(days): daily_meals = {} for meal_type in [“breakfast”, “lunch”, “dinner”]: # 假设生成三餐 # 1. 过滤:根据用户禁忌、设备过滤 candidate_recipes = filter_recipes(recipe_db, user_prefs, meal_type) # 2. 评分:根据用户偏好、近期使用情况、食材多样性等计算权重 for recipe in candidate_recipes: recipe.score = calculate_recipe_score(recipe, user_prefs, used_recipe_ids, primary_ingredient_tracker, day) # 3. 选择:按权重随机选择,而非绝对最高分 selected_recipe = weighted_random_choice(candidate_recipes) daily_meals[meal_type] = selected_recipe # 4. 更新跟踪器 used_recipe_ids.add(selected_recipe.id) update_ingredient_tracker(selected_recipe, primary_ingredient_tracker) weekly_menu.append({“day”: day, “meals”: daily_meals}) return weekly_menu def calculate_recipe_score(recipe, user_prefs, used_recipe_ids, ingredient_tracker, day_of_week): score = 0 # 基础分 score += 10 # 偏好加分:菜系匹配、标签匹配 if any(cuisine in user_prefs[“preferred_cuisines”] for cuisine in recipe.cuisine): score += 5 if any(tag in user_prefs[“preferred_tags”] for tag in recipe.tags): score += 3 # 时间适应性加分:工作日给快手菜加分 if day_of_week < 5 and recipe.cook_time < 30: # 周一到周五 score += 4 # 重复惩罚:完全相同的菜谱本周已用过 if recipe.id in used_recipe_ids: score -= 100 # 极大惩罚,几乎不可能被选中 # 食材多样性惩罚:如果本周某种肉类出现太多次,则降低同类菜谱权重 main_ingredient = get_main_ingredient(recipe) # 例如“鸡”、“猪”、“牛”、“鱼”、“豆腐” if main_ingredient in ingredient_tracker and ingredient_tracker[main_ingredient] >= 2: score -= 2 * ingredient_tracker[main_ingredient] # 出现越多,惩罚越重 return score关键函数解释:
weighted_random_choice: 不是选择最高分,而是根据分数作为权重进行随机选择。这保证了结果有一定的随机性,避免每周菜单都一样。实现时可以将分数转换为概率(概率 = 该菜谱分数 / 所有候选菜谱分数总和)。get_main_ingredient: 这是一个需要定义的启发式函数。可以从菜谱的ingredients中,根据category找出最重要的蛋白质来源或主要食材。
4. 完整实现流程与核心代码剖析
4.1 环境准备与项目初始化
我们选择 Python 作为实现语言。首先创建一个干净的项目目录。
mkdir week-menu-generator cd week-menu-generator python -m venv venv # 创建虚拟环境 # Windows: venv\Scripts\activate # Mac/Linux: source venv/bin/activate pip install pandas # 用于数据处理和购物清单汇总 # 如果计划做Web版,可以安装 Flask: pip install flask项目目录结构建议如下:
week-menu-generator/ ├── data/ │ ├── recipes.json # 核心菜谱数据库 │ ├── user_preferences.json # 用户偏好配置 │ └── ingredient_aliases.json # 食材别名映射 ├── src/ │ ├── __init__.py │ ├── recipe_loader.py # 负责加载和解析菜谱数据 │ ├── preference_manager.py # 管理用户偏好 │ ├── menu_generator.py # 核心生成引擎 │ ├── shopping_list.py # 生成购物清单 │ └── cli.py # 命令行交互入口 ├── outputs/ # 存放生成的菜单和清单 ├── requirements.txt └── README.md4.2 核心生成引擎的代码实现
我们聚焦于menu_generator.py的核心部分。这里实现上一节讨论的算法。
# src/menu_generator.py import json import random from typing import List, Dict, Any from .recipe_loader import Recipe, load_recipes from .preference_manager import UserPreferences class MenuGenerator: def __init__(self, recipe_db: List[Recipe], user_prefs: UserPreferences): self.recipe_db = recipe_db self.user_prefs = user_prefs self.used_recipe_ids = set() self.ingredient_tracker = {} # 记录主要食材使用次数 def _filter_recipes(self, meal_type: str) -> List[Recipe]: """根据用户偏好和餐别过滤菜谱""" filtered = [] for recipe in self.recipe_db: # 1. 餐别过滤 if meal_type not in recipe.meal_type: continue # 2. 饮食禁忌过滤 if self._has_disliked_ingredients(recipe): continue # 3. 过敏原过滤 if self._has_allergens(recipe): continue # 4. 设备过滤 if not self._has_required_equipment(recipe): continue # 5. 饮食限制过滤(如素食) if not self._meets_dietary_restrictions(recipe): continue filtered.append(recipe) return filtered def _calculate_recipe_score(self, recipe: Recipe, meal_type: str, day_index: int) -> float: """计算单个菜谱的权重分数""" score = 10.0 # 基础分 # 偏好匹配加分 for cuisine in recipe.cuisine: if cuisine in self.user_prefs.preferred_cuisines: score += 5.0 for tag in recipe.tags: if tag in self.user_prefs.preferred_tags: score += 2.0 if tag in self.user_prefs.avoid_tags: score -= 4.0 # 避免的标签有负分 # 时间适应性:工作日优先快手菜 if day_index < 5: # 周一到周五 total_time = recipe.prep_time + recipe.cook_time if total_time < 30: score += 6.0 elif total_time < 45: score += 3.0 else: score -= 2.0 # 工作日长耗时菜惩罚 # 重复惩罚 if recipe.id in self.used_recipe_ids: score -= 100.0 # 极大惩罚,确保不重复 # 食材多样性惩罚 main_ingredient = self._get_main_ingredient(recipe) if main_ingredient: count = self.ingredient_tracker.get(main_ingredient, 0) if count >= 1: score -= count * 1.5 # 出现次数越多,惩罚越重 return score def _get_main_ingredient(self, recipe: Recipe) -> str: """启发式方法获取菜谱的主要食材(蛋白质来源)""" protein_categories = [“肉类”, “禽类”, “鱼类”, “海鲜”, “豆制品”, “蛋类”] for ingredient in recipe.ingredients: if ingredient.get(“category”) in protein_categories: return ingredient[“name”] # 如果没有明确蛋白质,返回第一个食材的类别或名称 if recipe.ingredients: return recipe.ingredients[0].get(“category”, “other”) return “other” def _weighted_random_choice(self, recipes: List[Recipe], scores: List[float]) -> Recipe: """根据权重分数随机选择一个菜谱""" # 将分数转换为正数,并做平滑处理(避免负分或零分) min_score = min(scores) if min_score <= 0: adjusted_scores = [s - min_score + 1 for s in scores] else: adjusted_scores = scores[:] total = sum(adjusted_scores) if total == 0: return random.choice(recipes) # 退化为完全随机 prob = [s / total for s in adjusted_scores] return random.choices(recipes, weights=prob, k=1)[0] def generate_for_day(self, day_index: int) -> Dict[str, Any]: """生成一天的菜单""" daily_menu = {} for meal_type in [“breakfast”, “lunch”, “dinner”]: candidates = self._filter_recipes(meal_type) if not candidates: daily_menu[meal_type] = None continue scores = [self._calculate_recipe_score(r, meal_type, day_index) for r in candidates] chosen_recipe = self._weighted_random_choice(candidates, scores) # 更新状态 self.used_recipe_ids.add(chosen_recipe.id) main_ingredient = self._get_main_ingredient(chosen_recipe) self.ingredient_tracker[main_ingredient] = self.ingredient_tracker.get(main_ingredient, 0) + 1 daily_menu[meal_type] = { “id”: chosen_recipe.id, “name”: chosen_recipe.name, “prep_time”: chosen_recipe.prep_time, “cook_time”: chosen_recipe.cook_time, “ingredients”: chosen_recipe.ingredients } return daily_menu def generate_weekly_menu(self) -> List[Dict[str, Any]]: """生成一周菜单""" weekly_menu = [] self.used_recipe_ids.clear() self.ingredient_tracker.clear() for day_idx in range(7): day_menu = self.generate_for_day(day_idx) weekly_menu.append({ “day_index”: day_idx, “day_name”: [“Monday”, “Tuesday”, “Wednesday”, “Thursday”, “Friday”, “Saturday”, “Sunday”][day_idx], “meals”: day_menu }) return weekly_menu4.3 购物清单生成模块
生成菜单后,我们需要遍历一周的食材,合并同类项,生成购物清单。这里的关键是食材的归一化处理。
# src/shopping_list.py from collections import defaultdict import json def load_ingredient_aliases(alias_path: str) -> Dict[str, str]: """加载食材别名映射,将不同名称映射到标准名""" with open(alias_path, ‘r’, encoding=‘utf-8’) as f: alias_data = json.load(f) alias_map = {} for standard_name, aliases in alias_data.items(): for alias in aliases: alias_map[alias.lower()] = standard_name return alias_map def normalize_ingredient_name(name: str, alias_map: Dict[str, str]) -> str: """将食材名称标准化""" lower_name = name.lower() return alias_map.get(lower_name, lower_name) # 如果找到别名就用标准名,否则用小写原名 def generate_shopping_list(weekly_menu: List[Dict], alias_map_path: str, servings_factor: float = 1.0) -> Dict: """ 根据周菜单生成购物清单 servings_factor: 份量调整因子,例如默认是2人份,如果1人吃则设为0.5 """ alias_map = load_ingredient_aliases(alias_map_path) shopping_dict = defaultdict(lambda: {“quantity”: 0.0, “unit”: “”, “recipes”: []}) for day in weekly_menu: for meal_type, meal in day[“meals”].items(): if not meal: continue for ingredient in meal[“ingredients”]: std_name = normalize_ingredient_name(ingredient[“name”], alias_map) unit = ingredient[“unit”] quantity = ingredient[“quantity”] * servings_factor # 如果购物字典中已有该食材,但单位不同,需要处理单位转换(这是一个复杂问题,此处简化) if std_name in shopping_dict and shopping_dict[std_name][“unit”] != unit: # 简单策略:如果单位不同,先不合并,作为新条目添加后缀 # 实际项目中需要实现单位换算表 new_key = f“{std_name} ({unit})” if new_key not in shopping_dict: shopping_dict[new_key] = {“quantity”: quantity, “unit”: unit, “recipes”: []} shopping_dict[new_key][“quantity”] += quantity shopping_dict[new_key][“recipes”].append(meal[“name”]) else: shopping_dict[std_name][“quantity”] += quantity shopping_dict[std_name][“unit”] = unit shopping_dict[std_name][“recipes”].append(meal[“name”]) # 按食材类别分类(需要菜谱数据中包含category信息) categorized_list = defaultdict(list) for ing_name, details in shopping_dict.items(): # 这里简化处理,实际应从原始菜谱数据中获取category # 假设我们有一个函数 get_category_by_name(ing_name) category = “其他” # 默认类别 categorized_list[category].append({ “name”: ing_name, “quantity”: round(details[“quantity”], 2), “unit”: details[“unit”], “used_in”: list(set(details[“recipes”]))[:3] # 显示用在哪些菜里,最多3个 }) return dict(categorized_list)4.4 命令行界面与输出
最后,我们创建一个简单的CLI入口,将以上模块串联起来。
# src/cli.py import json from datetime import datetime from .recipe_loader import load_recipes from .preference_manager import load_preferences from .menu_generator import MenuGenerator from .shopping_list import generate_shopping_list def main(): # 1. 加载数据 print(“正在加载菜谱数据...”) recipes = load_recipes(“data/recipes.json”) print(f”已加载 {len(recipes)} 道菜谱”) print(“正在加载用户偏好...”) user_prefs = load_preferences(“data/user_preferences.json”) # 2. 生成菜单 print(“\n正在为您生成本周菜单...”) generator = MenuGenerator(recipes, user_prefs) weekly_menu = generator.generate_weekly_menu() # 3. 打印菜单 print(“\n” + “=”*50) print(“本周菜单推荐”) print(“=”*50) for day in weekly_menu: print(f”\n{day[‘day_name’]}:”) for meal_type, meal in day[“meals”].items(): if meal: time_info = f”({meal[‘prep_time’]}+{meal[‘cook_time’]}min)” if meal[‘cook_time’] > 0 else “” print(f” {meal_type:10} {meal[‘name’]:20} {time_info}”) else: print(f” {meal_type:10} 暂无推荐”) # 4. 生成并打印购物清单 print(“\n” + “=”*50) print(“购物清单”) print(“=”*50) shopping_list = generate_shopping_list( weekly_menu, “data/ingredient_aliases.json”, servings_factor=user_prefs.default_servings / 2.0 # 假设基准菜谱是2人份 ) for category, items in shopping_list.items(): print(f”\n【{category}】”) for item in items: print(f” {item[‘name’]:15} {item[‘quantity’]:5} {item[‘unit’]:5} (用于: {‘, ‘.join(item[‘used_in’])})”) # 5. 保存结果到文件 output_data = { “generated_at”: datetime.now().isoformat(), “weekly_menu”: weekly_menu, “shopping_list”: shopping_list } output_file = f”outputs/menu_{datetime.now().strftime(‘%Y%m%d_%H%M%S’)}.json” with open(output_file, ‘w’, encoding=‘utf-8’) as f: json.dump(output_data, f, ensure_ascii=False, indent=2) print(f”\n菜单和购物清单已保存至: {output_file}”) if __name__ == “__main__”: main()运行这个CLI程序,你就能在控制台看到生成的周菜单和购物清单,同时结果会保存为一个JSON文件。
5. 常见问题、优化方向与避坑指南
在实际开发和使用的过程中,你肯定会遇到各种各样的问题。下面是我总结的一些典型问题和进阶优化思路。
5.1 数据质量与维护问题
问题1:菜谱数据太少,很快就重复了。
- 解决方案:这是初期最大的瓶颈。除了手动添加,可以考虑:
- 半自动采集:编写一个脚本,从你常看的菜谱网站或APP上,以“学习”为目的,将你感兴趣的菜谱手动保存时,脚本自动帮你提取标题、食材等关键信息,你只需要补充时间和标签。这比纯手动录入快得多。
- 数据交换:如果项目开源,可以设计一个贡献系统,让用户以标准格式提交他们验证过的菜谱。
- 聚焦核心:其实对于一个人或一个家庭,常吃的菜可能就50-100道。先把这些做好、做精,系统的价值就已经很大了。
问题2:食材用量单位混乱,无法自动合并。
- 避坑指南:这是实现购物清单自动合并的最大障碍。
- 前期规范:在创建菜谱数据库时,就强制使用一套标准单位(如固体用“克”,液体用“毫升”,调味品用“汤匙/茶匙”,个状物用“个”)。
- 单位换算表:建立一个
unit_conversion.json,定义常见换算关系,如{“汤匙”: {“毫升”: 15}, “杯”: {“毫升”: 240}}。在合并时,尝试将不同单位转换为一个“基础单位”后再相加。 - 接受不完美:对于无法换算的单位(如“1个土豆”和“200克土豆”),在购物清单中分开显示,并给出提示。这比强行合并导致数量错误要好。
问题3:生成的菜单不符合“中国胃”或本地饮食习惯。
- 解决方案:这完全取决于你的菜谱数据库和规则设计。在评分函数中,可以加入强大的文化或习惯规则:
- 主食搭配:在中餐中,午餐和晚餐通常需要有“主食”(米饭、面条、馒头)。可以在规则中要求每日至少一餐包含主食标签。
- 汤水:习惯喝汤的地区,可以设置规则“每周至少生成3道汤品”。
- 剩菜利用:聪明的菜单规划会考虑剩菜再利用。例如,周一烤了一只鸡,周二的菜谱可以设计为“鸡丝凉面”。这需要更复杂的、菜谱间有关联的数据模型。
5.2 生成逻辑的优化与个性化
问题4:算法总是推荐那几道高分菜,缺乏惊喜。
- 优化方向:这是我们采用“加权随机”而非“绝对最高分”的原因。可以进一步:
- 引入“探索”因子:为每一道菜增加一个“上次被推荐的时间”字段。距离现在越久,其权重获得一个额外的“探索加分”,让老菜谱有机会重新进入视野。
- 人工设置“黑名单”与“白名单”:允许用户将某些菜谱永久排除,或将某些最爱菜谱固定在某些日子(比如“每周五晚上吃饺子”)。
- 季节性调整:根据当前季节,为使用当季食材的菜谱加分。
问题5:无法处理现有食材。
- 进阶功能:这是项目从“生成”到“规划”的关键飞跃。实现思路:
- 让用户输入当前冰箱/ pantry 里已有的食材及其大致数量。
- 在生成菜单前,先扫描菜谱数据库,优先选择那些主要食材已被包含的菜谱。
- 生成购物清单时,从总需求中减去已有库存。
- 这需要更复杂的食材匹配逻辑(可能需要支持模糊匹配和分量判断)。
5.3 工程化与部署问题
问题6:想和家人共享,但CLI不方便。
- 解决方案:将项目改造成一个简单的Web应用。
- 后端:用 Flask 将上面的
MenuGenerator和ShoppingList类包装成 REST API 端点,例如POST /api/generate_menu。 - 前端:用一个简单的HTML页面,提供表单让用户设置偏好,点击按钮后调用API,将生成的菜单和清单美观地渲染在页面上,并提供导出功能。
- 部署:可以部署在家庭内部的NAS(如群晖Docker)上,或者使用 PythonAnywhere、Heroku 等免费平台进行小范围共享。
- 后端:用 Flask 将上面的
问题7:每次运行都是全新的随机菜单,无法微调。
- 实操技巧:在保存输出结果(JSON文件)时,同时保存本次生成所用的“随机种子”。这样,如果用户对本周菜单大体满意,只想调整其中一两道菜,可以加载这个种子文件,在保持其他日子不变的基础上,重新生成特定某一天的菜单。这比完全推倒重来体验好得多。
一个容易被忽略的细节:菜谱的“烹饪复杂度”平衡。除了时间,还要考虑“精力消耗”。一道需要全程看守、频繁翻炒的爆炒菜,和一道放进烤箱就可以不管的烤菜,即使时间相同,劳累程度也不同。可以在菜谱标签中加入effort字段(如“低”、“中”、“高”),并在生成规则中,避免在同一天安排两道“高精力”菜谱。这个细节能极大提升生成菜单的实际可执行性。
这个项目的魅力在于,它从一个简单的想法出发,可以随着你的需求和技术成长,不断深化和扩展。你可以始终让它保持为一个轻量级的本地脚本,也可以将其发展成一个功能丰富的个人生活管理工具。最重要的是,它真正解决了你生活中的一个具体问题,并且在这个过程中,你实践了数据处理、算法设计、软件工程等多个方面的技能。
