文部科学省の食品成分データベースを活用した栄養素の自動計算機能

## はじめに

レシピ管理システムを作る中で、栄養素の計算機能を実装しました。以前はGoogle Translation APIとUSDA APIを使用していましたが、日本特有の食材や単位に対応するため、文部科学省の食品成分データベースを活用することにしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918624/eb4ea315-61aa-406b-9117-5ab4a28b14e3.gif" width=100%>

以前作成した際の記事がこちらです

https://qiita.com/HayatoOkamura0000/items/0e7a4acb77d02f1747a1

※記事の作成には生成AIを活用しています

## 目次

- [完成品](#完成品)
- [文部科学省の食品成分データベースとは](#文部科学省の食品成分データベースとは)
- [データベースのJSON変換](#データベースのjson変換)
- [実装の概要](#実装の概要)
- [具材の検索と選択機能の実装](#具材の検索と選択機能の実装)
- [まとめ](#まとめ)

## 完成品
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918624/e069ba49-9695-4405-a17f-edc3da028c40.png" width=49%>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918624/6119d82d-b5cd-4d19-bcfa-2d3dbdcdb951.png" width=49%>



## 文部科学省の食品成分データベースとは

文部科学省が提供する「日本食品標準成分表2020年版（八訂）」のデータベースを使用しています。このデータベースには以下の特徴があります

- 日本で一般的に使用される食品の成分データ
- 100gあたりの栄養成分値
- 日本特有の単位（個、本、枚など）に対応
- 定期的に更新される信頼性の高いデータ


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918624/74607c24-5221-4cb6-8aa0-347d8163c50d.png)

### データベースの入手方法

1. [文部科学省のウェブサイト](https://www.mext.go.jp/a_menu/syokuhinseibun/mext_01110.html)にアクセス
2. 「日本食品標準成分表2020年版（八訂）」のExcelファイルをダウンロード
3. ダウンロードしたファイルを解凍し、`food_composition_table.xlsx`として保存

## データベースのJSON変換

文部科学省のデータベースはExcelファイルで提供されているため、Pythonを使用してJSONファイルに変換します。

### 必要なPythonパッケージのインストール

```bash
pip install pandas openpyxl
```

### 変換スクリプトの作成

`convert_food_data.py`というファイルを作成し、以下のコードを実装します

```python
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()
```

### スクリプトの実行

```bash
python convert_food_data.py
```

### 変換後のJSONファイルの構造

```json
[
  {
    "id": "01001",
    "name": "米",
    "category": "穀類",
    "nutrition": {
      "calories": 356.0,
      "protein": 6.1,
      "fat": 0.9,
      "carbohydrates": 77.1,
      "salt": 0.0
    }
  }
]
```

## 実装の概要

### 必要なパッケージのインストール

```bash
npm install @types/node typescript
```

### 型定義の作成

`types/food.ts`ファイルを作成し、以下の型定義を実装します

```typescript
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`ファイルを作成し、以下のコードを実装します

```typescript
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`を作成します

```typescript
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`を作成します

```typescript
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`で使用する例

```typescript
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>
  );
};
```

### 使用方法

1. 具材の検索ボックスに食材名を入力
2. 候補が表示されるので、該当する具材をクリック
3. 具材がリストに追加される
4. 量と単位を入力
5. 必要に応じて具材を削除

これにより、ユーザーは簡単に具材を検索して選択し、栄養素を自動的に取得することができます。

## まとめ

文部科学省の食品成分データベースを活用することで、以下のメリットが得られました

- 日本特有の食材や単位に対応
- 信頼性の高い栄養成分データ
- 正確な栄養計算が可能
- 外部APIへの依存が不要
- データの更新が容易（ExcelファイルをJSONに変換するだけ）

## 参考リンク

[文部科学省 日本食品標準成分表2020年版（八訂）](https://www.mext.go.jp/a_menu/syokuhinseibun/mext_01110.html)