レシート画像をCSV化して、セルフメディケーション対象金額をSQLiteで集計する
セルフメディケーション税制の対象商品について、年間の購入金額を集計できるようにしました。
最初の入力はスマホで撮影したレシート画像です。
Geminiアプリなどでレシート画像を読み取り、購入履歴をCSVに変換します。そのCSVをSQLiteへインポートし、2025年のセルフメディケーション対象金額を合計します。
今回は、CSVインポート、重複スキップ、商品別集計、店舗別集計まで作成しました。
今回の目的
目的は、レシート画像から購入履歴を作成し、セルフメディケーション対象商品の合計金額を確認できるようにすることです。
あとで購入管理にも使いたいため、医薬品だけでなく、食品や日用品も含めてCSV化します。
セルフメディケーション対象外の商品は、対象金額を 0 にして保存します。
CSVの形式
レシート画像から、以下の形式のCSVを作成します。
購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額
例は以下です。
購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額 サンプルドラッグストアA,2025/06/18,サンテビオ 15mL,968,871,1,871 サンプルスーパーB,2025/04/18,タイレノールA 30錠,1790,1772,1,1772 サンプルスーパーC,2025/06/19,ゼリー飲料,118,354,3,0 サンプルスーパーC,2025/06/19,鮭,238,190,1,0 サンプルスーパーC,2025/06/19,天ぷら用えび,398,238,1,0 サンプルスーパーC,2025/06/19,たい,398,398,1,0 サンプルスーパーC,2025/06/19,塩銀鮭切身,398,398,1,0
このように、セルフメディケーション対象商品は対象金額を入れ、食品など対象外の商品は 0 にします。
GeminiやGPTへの指示
レシート画像を読み取るときは、以下のような方針で指示します。
後でDBで購入管理したいです。 レシート画像から以下の項目を読み取り、CSV形式で出力してください。 購入店舗名 購入年月日 商品名 単品税抜価格 価格 個数 セルフメディケーション対象金額 食品などセルフメディケーション対象外の商品は、 セルフメディケーション対象金額を 0 にしてください。 出力形式は以下にしてください。 購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額
このプロンプトで、複数枚のレシートをまとめてCSV化できます。
ただし、医薬品名や金額はOCRの誤認識が起きやすいため、最終的にはレシート原本と照合する前提です。
CSVファイルの配置
作成したCSVファイルは、MacBook Air側の以下のディレクトリに入れます。
./csv_files/
このフォルダ内にあるCSVをまとめて読み込みます。
CSVをSQLiteへインポートする
CSVのインポートには、以下のスクリプトを使います。
python import_receipts_with_dedup_and_log.py
このスクリプトでは、以下を行います。
csv_filesフォルダ内のCSVを読み込む- SQLiteの
receipts.dbに保存する - 同じCSVファイルを二重にインポートしない
- 重複した商品行は
skipped.csvに記録する
import_receipts_with_dedup_and_log.py
CSVをSQLiteへインポートするコードです。
import sqlite3
import csv
import os
from datetime import datetime
# 設定
csv_folder = './csv_files'
db_file = 'receipts.db'
skipped_log = 'skipped.csv'
# SQLite接続
conn = sqlite3.connect(db_file)
cursor = conn.cursor()
# purchasesテーブル(UNIQUE制約付き)
cursor.execute('''
CREATE TABLE IF NOT EXISTS purchases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
store TEXT,
date TEXT,
item TEXT,
unit_price INTEGER,
total_price INTEGER,
quantity INTEGER,
self_med_amount INTEGER,
UNIQUE(date, store, item)
)
''')
# import_logテーブル
cursor.execute('''
CREATE TABLE IF NOT EXISTS import_log (
filename TEXT PRIMARY KEY
)
''')
# skipped.csv が存在していたら上書き、なければ新規作成
with open(skipped_log, 'w', newline='', encoding='utf-8') as skipfile:
skip_writer = csv.writer(skipfile)
skip_writer.writerow([
'購入店舗名',
'購入年月日',
'商品名',
'単品税抜価格',
'価格',
'個数',
'セルフメディケーション対象金額'
])
for filename in os.listdir(csv_folder):
if not filename.endswith('.csv'):
continue
filepath = os.path.join(csv_folder, filename)
# ファイルの重複チェック
cursor.execute('SELECT 1 FROM import_log WHERE filename = ?', (filename,))
if cursor.fetchone():
print(f"[スキップ] {filename}(すでにインポート済み)")
continue
print(f"[処理中] {filename}")
try:
with open(filepath, newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
# ヘッダー行がデータ行に混入していたらスキップ
if row['購入年月日'] == '購入年月日':
continue
store = row['購入店舗名'].strip()
date = datetime.strptime(row['購入年月日'], '%Y/%m/%d').strftime('%Y%m%d')
item = row['商品名'].strip()
unit_price = int(row['単品税抜価格'])
total_price = int(row['価格'])
quantity = int(row['個数'])
self_med_amount = int(row['セルフメディケーション対象金額'])
try:
cursor.execute('''
INSERT INTO purchases
(store, date, item, unit_price, total_price, quantity, self_med_amount)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
store,
date,
item,
unit_price,
total_price,
quantity,
self_med_amount
))
except sqlite3.IntegrityError:
# 重複データをskipped.csvに記録
skip_writer.writerow([
store,
row['購入年月日'],
item,
unit_price,
total_price,
quantity,
self_med_amount
])
# ファイル名をインポート済みとして記録
cursor.execute('INSERT INTO import_log (filename) VALUES (?)', (filename,))
print(f"[完了] {filename} をインポートしました。")
except Exception as e:
print(f"[エラー] {filename}: {e}")
conn.commit()
conn.close()
print("✅ すべてのCSVの処理が完了しました。スキップされた商品は skipped.csv に保存されています。")
テーブル設計
購入履歴は purchases テーブルへ保存します。
| カラム | 内容 |
|---|---|
store |
購入店舗名 |
date |
購入日。YYYYMMDD 形式 |
item |
商品名 |
unit_price |
単品税抜価格 |
total_price |
購入価格 |
quantity |
個数 |
self_med_amount |
セルフメディケーション対象金額 |
重複防止として、以下のUNIQUE制約を付けています。
UNIQUE(date, store, item)
同じ日付・同じ店舗・同じ商品名のデータは重複登録しない方針です。
インポート済みファイルの管理
同じCSVファイルを何度も読み込まないように、import_log テーブルを作っています。
CREATE TABLE IF NOT EXISTS import_log (
filename TEXT PRIMARY KEY
)
一度読み込んだCSVファイル名は import_log に記録します。
次回実行時に同じファイル名があれば、インポートをスキップします。
セルフメディケーション対象金額を集計する
次に、セルフメディケーション対象商品の合計金額を確認します。
python medication_summary.py
コードは以下です。
import sqlite3
# SQLiteに接続
conn = sqlite3.connect("receipts.db")
cursor = conn.cursor()
print("📋 セルフメディケーション対象商品の合計金額(2025年)")
cursor.execute('''
SELECT SUM(self_med_amount)
FROM purchases
WHERE date BETWEEN '20250101' AND '20251231'
AND self_med_amount > 0
''')
total = cursor.fetchone()[0] or 0
print(f"▶ 合計金額:¥{total:,}")
print()
print("📊 商品別セルフメディケーション対象金額ランキング(2025年)")
cursor.execute('''
SELECT item, SUM(self_med_amount) AS total
FROM purchases
WHERE date BETWEEN '20250101' AND '20251231'
AND self_med_amount > 0
GROUP BY item
ORDER BY total DESC
''')
rows = cursor.fetchall()
if not rows:
print("(対象商品はありません)")
else:
print("商品名\t\t金額")
print("-" * 30)
for item, amount in rows:
print(f"{item[:14]:<14} ¥{amount:,}")
conn.close()
商品別集計の結果
実行結果は以下のようになりました。
📋 セルフメディケーション対象商品の合計金額(2025年) ▶ 合計金額:¥37,576 📊 商品別セルフメディケーション対象金額ランキング(2025年) 商品名 金額 ------------------------------ リングルアイビーα200 3 ¥17,213 タイレノールA 30錠 ¥5,494 リングルアイビーα200 3 ¥2,455 101佐藤製薬 リング(15 ¥1,944 101佐藤製薬 リング★ ¥1,944 118ロキソニンSテー★ ¥1,664 セタイレノールA 30錠 ¥1,611 セロキソニンSテープ 14枚 ¥1,422 新ルルAゴールドDXα 30 ¥1,069 ケラチナミン乾燥肌クリーム ¥970 新ルルAゴールドs 30錠 ¥919 サンテビオ 15mL ¥871
2025年のセルフメディケーション対象商品の合計金額は、37,576円 でした。
商品名の表記ゆれがある
集計結果を見ると、同じ商品と思われるものが複数行に分かれています。
これはレシートOCRやLLMの読み取りで、商品名が途中で切れたり、記号が混ざったりしているためです。
例えば、以下のような表記ゆれがあります。
- リングルアイビーα200 3
- 101佐藤製薬 リング(15
- 101佐藤製薬 リング★
このままだと商品別ランキングが分散するため、今後は商品名の正規化が必要です。
店舗別に集計する
実際に確定申告で入力する場合、店舗ごとに金額を分けて確認したい場合があります。
そのため、店舗別にセルフメディケーション対象金額を集計するスクリプトも作成しました。
python medication_summary_store.py
コードは以下です。
import sqlite3
conn = sqlite3.connect("receipts.db")
cursor = conn.cursor()
print("📋 セルフメディ対象合計(2025年)")
cursor.execute("""
SELECT SUM(self_med_amount)
FROM purchases
WHERE date BETWEEN '20250101' AND '20251231'
AND self_med_amount > 0
""")
total = cursor.fetchone()[0] or 0
print(f"▶ 合計:¥{total:,}")
print()
print("🏪 店舗別セルフメディ対象金額(2025年)")
cursor.execute("""
SELECT store, SUM(self_med_amount) AS total
FROM purchases
WHERE date BETWEEN '20250101' AND '20251231'
AND self_med_amount > 0
GROUP BY store
ORDER BY total DESC
""")
rows = cursor.fetchall()
if not rows:
print("対象データなし")
else:
for store, amount in rows:
print(f"{store} ¥{amount:,}")
conn.close()
店舗別集計の結果
実行結果は以下です。
📋 セルフメディ対象合計(2025年) ▶ 合計:¥37,576 🏪 店舗別セルフメディ対象金額(2025年) サンプルドラッグストアA ¥23,319 サンプルドラッグストアB ¥5,552 サンプルスーパーC ¥4,805 サンプルドラッグストアD ¥3,900
商品別だけでなく、店舗別でも対象金額を確認できるようになりました。
全体の流れ
今回の処理全体は以下です。
- スマホでレシートを撮影する
- Geminiアプリなどでレシート画像を読み取る
- CSV形式で出力する
- CSVを
csv_filesフォルダへ保存する import_receipts_with_dedup_and_log.pyでSQLiteへインポートするmedication_summary.pyで商品別に集計するmedication_summary_store.pyで店舗別に集計する
今回できたこと
今回の作業で、以下までできました。
- レシート画像からCSVを作成する方針を決めた
- セルフメディケーション対象金額の列を追加した
- 食品など対象外商品も同じCSVに残せるようにした
- CSVをSQLiteへインポートできるようにした
- 同じCSVファイルの二重インポートを防げるようにした
- 重複行を
skipped.csvに記録できるようにした - 2025年のセルフメディケーション対象金額を商品別に集計できた
- 店舗別の対象金額も集計できた
ハマりどころ
OCRの商品名が安定しない
レシートの商品名は省略表記が多く、OCRやLLMの読み取りで表記ゆれが発生します。
商品別に正確に集計するには、商品名正規化テーブルが必要になりそうです。
セルフメディケーション対象商品の判定は確認が必要
GeminiやGPTで対象商品を推定できますが、税制に関わるため、最終的にはレシートや公式情報で確認する必要があります。
特に、似た商品名や医薬部外品、健康食品などは誤判定しやすい可能性があります。
同じ日に同じ店舗で同じ商品を複数回買った場合
現在のUNIQUE制約は以下です。
UNIQUE(date, store, item)
このため、同じ日・同じ店舗・同じ商品を別会計で購入した場合、重複扱いになる可能性があります。
現時点では家庭内の簡易管理として問題ないですが、より厳密にするなら、レシート番号や購入時刻も持たせた方がよさそうです。
次にやること
次は、以下を追加していきたいです。
- 商品名正規化テーブルを作る
- 同じ商品の表記ゆれをまとめる
- レシート番号や購入時刻を保存する
- セルフメディケーション対象商品の候補リストを作る
- 店舗別・月別の集計を追加する
- 確定申告用にCSV出力する
- Neo4jへ商品・店舗・購入日の関係を登録する
まとめ
レシート画像をGeminiなどでCSV化し、SQLiteへインポートすることで、セルフメディケーション対象商品の年間購入金額を集計できるようになりました。
2025年の対象金額は、現時点で 37,576円 でした。
さらに、商品別だけでなく店舗別にも集計できるようにしたため、確定申告時の確認にも使いやすくなりそうです。
今後は、商品名の正規化、レシート単位の管理、確定申告用CSV出力、Neo4j連携へ進めていきます。

コメント