レシート画像をCSV化して、セルフメディケーション対象金額をSQLiteで集計する

レシート画像を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

商品別だけでなく、店舗別でも対象金額を確認できるようになりました。

全体の流れ

今回の処理全体は以下です。

  1. スマホでレシートを撮影する
  2. Geminiアプリなどでレシート画像を読み取る
  3. CSV形式で出力する
  4. CSVを csv_files フォルダへ保存する
  5. import_receipts_with_dedup_and_log.py でSQLiteへインポートする
  6. medication_summary.py で商品別に集計する
  7. 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連携へ進めていきます。

コメント

タイトルとURLをコピーしました