"""按中国食物成分表校准菜品营养成分。 原理:解析每道菜配料明细中的食材与克数,乘以对应食材营养密度(每 100g), 再按烹饪方式(炒/红烧/油炸/蒸煮)修正油脂与糖的用量,得到更符合实际的值。 用法:python manage.py calibrate_nutrition [--apply] [--export-seed] """ import re from django.core.management.base import BaseCommand from meals.models import Dish # 每 100g 可食部:蛋白质g, 脂肪g, 热量kcal(参考中国食物成分表/常见值) NUTRITION = { '五花肉': (9.5, 35.0, 400), '猪五花': (9.5, 35.0, 400), '梅花肉': (20.0, 8.0, 150), '里脊肉': (20.3, 6.2, 143), '瘦肉': (20.3, 6.2, 143), '肉丝': (20.0, 7.0, 150), '肉片': (20.0, 8.0, 155), '肉糜': (18.0, 12.0, 180), '肉馅': (18.0, 12.0, 180), '肉末': (18.0, 12.0, 180), '猪大排': (12.6, 12.6, 168), '大排': (12.6, 12.6, 168), '肋排': (12.8, 14.3, 185), '排骨': (12.8, 14.3, 185), '小排': (12.8, 14.3, 185), '汤骨': (15.0, 15.0, 220), '牛腩': (20.0, 8.0, 150), '牛肉': (20.2, 4.2, 125), '羊肉': (19.0, 14.1, 203), '鸡胸': (24.0, 1.5, 118), '鸡丁': (19.0, 6.0, 140), '鸡块': (19.0, 8.0, 150), '鸡肉': (19.3, 9.4, 167), '鸡腿': (14.0, 9.0, 150), '鸡翅': (17.4, 11.8, 194), '烤翅': (19.0, 16.0, 240), '鸭腿': (16.0, 18.0, 230), '鸭块': (15.5, 19.7, 240), '鸭肉': (15.5, 19.7, 240), '鱼片': (17.0, 4.0, 110), '鱼块': (16.6, 5.2, 113), '草鱼': (16.6, 5.2, 113), '鲈鱼': (18.6, 3.4, 105), '鲫鱼': (17.1, 2.7, 108), '基围虾': (18.6, 0.8, 93), '虾仁': (18.6, 0.8, 93), '虾皮': (30.7, 2.2, 153), '鸡蛋': (13.3, 8.8, 144), '咸蛋黄': (16.0, 30.0, 330), '皮蛋': (14.2, 10.7, 171), '豆腐': (8.1, 3.7, 81), '素鸡': (16.5, 12.5, 192), '烤麸': (22.5, 0.7, 121), '百叶结': (24.5, 9.0, 205), '油面筋': (17.1, 17.6, 244), '油方': (17.1, 17.6, 244), '腐竹': (44.6, 21.7, 459), '粉丝': (0.8, 0.2, 338), '花生米': (24.1, 44.4, 574), '花生': (24.1, 44.4, 574), '贡丸': (12.0, 12.0, 200), '红肠': (12.0, 15.0, 220), '烤肠': (10.0, 20.0, 230), '土豆': (2.0, 0.2, 77), '红薯': (1.1, 0.2, 90), '南瓜': (0.7, 0.1, 23), '山药': (1.9, 0.2, 57), '玉米棒': (4.0, 1.2, 112), '玉米': (4.0, 1.2, 112), '冬瓜': (0.4, 0.2, 12), '西葫芦': (0.8, 0.2, 19), '丝瓜': (1.0, 0.2, 20), '番茄': (0.9, 0.2, 20), '西红柿': (0.9, 0.2, 20), '茄子': (1.1, 0.2, 23), '青椒': (1.0, 0.2, 22), '辣椒': (1.3, 0.4, 32), '干辣椒': (15.0, 12.0, 212), '彩椒': (1.0, 0.2, 22), '大白菜': (1.5, 0.1, 17), '白菜': (1.5, 0.1, 17), '卷心菜': (1.5, 0.2, 22), '牛心菜': (1.5, 0.2, 22), '包菜': (1.5, 0.2, 24), '娃娃菜': (1.7, 0.3, 15), '青菜': (1.7, 0.3, 15), '菠菜': (2.6, 0.3, 28), '空心菜': (2.2, 0.3, 20), '菜心': (2.8, 0.5, 22), '生菜': (1.3, 0.3, 15), '杭白菜': (1.8, 0.4, 18), '茼蒿菜': (1.9, 0.3, 24), '茼蒿': (1.9, 0.3, 24), '莴笋': (1.0, 0.1, 15), '芦笋': (2.6, 0.2, 22), '西兰花': (4.1, 0.6, 36), '莲藕': (1.9, 0.2, 73), '藕片': (1.9, 0.2, 73), '藕丁': (1.9, 0.2, 73), '茭白': (1.2, 0.2, 26), '蚕豆': (8.8, 0.4, 104), '荷兰豆': (2.5, 0.3, 30), '四季豆': (2.0, 0.4, 31), '豆芽': (2.1, 0.1, 18), '香菇': (2.2, 0.3, 26), '菌菇': (2.5, 0.3, 25), '木耳': (1.5, 0.2, 27), '紫菜': (26.7, 1.1, 250), '海带': (1.2, 0.1, 13), '黄瓜': (0.8, 0.2, 16), '洋葱': (1.1, 0.2, 40), '大葱': (1.7, 0.3, 30), '韭菜': (2.4, 0.4, 26), '胡萝卜': (1.0, 0.2, 39), '马蹄': (1.2, 0.2, 61), '荸荠': (1.2, 0.2, 61), '酸菜': (1.2, 0.2, 18), '蒜苗': (2.1, 0.4, 37), '豆豉': (24.0, 8.0, 220), '葱油': (0.0, 100.0, 900), '笋丝': (2.6, 0.2, 25), '笋': (2.6, 0.2, 25), '芹菜': (1.2, 0.2, 22), '红枣': (3.2, 0.5, 276), '剁椒': (2.0, 1.0, 40), '荸荠': (1.2, 0.2, 61), '米饭': (2.6, 0.3, 116), '黑米饭': (3.2, 0.6, 122), '麦片饭': (4.0, 1.2, 130), '杂粮饭': (3.5, 0.8, 125), '炒面': (4.5, 6.5, 175), '豆沙包': (8.5, 3.0, 220), '奶黄包': (7.0, 4.0, 200), '小笼包': (9.0, 5.0, 220), '花卷': (7.0, 1.0, 220), '馒头': (7.0, 1.0, 220), } # 非克单位 → 每单位可食部克数 UNIT_GRAMS = { '只': {'鸡翅': 30, '烤翅': 35, '鸡蛋': 50, '蛋': 50, '基围虾': 10, '虾': 10, '鸡腿': 100, '鸭腿': 100, '油面筋': 20, '油方': 20, '小笼包': 25}, '根': {'烤肠': 50, '火腿肠': 50}, '块': {'大排': 70, '排骨': 100, '鱼块': 100, '鸡块': 35, '素鸡': 50, '豆腐': 100}, } UNIT_DEFAULT = {'只': 30, '根': 50, '块': 80} # 烹饪方式修正(额外油/糖,每份):加 蛋白质g、脂肪g、热量kcal COOK_OIL = [ (('油炸', '炸'), (0.0, 12.0, 108.0)), (('地三鲜', '水煮', '回锅'), (0.0, 8.0, 72.0)), (('红烧', '糖醋', '蜜汁', '拔丝', '可乐', '鱼香', '酱爆', '酱鸭', '东坡', '焖', '油焖'), (0.0, 5.0, 80.0)), (('炒', '爆', '干煸', '蒜香', '烤', '焗'), (0.0, 5.0, 45.0)), (('蒸', '炖', '煮', '白灼', '凉拌', '汤', '清炒', '蒜蓉'), (0.0, 2.0, 18.0)), (('糖拌',), (0.0, 0.0, 45.0)), (('塞肉',), (4.5, 3.0, 45.0)), ] # 类型合理范围 clamp:(protein, fat, kcal) CLAMP = { 'meat': ((4.0, 20.0), (2.0, 28.0), (90.0, 340.0)), 'veg': ((1.0, 10.0), (0.5, 12.0), (35.0, 170.0)), 'soup': ((1.5, 7.0), (1.0, 7.0), (30.0, 100.0)), 'staple': ((2.0, 9.0), (0.5, 10.0), (70.0, 320.0)), } def _parse_items(text): """解析 '食材12g食材34g' / '食材2只' → [(食材名, 克数)]""" items = [] for m in re.finditer(r'([\u4e00-\u9fa5]{1,10}?)(\d+\.?\d*)g', text): items.append((m.group(1), float(m.group(2)))) for m in re.finditer(r'([\u4e00-\u9fa5]{1,10}?)(\d+)(?:-(\d+))?(只|根|块)', text): name, unit = m.group(1), m.group(4) n = int(m.group(3) or m.group(2)) grams = UNIT_GRAMS.get(unit, {}).get(name) if grams is None: grams = next((v for k, v in UNIT_GRAMS.get(unit, {}).items() if k in name), UNIT_DEFAULT[unit]) items.append((name, grams * n)) return items def _food_nutrition(name): for key in sorted(NUTRITION, key=len, reverse=True): if key in name: return NUTRITION[key] return None def _cook_fix(name): for keywords, fix in COOK_OIL: for k in keywords: if k in name: return fix return (0.0, 3.0, 27.0) def _calc(name, dish_type, text): items = _parse_items(text) protein = fat = kcal = 0.0 matched = 0 for food, grams in items: n = _food_nutrition(food) if n: matched += 1 p, f, k = n protein += p * grams / 100 fat += f * grams / 100 kcal += k * grams / 100 if not matched: return None if dish_type != 'staple': fix = _cook_fix(name) fat += fix[1] kcal += fix[2] (p_min, p_max), (f_min, f_max), (k_min, k_max) = CLAMP[dish_type] protein = min(max(protein, p_min), p_max) fat = min(max(fat, f_min), f_max) kcal = min(max(round(kcal), int(k_min)), int(k_max)) return round(protein, 1), round(fat, 1), int(kcal) class Command(BaseCommand): help = '按食物成分表校准菜品营养成分(--apply 写入数据库,--export-seed 重新生成 seed_dishes.py)' def add_arguments(self, parser): parser.add_argument('--apply', action='store_true', help='写入数据库(默认仅预览)') parser.add_argument('--export-seed', action='store_true', help='重新生成 seed_dishes.py') def handle(self, *args, **options): apply_changes = options['apply'] updated, unchanged, skipped = [], [], [] rows = [] for dish in Dish.objects.filter(is_active=True).order_by('dish_type', 'cuisine', 'id'): result = _calc(dish.name, dish.dish_type, dish.ingredient_detail or '') if result is None: skipped.append(dish.name) continue p, f, k = result old = (float(dish.protein), float(dish.fat), int(dish.calorie)) rows.append((dish, old, result)) if abs(old[0] - p) < 0.05 and abs(old[1] - f) < 0.05 and old[2] == k: unchanged.append(dish.name) continue updated.append(dish.name) self.stdout.write( f'{dish.name}: 蛋白 {old[0]}→{p} | 脂肪 {old[1]}→{f} | 热量 {old[2]}→{k}' ) self.stdout.write(self.style.WARNING(f'\n将更新 {len(updated)} 道,不变 {len(unchanged)} 道,无法解析 {len(skipped)} 道')) if skipped: self.stdout.write('无法解析(保留原值): ' + '、'.join(skipped)) if apply_changes: for dish, _, (p, f, k) in rows: dish.protein, dish.fat, dish.calorie = p, f, k dish.save(update_fields=['protein', 'fat', 'calorie']) self.stdout.write(self.style.SUCCESS('已写入数据库')) if options['export_seed']: self._export_seed(rows, skipped) def _export_seed(self, rows, skipped): path = __file__.replace('calibrate_nutrition.py', 'seed_dishes.py') grouped = {} for dish, _, (p, f, k) in rows: grouped.setdefault(dish.dish_type, []).append(dish) lines = [] lines.append('from django.core.management.base import BaseCommand') lines.append('') lines.append('from meals.models import Dish') lines.append('') lines.append('# (菜名, 菜系, 配料明细, 蛋白质g, 脂肪g, 热量kcal)') lines.append('SAMPLE_DISHES = {') for t in ('meat', 'veg', 'soup', 'staple'): dishes = [d for d in grouped.get(t, []) if d.name not in skipped] lines.append(f" '{t}': [") for d in dishes: lines.append( f" ('{d.name}', '{d.cuisine}', '{d.ingredient_detail}', {d.protein}, {d.fat}, {d.calorie})," ) lines.append(' ],') lines.append('}') tail = ''' class Command(BaseCommand): help = '写入示例菜品数据(含菜系)' def handle(self, *args, **options): count = 0 for dish_type, items in SAMPLE_DISHES.items(): for name, cuisine, ingredient, protein, fat, calorie in items: _, created = Dish.objects.update_or_create( name=name, defaults={ 'dish_type': dish_type, 'cuisine': cuisine, 'ingredient_detail': ingredient, 'protein': protein, 'fat': fat, 'calorie': calorie, }, ) if created: count += 1 self.stdout.write(self.style.SUCCESS(f'示例菜品已就绪(新增 {count} 道,库中共 {Dish.objects.count()} 道)')) ''' with open(path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines) + tail) self.stdout.write(self.style.SUCCESS(f'seed_dishes.py 已重新生成:{path}'))