はじめに
レシピ管理システムを作る中で、栄養素の計算機能を実装しました。以前はGoogle Translation APIとUSDA APIを使用していましたが、日本特有の食材や単位に対応するため、文部科学省の食品成分データベースを活用することにしました。
以前作成した際の記事がこちらです
※記事の作成には生成AIを活用しています
目次
文部科学省の食品成分データベースとは
文部科学省が提供する「日本食品標準成分表2020年版(八訂)」のデータベースを使用しています。このデータベースには以下の特徴があります
- 日本で一般的に使用される食品の成分データ
- 100gあたりの栄養成分値
- 日本特有の単位(個、本、枚など)に対応
- 定期的に更新される信頼性の高いデータ
データベースの入手方法
- 文部科学省のウェブサイトにアクセス
- 「日本食品標準成分表2020年版(八訂)」のExcelファイルをダウンロード
- ダウンロードしたファイルを解凍し、
food_composition_table.xlsx
として保存
データベースのJSON変換
文部科学省のデータベースはExcelファイルで提供されているため、Pythonを使用してJSONファイルに変換します。
必要なPythonパッケージのインストール
pip install pandas openpyxl
変換スクリプトの作成
convert_food_data.py
というファイルを作成し、以下のコードを実装します
import pandas as pd
import json
from pathlib import Path
def convert_excel_to_json():
# 入力ファイルと出力ファイルのパスを設定
input_file = Path('food_composition_table.xlsx')
output_file = Path('food_composition.json')
# Excelファイルを読み込む
print(f"Excelファイルを読み込んでいます: {input_file}")
df = pd.read_excel(input_file)
# 必要な列を選択
foods = []
for _, row in df.iterrows():
try:
food = {
'id': str(row['食品番号']),
'name': row['食品名'],
'category': row['分類名'],
'nutrition': {
'calories': float(row['エネルギー(kcal)']),
'protein': float(row['たんぱく質(g)']),
'fat': float(row['脂質(g)']),
'carbohydrates': float(row['炭水化物(g)']),
'salt': float(row['食塩相当量(g)'])
}
}
foods.append(food)
except (ValueError, KeyError) as e:
print(f"警告: 行の処理中にエラーが発生しました: {e}")
continue
# JSONファイルに保存
print(f"JSONファイルに保存しています: {output_file}")
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(foods, f, ensure_ascii=False, indent=2)
print(f"変換が完了しました。{len(foods)}件の食品データを処理しました。")
if __name__ == '__main__':
convert_excel_to_json()
スクリプトの実行
python convert_food_data.py
変換後のJSONファイルの構造
[
{
"id": "01001",
"name": "米",
"category": "穀類",
"nutrition": {
"calories": 356.0,
"protein": 6.1,
"fat": 0.9,
"carbohydrates": 77.1,
"salt": 0.0
}
}
]
実装の概要
必要なパッケージのインストール
npm install @types/node typescript
型定義の作成
types/food.ts
ファイルを作成し、以下の型定義を実装します
export interface Nutrition {
calories: number; // カロリー(kcal)
protein: number; // タンパク質(g)
fat: number; // 脂質(g)
carbohydrates: number; // 炭水化物(g)
salt: number; // 塩分(g)
}
export interface Food {
id: string;
name: string;
category: string;
nutrition: Nutrition;
}
export interface Unit {
id: number;
name: string;
description: string;
step: number;
}
export interface IngredientWithNutrition {
id: string;
name: string;
quantity: number;
unit: Unit;
nutrition: Nutrition;
}
栄養素計算ユーティリティの作成
utils/nutritionCalculator.ts
ファイルを作成し、以下のコードを実装します
import { Nutrition, IngredientWithNutrition } from '../types/food';
export const convertToGrams = (quantity: number, unit: Unit): number => {
switch (unit.name) {
// 個数系単位
case '個':
case '本':
case '房':
case '株':
case '袋':
case '缶':
case '匹':
case '尾':
case 'パック':
return quantity * 100;
case '枚':
return quantity * 20;
case '切れ':
return quantity * 50;
// 調味料系単位
case '小さじ':
return quantity * 5;
case '大さじ':
return quantity * 15;
case 'カップ':
return quantity * 200;
// 質量・容量単位
case 'kg':
return quantity * 1000;
case 'ml':
return quantity;
case 'L':
return quantity * 1000;
// その他
case '滴':
return quantity;
case '適量':
case '少々':
return 0;
// すでにgの場合はそのまま
case 'g':
return quantity;
default:
console.warn(`未対応の単位: ${unit.name}、gとして計算します`);
return quantity;
}
};
export const calculateNutrition = (ingredients: IngredientWithNutrition[]): Nutrition => {
const nutrition: Nutrition = {
calories: 0,
protein: 0,
fat: 0,
carbohydrates: 0,
salt: 0,
};
ingredients.forEach((ing) => {
if (ing.nutrition) {
const quantityInGrams = convertToGrams(ing.quantity, ing.unit);
const ratio = quantityInGrams / 100;
// 各栄養素を計算して合計
nutrition.calories += Math.floor(ing.nutrition.calories * ratio);
nutrition.protein += Number((ing.nutrition.protein * ratio).toFixed(1));
nutrition.fat += Number((ing.nutrition.fat * ratio).toFixed(1));
nutrition.carbohydrates += Number((ing.nutrition.carbohydrates * ratio).toFixed(1));
nutrition.salt += Number((ing.nutrition.salt * ratio).toFixed(2));
}
});
return nutrition;
};
具材の検索と選択機能の実装
コンポーネントの作成
IngredientSearch.tsx
を作成します
import { useState, useEffect } from 'react';
import { Food } from '@/types/food';
import styles from './IngredientSearch.module.scss';
interface IngredientSearchProps {
onSelect: (food: Food) => void;
}
export const IngredientSearch = ({ onSelect }: IngredientSearchProps) => {
const [searchTerm, setSearchTerm] = useState('');
const [foods, setFoods] = useState<Food[]>([]);
const [filteredFoods, setFilteredFoods] = useState<Food[]>([]);
const [isLoading, setIsLoading] = useState(false);
// 食品データの読み込み
useEffect(() => {
const loadFoods = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/foods');
const data = await response.json();
setFoods(data);
} catch (error) {
console.error('食品データの読み込みに失敗しました:', error);
} finally {
setIsLoading(false);
}
};
loadFoods();
}, []);
// 検索処理
useEffect(() => {
if (!searchTerm) {
setFilteredFoods([]);
return;
}
const filtered = foods.filter(food =>
food.name.includes(searchTerm) ||
food.category.includes(searchTerm)
);
setFilteredFoods(filtered.slice(0, 10)); // 最大10件まで表示
}, [searchTerm, foods]);
return (
<div className={styles.container}>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="具材を検索..."
className={styles.searchInput}
/>
{isLoading && <div className={styles.loading}>読み込み中...</div>}
{filteredFoods.length > 0 && (
<ul className={styles.results}>
{filteredFoods.map((food) => (
<li
key={food.id}
onClick={() => {
onSelect(food);
setSearchTerm('');
setFilteredFoods([]);
}}
className={styles.resultItem}
>
<span className={styles.name}>{food.name}</span>
<span className={styles.category}>{food.category}</span>
<div className={styles.nutrition}>
<span>カロリー: {food.nutrition.calories}kcal</span>
<span>タンパク質: {food.nutrition.protein}g</span>
</div>
</li>
))}
</ul>
)}
</div>
);
};
APIエンドポイントの作成
pages/api/foods.ts
を作成します
import { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs';
import path from 'path';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const filePath = path.join(process.cwd(), 'public', 'food_composition.json');
const fileContents = fs.readFileSync(filePath, 'utf8');
const foods = JSON.parse(fileContents);
res.status(200).json(foods);
} catch (error) {
console.error('食品データの読み込みに失敗しました:', error);
res.status(500).json({ error: '食品データの読み込みに失敗しました' });
}
}
レシピフォームでの使用例
components/features/RecipeForm/RecipeForm.tsx
で使用する例
import { IngredientSearch } from '../IngredientSearch/IngredientSearch';
import { Food } from '@/types/food';
export const RecipeForm = () => {
const [ingredients, setIngredients] = useState<Food[]>([]);
const handleIngredientSelect = (food: Food) => {
setIngredients(prev => [...prev, food]);
};
return (
<div>
<h2>具材の追加</h2>
<IngredientSearch onSelect={handleIngredientSelect} />
<div className={styles.ingredientsList}>
{ingredients.map((ingredient, index) => (
<div key={index} className={styles.ingredientItem}>
<span>{ingredient.name}</span>
<input
type="number"
min="0"
step="0.1"
placeholder="量"
/>
<select>
<option value="g">g</option>
<option value="個">個</option>
<option value="本">本</option>
{/* 他の単位も追加 */}
</select>
<button onClick={() => {
setIngredients(prev => prev.filter((_, i) => i !== index));
}}>
削除
</button>
</div>
))}
</div>
</div>
);
};
使用方法
- 具材の検索ボックスに食材名を入力
- 候補が表示されるので、該当する具材をクリック
- 具材がリストに追加される
- 量と単位を入力
- 必要に応じて具材を削除
これにより、ユーザーは簡単に具材を検索して選択し、栄養素を自動的に取得することができます。
まとめ
文部科学省の食品成分データベースを活用することで、以下のメリットが得られました
- 日本特有の食材や単位に対応
- 信頼性の高い栄養成分データ
- 正確な栄養計算が可能
- 外部APIへの依存が不要
- データの更新が容易(ExcelファイルをJSONに変換するだけ)