2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-05-08

はじめに

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

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

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

目次

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

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

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

データベースの入手方法

  1. 文部科学省のウェブサイトにアクセス
  2. 「日本食品標準成分表2020年版(八訂)」のExcelファイルをダウンロード
  3. ダウンロードしたファイルを解凍し、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>
  );
};

使用方法

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

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

まとめ

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

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

参考リンク

文部科学省 日本食品標準成分表2020年版(八訂)

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?