この記事は 日本システム技研 - Qiita Advent Calendar 2025 - Qiita 18日目の記事です!
導入
この記事のコードは全てAIに書かせています!
みなさん、たまにこういうことはないでしょうか?
あー、これもう少し便利にならないかなーって思うことが。
(ここから語りが入りますが、この段落読み飛ばしてもらって大丈夫です)
私の日常の出来事です。夕飯を作るのに何を作ろうか思いつかず、日課の DELISH KITCHEN を眺めていました。
お、良いのがあったぞとその料理を作るための材料を確認しました。
そこには「2人分」と書かれています。だいたいのものはそうです。当たり前です。
でも私が知りたいのは「2人分」でも「4人分」でもなく「3人分」なのです。
作る時に1.5倍にして計算すれば良いのはわかります。
ですが、2倍ならともかく1.5倍の計算ってめんどくさい…。
~閑話休題~
ここで天啓を得ました。AIに何か作ってもらおうと。
(AIに聞けば計算してくれると思うのですが、そこは置いておいてですね。)
基本面倒くさがりなので、細かいことは考えたくない。
ということで、やりたいことを伝えればいい感じにしてくれるのか。
やってみました。
実践
AIに頼む前に
いざ、AIに頼む!としたいのですが、どう聞けばいいでしょう?
とりあえず、最終的にどんなものが欲しいか考えてまとめてみます。
今回はとりあえずスクリプトを書いてもらうことにしました。
スクリプトさえあれば色々なことに使いまわせますしね!
ということで主旨はこれです。
DELISH KITCHENのレシピについて、分量を人数で再計算し直すスクリプトをPythonで作成したい
これだけだと伝わるか怪しいので、補足も書いておこうかなと思います。
やりたいこと
- 例えば2人分の材料で書かれている材料の分量を3人分の材料ではどのくらいの分量になるか計算したい
あとは、思いついたこうであって欲しいということを書いておきましょう。
条件
- DELISH KITCHENのレシピに書かれている分量をコピペして貼り付けて動作するようにしたい
- 小さじ3=大さじ1として記述して欲しい(例:小さじ4であれば大さじ1と小さじ1)
- 小数点ではなく4/1までの分数で書いて欲しい(玉ねぎ中1個→1個と1/2個)
完璧ですね😏
(こんな適当な感じで大丈夫かと内心思ってはいます。)
AIに頼んでみる
今回はGeminiに頼んでみます!
(※回答は要約して書いています。)
👩💻 私:
DELISH KITCHENのレシピについて、分量を人数で再計算し直すスクリプトをPythonで作成したい
やりたいこと
・例えば2人分の材料で書かれている材料の分量を3人分の材料ではどのくらいの分量になるか計算したい条件
・DELISH KITCHENのレシピに書かれている分量をコピペして貼り付けて動作するようにしたい
・小さじ3=大さじ1として記述して欲しい(例:小さじ4であれば大さじ1と小さじ1)
・小数点ではなく4/1までの分数で書いて欲しい(玉ねぎ中1個→1個と1/2個)
🤖 Gemini:
お任せください。DELISH KITCHENのレシピの分量を、指定された条件で人数に合わせて再計算するPythonスクリプトを作成します。
このスクリプトでは、以下の処理を行います。
- 分量の読み取りと目標人数の設定: 現在の分量が何人分か、そして何人分にしたいかをユーザーに入力してもらいます。
- 分量の抽出と単位の正規化: 入力されたレシピテキストから、「数字」「単位(小さじ、大さじ、ml、g、個など)」を分離します。
- 分量の再計算: 目標人数と現在の人数から計算し、結果を分数表記(4分の1まで)にします。
- 単位変換の適用: 「小さじ3 = 大さじ1」のルールを適用して、分量を整形します。
出力されたコード(長いので折りたたんでおきます!)
import re
from fractions import Fraction
from decimal import Decimal, getcontext
# Decimalの精度を設定(計算誤差を減らすため)
getcontext().prec = 50
# 単位のグループ化と正規表現パターン
UNIT_PATTERNS = {
# 容量/重量系 (小さじ/大さじは特別処理)
'volume_weight': r'(?:ml|g|cc|L|kg|カップ|合|袋|個|本|枚|房|束|cm|mm|株|かけ|パック|缶|びん|瓶|ブロック|切れ|つ|粒|滴|Dash|適量|少々)',
# 小さじ・大さじ
'spoon': r'(?:小さじ|大さじ|tsp|tbsp)',
}
# 小さじと大さじの変換係数
TSP_TO_TBSP = 3 # 小さじ3 = 大さじ1
def convert_to_fraction_string(value, unit_str=''):
"""
数値を「整数 + 1/4までの分数」の文字列に変換する。
例: 1.5 -> '1と1/2'、 0.33 -> '1/3' (近似)
"""
if value == 0:
return '0'
# 小数点以下が0.25, 0.5, 0.75に近いか確認
decimal_part = value - int(value)
# 小数部分を分数に変換(分母は4までを優先)
if decimal_part >= 0.99:
integer_part = int(value) + 1
fraction_part_str = ''
elif decimal_part >= 0.745:
integer_part = int(value)
fraction_part_str = '3/4'
elif decimal_part >= 0.495:
integer_part = int(value)
fraction_part_str = '1/2'
elif decimal_part >= 0.245:
integer_part = int(value)
fraction_part_str = '1/4'
else:
# それ以外は、一旦最も近い分数に変換を試みる(今回は簡易処理として一旦は上記で対応)
# 1/3, 2/3などにも対応するにはより複雑な処理が必要だが、今回は指定の1/4までで対応する
if decimal_part < 0.005:
integer_part = int(value)
fraction_part_str = ''
else:
# 1/3, 2/3のような一般的な分数に対応させるための処理(今回は指定外だが実用性のため追加)
# Fractionオブジェクトを使って、近い分数を探す
frac = Fraction(Decimal(decimal_part)).limit_denominator(4) # 分母4までを優先
# 分母が4で表現できない場合は、そのまま小数点以下2桁までで表現する(例:1/3は0.33)
if frac.denominator > 4 and unit_str not in UNIT_PATTERNS['spoon']:
# 小数点以下1桁までで表示 (小さじ/大さじ以外)
return f"{value:.1f}".rstrip('0').rstrip('.')
integer_part = int(value)
fraction_part_str = f"{frac.numerator}/{frac.denominator}"
if frac.numerator == 0:
fraction_part_str = ''
result = []
if integer_part > 0:
result.append(str(integer_part))
if fraction_part_str:
result.append('と')
if fraction_part_str:
if integer_part > 0 and 'と' in result:
# 'と' の後に分数
result.append(fraction_part_str)
elif integer_part == 0:
# 整数部分が0なら分数のみ
result.append(fraction_part_str)
else:
# 整数部分があるが、上記で 'と' が挿入されていない場合
pass
if not result and value != 0:
# 万が一何も入らなかった場合(例:非常に小さな値)
return f"{value:.2f}"
return "".join(result).replace('と1/1', 'と1').replace('と0', '').replace('1/1', '1') # 1/1は1に変換
def convert_tsp_to_tbsp(tsp_amount):
"""
小さじの量を「大さじXと小さじY」に変換し、分数文字列で返す。
小さじ3 = 大さじ1
"""
if tsp_amount == 0:
return '0', '小さじ'
# 大さじの量
tbsp = tsp_amount // TSP_TO_TBSP
# 残りの小さじの量
remaining_tsp = tsp_amount % TSP_TO_TBSP
result = []
# 大さじの表示
if tbsp > 0:
tbsp_frac_str = convert_to_fraction_string(tbsp)
result.append(f"{tbsp_frac_str}大さじ")
# 残りの小さじの表示
if remaining_tsp > 0:
tsp_frac_str = convert_to_fraction_string(remaining_tsp)
result.append(f"{tsp_frac_str}小さじ")
if not result:
# 0の場合
return '0', '小さじ'
return 'と'.join(result), '' # 単位は空にしておく(文字列内で表現済みのため)
def recalculate_recipe(recipe_text, current_servings, target_servings):
"""
レシピの分量を人数に合わせて再計算するメイン関数。
"""
if current_servings <= 0 or target_servings <= 0:
return "エラー: 人数は正の数である必要があります。"
# 計算比率
ratio = Decimal(target_servings) / Decimal(current_servings)
# 結果格納用リスト
recalculated_lines = []
# 入力テキストを改行で分割
lines = recipe_text.strip().split('\n')
# レシピ項目ごとに処理
for line in lines:
if not line.strip():
continue
# 小さじ/大さじを含む行の処理
if re.search(UNIT_PATTERNS['spoon'], line):
# 小さじを基本単位として全て抽出・計算する
# (数) + (小さじ or 大さじ) のパターンを抽出
# 整数, 小数, 分数(1/2, 1/3など) のパターンに対応
spoon_regex = r'([\d\.\/]+)\s*(大さじ|小さじ|tbsp|tsp)'
# 小さじ換算での合計量(Decimal)
total_tsp_amount = Decimal(0)
# 置換対象を保持するためのリスト
replacements = []
for match in re.finditer(spoon_regex, line):
amount_str, unit_str = match.groups()
amount = 0
try:
if '/' in amount_str:
# 分数の場合 (例: 1/2)
num, den = map(int, amount_str.split('/'))
amount = Decimal(num) / Decimal(den)
else:
# 整数・少数の場合
amount = Decimal(amount_str)
except Exception:
# 分量としてパースできない場合はスキップ
continue
# 小さじ換算に変換
if unit_str in ['大さじ', 'tbsp']:
total_tsp_amount += amount * Decimal(TSP_TO_TBSP)
elif unit_str in ['小さじ', 'tsp']:
total_tsp_amount += amount
# 置換対象として元の文字列と開始・終了位置を保存
replacements.append((match.start(), match.end(), match.group()))
if total_tsp_amount > 0:
# 合計小さじ量を再計算
new_total_tsp_amount = total_tsp_amount * ratio
# 新しい大さじ/小さじの文字列を生成
new_spoon_str, _ = convert_tsp_to_tbsp(new_total_tsp_amount)
# 元の行から、最初に見つかった「数+単位」の部分を再計算結果に置き換える
# 複雑な複数表記は簡易化のため、最初の分量表記のみ置き換え、残りは削除
# 置換対象をソートして、最初に見つかったものだけを置き換える
replacements.sort(key=lambda x: x[0])
if replacements:
start, end, original_text = replacements[0]
# 元の行から置き換え対象を削除し、再計算結果を挿入
# 注意:この処理だと、同じ行に他の分量(例:玉ねぎ1個)があっても残ってしまうが、
# 小さじ・大さじが含まれる行の分量表記は全て置換対象とする
# 最初の分量表記の部分を再計算結果で置き換え
new_line = line[:start] + new_spoon_str + line[end:]
# 2つ目以降の「数+単位」は削除する(簡易化のため)
for start_i, end_i, original_t in replacements[1:]:
new_line = new_line.replace(original_t, '', 1) # 最初に見つかったものだけ置換
recalculated_lines.append(new_line.strip())
continue # 次の行へ
# 上記以外の一般的な分量処理 (個, g, ml など)
# (数) + (単位) のパターンを抽出(小さじ/大さじは上記で処理済み)
# 単位に「適量」「少々」など数がないものも含むが、数がある場合のみ計算する
# 例: 1/2, 1, 1.5, 2/3
general_regex = r'([\d\.\/]+)\s*(' + UNIT_PATTERNS['volume_weight'] + r')'
# 置換済みフラグ
line_replaced = False
new_line = line
# 複数の一致を順に処理
for match in re.finditer(general_regex, line):
amount_str, unit_str = match.groups()
original_text = match.group()
amount = 0
try:
if '/' in amount_str:
# 分数の場合 (例: 1/2)
num, den = map(int, amount_str.split('/'))
amount = Decimal(num) / Decimal(den)
else:
# 整数・少数の場合
amount = Decimal(amount_str)
except Exception:
# 分量としてパースできない場合はスキップ
continue
# 再計算
new_amount = amount * ratio
# 新しい分量文字列(分数表記)
new_amount_str = convert_to_fraction_string(new_amount, unit_str)
# 新しい分量+単位の文字列
new_text = f"{new_amount_str}{unit_str}"
# 元のテキストを新しいテキストに置き換え
new_line = new_line.replace(original_text, new_text, 1) # 最初に見つかったものだけ置換
line_replaced = True
# 単位の前の数字がない場合(例: 玉ねぎ 中 1個, 少々, 適量など)
if not line_replaced:
# 「中1個」「大さじ」のようなパターンに対応するため、数字のない単位を検出
# 例: 中1個 -> 中 1個 として扱えるようにするため
# 中、小、大などの接頭語と数字+単位の組み合わせを検出 (例: '中1個')
prefix_unit_regex = r'(中|小|大)\s*([\d\.\/]+)(' + UNIT_PATTERNS['volume_weight'] + r')'
match_prefix = re.search(prefix_unit_regex, line)
if match_prefix:
prefix, amount_str, unit_str = match_prefix.groups()
original_text = match_prefix.group()
amount = 0
try:
if '/' in amount_str:
num, den = map(int, amount_str.split('/'))
amount = Decimal(num) / Decimal(den)
else:
amount = Decimal(amount_str)
except Exception:
recalculated_lines.append(line)
continue
new_amount = amount * ratio
new_amount_str = convert_to_fraction_string(new_amount, unit_str)
new_text = f"{prefix}{new_amount_str}{unit_str}"
new_line = new_line.replace(original_text, new_text, 1)
line_replaced = True
recalculated_lines.append(new_line.strip())
return '\n'.join(recalculated_lines)
# --- ユーザーインターフェース部分 ---
def run_recipe_recalculator():
"""
スクリプト実行用のインターフェース関数。
"""
print("--- DELISH KITCHEN レシピ分量再計算スクリプト ---")
print("現在の分量が何人分か、そして何人分にしたいかを入力してください。")
# 現在の人数入力
while True:
try:
current = float(input("現在の分量 (人数): "))
if current <= 0:
print("人数は0より大きい値を入力してください。")
continue
break
except ValueError:
print("無効な入力です。数値を入力してください。")
# 目標人数入力
while True:
try:
target = float(input("目標の分量 (人数): "))
if target <= 0:
print("人数は0より大きい値を入力してください。")
continue
break
except ValueError:
print("無効な入力です。数値を入力してください。")
print("\n--- レシピの分量部分を貼り付けてください (貼り付け後、改行してEnterを2回押してください) ---")
recipe_lines = []
while True:
try:
line = input()
if not line:
break
recipe_lines.append(line)
except EOFError:
break
recipe_text = '\n'.join(recipe_lines)
if not recipe_text.strip():
print("\nレシピの入力がありませんでした。処理を終了します。")
return
print("\n--- 計算結果 ---")
result = recalculate_recipe(recipe_text, current, target)
print(result)
print("----------------")
if __name__ == "__main__":
run_recipe_recalculator()
スクリプトの実行
出力されたコードをコピペして実行!
% python3 recalculate_recipe_amounts.py
--- DELISH KITCHEN レシピ分量再計算スクリプト ---
現在の分量が何人分か、そして何人分にしたいかを入力してください。
現在の分量 (人数): 2
目標の分量 (人数): 3
--- レシピの分量部分を貼り付けてください (貼り付け後、改行してEnterを2回押してください) ---
鶏もも肉 250g
玉ねぎ 中1個
にんじん 1/3本
ピーマン 2個
酒 大さじ1
しょうゆ 大さじ1
みりん 大さじ1
砂糖 小さじ1
塩 小さじ1/2
こしょう 少々
片栗粉 大さじ2
揚げ油 適量
--- 計算結果 ---
鶏もも肉 375g
玉ねぎ 中1と1/2個
にんじん 1/2本
ピーマン 3個
酒 大さじ1
しょうゆ 大さじ1
みりん 大さじ1
砂糖 小さじ1
塩 小さじ1/2
こしょう 少々
片栗粉 大さじ2
揚げ油 適量
----------------
すごい!!かなりよしなにやってくれています🥺!
言わなくても対話にしてくれてる!「中1個」も「中1と1/2個」にしてくれている!
少々とか適量はそのままにしてくれているし、やりたいことができています!
いや待て…大さじと小さじが計算されてないじゃないですか!!!
Geminiちゃん!
2人分で大さじ1のとき3人分でも大さじ1になってるよ!
👩💻 私:
大さじ小さじだけ計算されていないようです。
2人分で大さじ1のとき3人分でも大さじ1になっています。
🤖 Gemini:
ご指摘ありがとうございます。確かに、前回のスクリプトでは大さじ・小さじの抽出パターンが、数字と単位の間にスペースがない場合に正しく対応できていませんでした。
また、複雑な複数表記の処理にも改善の余地がありました。
改良したコードを教えてくれたので、再度実行!
% python3 recalculate_recipe_amounts.py
--- DELISH KITCHEN レシピ分量再計算スクリプト ---
現在の分量が何人分か、そして何人分にしたいかを入力してください。
現在の分量 (人数): 2
目標の分量 (人数): 3
--- レシピの分量部分を貼り付けてください (貼り付け後、改行してEnterを2回押してください) ---
鶏もも肉 250g
玉ねぎ 中1個
にんじん 1/3本
ピーマン 2個
酒 大さじ1
しょうゆ 大さじ1
みりん 大さじ1
砂糖 小さじ1
塩 小さじ1/2
こしょう 少々
片栗粉 大さじ2
揚げ油 適量
--- 計算結果 ---
鶏もも肉 375g
玉ねぎ 中1と1/2個
にんじん 1/2本
ピーマン 3個
酒 大さじ1
しょうゆ 大さじ1
みりん 大さじ1
砂糖 小さじ1
塩 小さじ1/2
こしょう 少々
片栗粉 大さじ2
揚げ油 適量
----------------
Geminiちゃん!!!!!!!直ってないよ!!!!!
何回かやり直しても改善しない…。やはり丸投げは無理だったのか…。
コードを見て修正するのも面倒です。ということで、私の検証は終了…
といいたいところですが、まだ頑張ります。Geminiの力はこんなものじゃないはず!
新しいGeminiちゃんを召喚(新しいチャット)してコードベースから原因を聞いてみましょう。
👩💻 私:
以下のスクリプトで、以下の文字列を入力時に大さじの行と小さじの行以外は計算されるのですが、大さじと小さじだけ計算をしてくれないのですが、原因がわかりますか?
入力文字列:
鶏もも肉 250g
酒 大さじ1
砂糖 小さじ1
スクリプト:
...略...
🤖 Gemini:
入力例と現象(大さじ・小さじの行だけが計算されない)を拝見しました。
この現象が発生する最も根本的な理由は、「酒 大さじ1」のように、数値が単位の直後ではなく、少し離れた位置にある場合のパターンを考慮できていないことにあります。
- recalculate_recipe 関数のセクション1(大さじ・小さじ)
大さじ・小さじを処理するための正規表現はこれです。spoon_regex = r'([\d\.\/]+)\s*(' + UNIT_PATTERNS['spoon'] + r')' # 期待されるパターン: (半角数字/分数) + (スペース*) + (大さじ/小さじ)この正規表現では、以下のような文字列しかマッチしません。
マッチする例 理由 1/2大さじ 数値と単位が連続している 1 大さじ 数値と単位の間に半角スペースがある 1.5小さじ 小数点を含む半角数値 ご提示の入力例(例: 酒 大さじ1)は、「単位」が先に来て、その後に「数値」が来る、という日本語のレシピ特有の構造になっています。
酒 大さじ1
...略...
次にご希望であれば、修正したフルスクリプトを提供し、動作を確認いただくことが可能です。いかがでしょうか?
👩💻 私:
修正したフルスクリプトをお願いします!
🤖 Gemini:
承知いたしました。ご提示いただいた問題を解決するため
recalculate_recipe関数内の小さじ・大さじを処理する正規表現とロジックを修正しました。
出力されたコード(長いので折りたたんでおきます!)
import re
from fractions import Fraction
from decimal import Decimal, getcontext
# Decimalの精度を設定(計算誤差を減らすため)
getcontext().prec = 50
# 単位のグループ化と正規表現パターン
UNIT_PATTERNS = {
# 容量/重量系 (小さじ/大さじは特別処理)
'volume_weight': r'(?:ml|g|cc|L|kg|カップ|合|袋|個|本|枚|房|束|cm|mm|株|かけ|パック|缶|びん|瓶|ブロック|切れ|つ|粒|滴|Dash|適量|少々)',
# 小さじ・大さじ
'spoon': r'(?:小さじ|大さじ|tsp|tbsp)',
}
# 小さじと大さじの変換係数
TSP_TO_TBSP = 3 # 小さじ3 = 大さじ1
def parse_amount(amount_str):
"""分量文字列をDecimalに変換するヘルパー関数"""
try:
if '/' in amount_str:
# 分数の場合 (例: 1/2)
num, den = map(int, amount_str.split('/'))
return Decimal(num) / Decimal(den)
else:
# 整数・少数の場合
return Decimal(amount_str)
except Exception:
return Decimal(0)
def convert_to_fraction_string(value, unit_str=''):
"""
数値を「整数 + 1/4までの分数」の文字列に変換する。
"""
if value == 0:
return '0'
# 小数点以下が0.25, 0.5, 0.75に近いか確認
decimal_part = value - int(value)
# 小数部分を分数に変換(分母は4までを優先)
if decimal_part >= 0.995:
integer_part = int(value) + 1
fraction_part_str = ''
elif decimal_part >= 0.745:
integer_part = int(value)
fraction_part_str = '3/4'
elif decimal_part >= 0.495:
integer_part = int(value)
fraction_part_str = '1/2'
elif decimal_part >= 0.245:
integer_part = int(value)
fraction_part_str = '1/4'
else:
if decimal_part < 0.005:
integer_part = int(value)
fraction_part_str = ''
else:
# 1/3, 2/3のような一般的な分数に対応させるための処理
frac = Fraction(Decimal(decimal_part)).limit_denominator(4)
# 小さじ・大さじ以外の単位で、分母が4より大きい場合は小数表示に戻す
if frac.denominator > 4 and unit_str not in UNIT_PATTERNS['spoon']:
return f"{value:.1f}".rstrip('0').rstrip('.')
integer_part = int(value)
fraction_part_str = f"{frac.numerator}/{frac.denominator}"
if frac.numerator == 0:
fraction_part_str = ''
result = []
if integer_part > 0:
result.append(str(integer_part))
if fraction_part_str:
result.append('と')
if fraction_part_str:
if integer_part > 0 and 'と' in result:
result.append(fraction_part_str)
elif integer_part == 0:
result.append(fraction_part_str)
else:
pass
if not result and value != 0:
return f"{value:.2f}"
return "".join(result).replace('と1/1', 'と1').replace('と0', '').replace('1/1', '1')
def convert_tsp_to_tbsp(tsp_amount):
"""
小さじの量を「大さじXと小さじY」に変換し、分数文字列で返す。
小さじ3 = 大さじ1
"""
if tsp_amount == 0:
return '0', '小さじ'
# 大さじの量
tbsp = tsp_amount // TSP_TO_TBSP
# 残りの小さじの量
remaining_tsp = tsp_amount % TSP_TO_TBSP
result = []
# 大さじの表示
if tbsp > 0:
tbsp_frac_str = convert_to_fraction_string(tbsp)
result.append(f"大さじ{tbsp_frac_str}")
# 残りの小さじの表示
if remaining_tsp > 0:
tsp_frac_str = convert_to_fraction_string(remaining_tsp)
result.append(f"小さじ{tsp_frac_str}")
if not result:
return '0', '小さじ'
# 「大さじXと小さじY」のように結合
return 'と'.join(result), ''
def recalculate_recipe(recipe_text, current_servings, target_servings):
"""
レシピの分量を人数に合わせて再計算するメイン関数。
"""
if current_servings <= 0 or target_servings <= 0:
return "エラー: 人数は正の数である必要があります。"
# 計算比率
ratio = Decimal(target_servings) / Decimal(current_servings)
# 結果格納用リスト
recalculated_lines = []
# 入力テキストを改行で分割
lines = recipe_text.strip().split('\n')
# レシピ項目ごとに処理
for line in lines:
if not line.strip():
continue
new_line = line # 処理後の行を保持する変数
# --- 1. 小さじ・大さじを含む行の処理 ---
# ★★★ 修正箇所 ★★★
# (単位) + (スペース*) + (数値) のパターン (例: 大さじ1) または
# (数値) + (スペース*) + (単位) のパターン (例: 1大さじ)
# グループ構成:
# P1 (単位-数値): group(1)=単位, group(2)=数値
# P2 (数値-単位): group(3)=数値, group(4)=単位
spoon_regex = r'(' + UNIT_PATTERNS['spoon'] + r')\s*([\d\.\/]+)' + r'|' + r'([\d\.\/]+)\s*(' + UNIT_PATTERNS['spoon'] + r')'
# 小さじ換算での合計量(Decimal)
total_tsp_amount = Decimal(0)
# 行内のすべての大さじ/小さじの分量表記を抽出
spoon_matches = list(re.finditer(spoon_regex, line))
# 小さじ・大さじの分量があった場合に実行
if spoon_matches:
# 抽出した分量表記を全て小さじ量に換算して合計
for match in spoon_matches:
amount_str, unit_str, original_text = '', '', ''
if match.group(1): # P1 (単位-数値) にマッチ
unit_str = match.group(1)
amount_str = match.group(2)
original_text = match.group(0)
elif match.group(3): # P2 (数値-単位) にマッチ
amount_str = match.group(3)
unit_str = match.group(4)
original_text = match.group(0)
else:
continue
amount = parse_amount(amount_str)
# 小さじ換算に変換
if unit_str in ['大さじ', 'tbsp']:
total_tsp_amount += amount * Decimal(TSP_TO_TBSP)
elif unit_str in ['小さじ', 'tsp']:
total_tsp_amount += amount
# 再計算して新しい行を作成
if total_tsp_amount > 0:
# 合計小さじ量を再計算
new_total_tsp_amount = total_tsp_amount * ratio
# 新しい大さじ/小さじの文字列を生成(例: '大さじ2と小さじ1と1/2')
new_spoon_str, _ = convert_tsp_to_tbsp(new_total_tsp_amount)
# 元の行から、すべての大さじ・小さじの表記を削除
temp_line = line
# 後ろから処理しないと、span()の位置がずれるため、マッチを反転させる
for match in reversed(spoon_matches):
start, end = match.span()
# マッチした分量全体を削除
temp_line = temp_line[:start] + temp_line[end:]
# 挿入位置: 最初にマッチした分量の開始位置を利用
first_match_start = spoon_matches[0].span()[0]
# 材料名(分量が削除された部分より前)と新しい分量を結合
# 挿入位置: 最初の分量表記の直前
new_line = line[:first_match_start] + new_spoon_str + ' ' + temp_line[first_match_start:].strip()
# セクション2の処理のためにnew_lineを更新するが、この行は既にほぼ完成している
# 他の単位が混在している可能性があるので、処理を続行する(continueを削除)
# --- 2. 上記以外の一般的な分量処理 (個, g, ml など) ---
general_regex = r'([\d\.\/]+)\s*(' + UNIT_PATTERNS['volume_weight'] + r')'
# 置換済みフラグ(セクション1で分量が処理されたかどうかの判断には使わない)
line_replaced = False
# 複数の一致を順に処理
# セクション1でnew_lineが更新されている場合、その更新された行に対して一般分量の処理を行う
temp_line_for_general_replace = new_line
# finditerはループ中に文字列が変わるとずれるため、文字列が変わるたびに再マッチングが必要だが、
# ここではreplace(..., 1)で最初のマッチだけを置き換え、元のロジックに従う
# NOTE: 正確性を高めるため、セクション1で既に処理した分量が含まれていないか確認すべきだが、
# 正規表現のパターンが異なるため、基本的に重複はしないと仮定して進める。
general_matches = list(re.finditer(general_regex, new_line)) # 現在のnew_lineに対してマッチング
for match in general_matches:
amount_str, unit_str = match.groups()
original_text = match.group()
amount = parse_amount(amount_str)
# 再計算
new_amount = amount * ratio
# 新しい分量文字列(分数表記)
new_amount_str = convert_to_fraction_string(new_amount, unit_str)
# 新しい分量+単位の文字列
new_text = f"{new_amount_str}{unit_str}"
# 元のテキストを新しいテキストに置き換え
temp_line_for_general_replace = temp_line_for_general_replace.replace(original_text, new_text, 1) # 最初に見つかったものだけ置換
line_replaced = True
new_line = temp_line_for_general_replace
# 単位の前の数字がない場合(例: 玉ねぎ 中1個)
if not line_replaced:
# (中|小|大) + (スペース*) + (数) + (単位)
prefix_unit_regex = r'(中|小|大)\s*([\d\.\/]+)(' + UNIT_PATTERNS['volume_weight'] + r')'
match_prefix = re.search(prefix_unit_regex, new_line)
if match_prefix:
prefix, amount_str, unit_str = match_prefix.groups()
original_text = match_prefix.group()
amount = parse_amount(amount_str)
new_amount = amount * ratio
new_amount_str = convert_to_fraction_string(new_amount, unit_str)
# NOTE: ここもスペースなしで結合
new_text = f"{prefix}{new_amount_str}{unit_str}"
new_line = new_line.replace(original_text, new_text, 1)
recalculated_lines.append(new_line.strip())
return '\n'.join(recalculated_lines)
# --- ユーザーインターフェース部分 (変更なし) ---
def run_recipe_recalculator():
"""
スクリプト実行用のインターフェース関数。
"""
print("--- DELISH KITCHEN レシピ分量再計算スクリプト ---")
print("現在の分量が何人分か、そして何人分にしたいかを入力してください。")
# 現在の人数入力
while True:
try:
current = float(input("現在の分量 (人数): "))
if current <= 0:
print("人数は0より大きい値を入力してください。")
continue
break
except ValueError:
print("無効な入力です。数値を入力してください。")
# 目標人数入力
while True:
try:
target = float(input("目標の分量 (人数): "))
if target <= 0:
print("人数は0より大きい値を入力してください。")
continue
break
except ValueError:
print("無効な入力です。数値を入力してください。")
print("\n--- レシピの分量部分を貼り付けてください (貼り付け後、改行してEnterを2回押してください) ---")
recipe_lines = []
while True:
try:
line = input()
if not line:
break
recipe_lines.append(line)
except EOFError:
break
recipe_text = '\n'.join(recipe_lines)
if not recipe_text.strip():
print("\nレシピの入力がありませんでした。処理を終了します。")
return
print("\n--- 計算結果 ---")
result = recalculate_recipe(recipe_text, current, target)
print(result)
print("----------------")
if __name__ == "__main__":
run_recipe_recalculator()
原因を教えてくれました。
なるほど!コード上では**「1大さじ」のような形の正規表現**になっていたんですね!
その上修正したコードも出力してくれました。
これで解決だ!
出力されたスクリプトを早速実行してみます。
% python3 recalculate_recipe_amounts.py
--- DELISH KITCHEN レシピ分量再計算スクリプト ---
現在の分量が何人分か、そして何人分にしたいかを入力してください。
現在の分量 (人数): 2
目標の分量 (人数): 3
--- レシピの分量部分を貼り付けてください (貼り付け後、改行してEnterを2回押してください) ---
鶏もも肉 250g
玉ねぎ 中1個
にんじん 1/3本
ピーマン 2個
酒 大さじ1
しょうゆ 大さじ1
みりん 大さじ1
砂糖 小さじ1
塩 小さじ1/2
こしょう 少々
片栗粉 大さじ2
揚げ油 適量
--- 計算結果 ---
鶏もも肉 375g
玉ねぎ 中1と1/2個
にんじん 1/2本
ピーマン 3個
酒 大さじ1と小さじ1と1/2
しょうゆ 大さじ1と小さじ1と1/2
みりん 大さじ1と小さじ1と1/2
砂糖 小さじ1と1/2
塩 小さじ3/4
こしょう 少々
片栗粉 大さじ3
揚げ油 適量
おお!ちゃんと計算できてる!!
細かいところに修正を入れたいところはありますが、ほぼ完成と言っていいでしょう!
結論
(詰まった部分はありましたが)ほぼ丸投げで実用スクリプトは作れました!
感想
正直、最初は「条件を書いて投げれば、いい感じに全部やってくれるだろう」と思っていました。
実際、かなりの部分まではその期待通りに動いてくれて、「これはすごい…!」と感動しました!
ただ一方で、途中で詰まった「大さじ・小さじが計算されない問題」は、AIに丸投げしただけでは解決できませんでした。
雑なお願いでは正確なコードは返ってこないというのはありそうです。
そのあと、「なぜ動かないか」を言語化して聞き直すことで、AIの回答の精度が一気に上がって期待通りのコードになりました。
こういうところのやり方が難しいですね…🤔
結果として、
- ほぼコードを書かず
- 詰まった部分も対話で解決し
- 実用レベルのスクリプトを作ることができました。
なので、次はもう少し欲張った条件でも試してみたいです。
また今回の検証で、プロンプトの書き方によっても精度が変わりそうな雰囲気を感じましたので、次回その辺の記事を書けたら良いなと思います。