0
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?

家に届く紙、全部捨てられない問題をDR-P208IIとGeminiでなんとかした備忘録

0
Posted at

家に届く紙、結局どう捨てればいいんやねん問題をDR-P208IIとGeminiでなんとかした備忘録

はじめに

家に紙があふれる。

郵便物、病院の領収書、税金の通知、保険の書類、レシート、チラシ。
「あとで見るかも」と思って置いておくと、いつの間にか紙の山。

そして一番つらいのがこれ。

これ、捨てていいの?
なんか対応必要だったりする?
原本いるやつ?

この判断が重い。

紙を整理したいというより、捨てていいか誰かに判断してほしい
ついでに、やらなきゃいけないことがあればメモしてほしい。

ということで、Canon のモバイルスキャナ DR-P208II と Gemini / Vertex AI を使って、自宅の紙書類をAIに判定してもらう仕組みを作りました。

備忘録的に書きます。


📌 完成イメージ

最終的にやりたいのはこれ。

DR-P208II
↓
PC
↓
Cloud Run
↓
Gemini
↓
Firestore

でも、いきなりCloud RunとかFirestoreまで行くと、たぶん途中で力尽きます。そういうところある。

なので、まずはローカルPCで動くMVPを作りました。

今回作ったのはこれ。

DR-P208II
↓
Windows PC
↓
C:\scanbox\inbox
↓
Python
↓
Gemini on Vertex AI
↓
CSV
↓
processed / review / receipts

要するに、

紙をスキャン
↓
PDFになる
↓
AIが読む
↓
捨てていいか判定
↓
要対応ならreviewへ
↓
レシートならログへ

という流れです。


できるようになったこと

今できることはこの2つ。

python app.py

これで inbox にあるPDFをAIが読んで分類します。

python todo.py

これで、対応が必要なものだけ表示します。

たとえばこんな感じ。

==============================
未対応タスク
==============================

[税金]
確定申告

ファイル: 20260530_税金_ふるさと納税不適用通知.pdf
期限: 期限なし・要確認

この時点でかなり嬉しい。

紙の山を見て「うっ……」となるのではなく、
todo.py を見ればいい状態になります。


1. まずスキャナの保存先を固定する

DR-P208II は Windows PC に接続して、スキャン先を固定しました。

C:\scanbox\inbox

フォルダ構成はこんな感じ。

C:\scanbox
├─ inbox
├─ processed
├─ review
└─ receipts

それぞれの役割はこちら。

フォルダ 役割
inbox スキャン直後のPDF置き場
processed 処理済み。特に対応不要なもの
review 要対応、原本保管など確認が必要なもの
receipts レシート類をまとめて置く場所

💡 最初は inbox だけでもいいです。
いきなり完璧な分類フォルダを作ろうとすると、にっくき整理欲が暴れます。


2. Python用の作業フォルダを作る

アプリ側は別フォルダにしました。

C:\home-paper-inbox
├─ app.py
├─ todo.py
├─ result.csv
├─ receipt_log.txt
├─ .env
├─ .env.example
├─ requirements.txt
└─ README.md

実際の自分のPCでは別名のフォルダを使っていましたが、GitHubに上げるときに個人名っぽいものは隠したかったので、.env で外出ししました。


3. .env を使う

.env はこんな感じ。

GOOGLE_GENAI_USE_VERTEXAI=true
GOOGLE_CLOUD_PROJECT=your-gcp-project-id
GOOGLE_CLOUD_LOCATION=us-central1

BASE_SCAN_DIR=C:\scanbox
BASE_APP_DIR=C:\home-paper-inbox

コードに直接、

C:\scanbox_nantoka

みたいに書くと、GitHubに上げるときに「あっ」となります。
なりました。

なので .env に逃がします。


4. GitHubに上げないものを決める

個人情報の塊たちを紹介!

.env
result.csv
receipt_log.txt
PDF全部
.venv

こいつらはGitHubに上げない。

.gitignore はこんな感じ。

.env
.venv/
__pycache__/
*.pyc

result.csv
receipt_log.txt

*.pdf

💡 PDF、CSV、ログは普通に個人情報が入ります。
特にレシートとか病院とか税金とか、うっかり上げたらだめ。

5. 必要ライブラリを入れる

仮想環境を作ります。

python -m venv .venv
.\.venv\Scripts\Activate.ps1

必要なものを入れます。

pip install google-genai python-dotenv pandas
pip freeze > requirements.txt

今回のメンバーを紹介。

ライブラリ 役割
google-genai Gemini / Vertex AI を呼ぶ
python-dotenv .env を読む
pandas CSVからTodoを読む

6. 最初はGemini APIキーで試した

最初は Google AI Studio のAPIキーで動かしました。

client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))

これでPDFを投げてみたところ、ちゃんと読めた。

まじですごい。

ただし、何回か試していたら無料枠の上限に当たりました。

429 RESOURCE_EXHAUSTED

ということで、GCPクレジットを使うために Vertex AI 経由へ切り替えました。


7. Vertex AI に切り替える

gcloud はこんな感じで設定しました。

gcloud auth login
gcloud auth application-default login

gcloud config set project your-gcp-project-id
gcloud auth application-default set-quota-project your-gcp-project-id

gcloud services enable aiplatform.googleapis.com

.env はこう。

GOOGLE_GENAI_USE_VERTEXAI=true
GOOGLE_CLOUD_PROJECT=your-gcp-project-id
GOOGLE_CLOUD_LOCATION=us-central1

Python側はこう。

client = genai.Client(
    vertexai=True,
    project=os.getenv("GOOGLE_CLOUD_PROJECT"),
    location=os.getenv("GOOGLE_CLOUD_LOCATION"),
)

8. Vertex AI では client.files.upload が使えなかった

ここで一回詰まりました。

最初はこういう感じでPDFをアップロードしていました。

uploaded_file = client.files.upload(file=file_path)

でもVertex AI版だと、

This method is only supported in the Gemini Developer client.

と言われました。

はい、君はDeveloper API専用なのね。
理解した。

なので、PDFをbytesとして読んで渡す方式に変更しました。

from google.genai import types

with open(file_path, "rb") as f:
    pdf_bytes = f.read()

pdf_part = types.Part.from_bytes(
    data=pdf_bytes,
    mime_type="application/pdf",
)

これでVertex AI経由でもPDFを読めました。


9. AIに返してもらうJSON

Geminiには、必ずこの形で返してもらいます。

{
  "title": "短い文書名。20文字以内。例: ふるさと納税不適用通知",
  "document_date": "文書の日付または取引日。YYYY-MM-DD形式。不明ならnull",
  "summary": "文書の概要",
  "category": "医療 / 税金 / 保険 / 契約 / レシート / チラシ / 行政 / その他",
  "discard_paper": true,
  "keep_original": false,
  "action_required": false,
  "task": null,
  "due_date": null,
  "reason": "判定理由"
}

特に大事なのがこの3つ。

項目 意味
discard_paper 紙を捨ててよいか
keep_original 原本保管が必要そうか
action_required 何か対応が必要か

このへんが欲しかった。


10. 日付はスキャン日ではなく文書の日付を使う

最初はファイル名にスキャン日を使っていました。

20260530_レシート_Mira!レシート.pdf

でも、よく考えたらこれは微妙。

2026年にスキャンした2025年のレシートなら、本当に欲しいのは取引日です。

20250122_レシート_Mira!レシート.pdf

こっちが正しい。

なのでAIに、

レシートの場合は、スキャン日ではなくレシートに記載された購入日・取引日を document_date に入れる

と指示しました。

ファイル名はこう作っています。

document_date = data.get("document_date")

if document_date:
    date_part = document_date.replace("-", "")
else:
    date_part = target_file[:8]

new_filename = f"{date_part}_{category}_{title}.pdf"

11. Windowsで使えない文字をファイル名から消す

AIがタイトルを作ると、たまにこういう文字が入りそうです。

/
\
:
*
?
"
<
>
|

Windows「それ無理です」

なので、ファイル名用に掃除します。

def sanitize_filename(name):
    invalid_chars = r'<>:"/\|?*'

    for char in invalid_chars:
        name = name.replace(char, "_")

    return name.strip()

地味だけど大事。

こういう小さいところを放置すると、あとで「なんで落ちたん?」となるらしい。そうAIがいってた。


12. 判定結果でフォルダ移動する

判定結果によって、PDFを移動します。

レシート → receipts
要対応 or 原本保管 → review
それ以外 → processed

イメージはこう。

if category == "レシート":
    append_receipt_log(row)
    dest = os.path.join(RECEIPTS_DIR, new_filename)
    shutil.move(target_path, dest)

elif data.get("action_required") or data.get("keep_original"):
    dest = os.path.join(REVIEW_DIR, new_filename)
    shutil.move(target_path, dest)

else:
    dest = os.path.join(PROCESSED_DIR, new_filename)
    shutil.move(target_path, dest)

これで review フォルダだけ見れば、
「ちゃんと見ないといけない紙」が残ります。

これ、かなり便利。


13. レシートは receipt_log.txt に残す

レシートPDFって、地味に容量が大きいです。

でも、全部永久保存したいかというと、そうでもない。

なのでレシートは receipt_log.txt に概要を残して、PDFは receipts に隔離する方針にしました。

ログ例。

========================================
日付: 2025-01-22
ファイル: 20250122_レシート_Mira! 飲食レシート.pdf
タイトル: Mira! 飲食レシート
カテゴリ: レシート
概要: 横浜市の飲食店「Mira!」での飲食レシート。ワインや料理などの購入明細と合計金額12,500円が記載されています。
紙を捨ててOK: True

将来的には、レシートPDFは30日後とか90日後に削除候補にしたいです。


14. todo.py で未対応だけ見る

todo.py は、result.csv の中から action_required == True の行だけ表示します。

from dotenv import load_dotenv
import os
import pandas as pd

load_dotenv()

BASE_APP_DIR = os.getenv("BASE_APP_DIR", r"C:\home-paper-inbox")
CSV_PATH = os.path.join(BASE_APP_DIR, "result.csv")

df = pd.read_csv(CSV_PATH)

todos = df[df["action_required"] == True]

print()
print("=" * 30)
print("未対応タスク")
print("=" * 30)

if len(todos) == 0:
    print("未対応なし")
else:
    for _, row in todos.iterrows():
        due_date = row["due_date"]
        if pd.isna(due_date):
            due_date = "期限なし・要確認"

        print()
        print(f"[{row['category']}]")
        print(row["task"])
        print()
        print(f"ファイル: {row['new_filename']}")
        print(f"期限: {due_date}")

使うときはこれ。

python todo.py

これで、紙の中から「やること」だけ拾えます。


15. 実際に拾えたタスク

実際に試したら、ふるさと納税ワンストップ特例の不適用通知を拾えました。

AIの判定はだいたいこんな感じ。

カテゴリ: 税金
要対応: True
タスク: 確定申告
概要: 住所相違によりワンストップ特例が不適用。寄附金控除を受けるには確定申告を検討する必要あり。

これを紙の山に埋もれさせていたら、普通に見落としていたかもしれない。


16. 詰まったところ

pypdfで日本語PDFが文字化けした

最初はPDFからテキスト抽出してAIに渡そうとしました。

でも、日本語のスキャンPDFで盛大に文字化けしました。

Advanced encoding /90ms-RKSJ-H not implemented yet

みたいなやつ。

にっくき文字コード。

結局、PDFをそのままGeminiに渡す方式にしたら解決しました。


1PDFにいろいろ混ぜると判定がぶれる

最初はいろんな紙をまとめてスキャンしていました。

レシート
クレカ売上票
リフト券
ピアス説明書

こういうのが1PDFに入っていると、AIが「その他」と判定しがち。

そりゃそう。

なので、今後は、

1書類 = 1PDF

で運用します。

これが大事。


CSVが壊れた

途中でCSVの列を変えたり、古い形式と新しい形式が混ざったりして、todo.py が落ちました。

エラーはこんな感じ。

Expected 11 fields in line 5, saw 19

つまり、CSVは11列のはずなのに、ある行だけ19列に見えている状態。

これは、手編集や古い形式の行が混ざったのが原因っぽいです。

一旦、壊れたCSVを退避して作り直しました。

ren result.csv result_broken.csv
ni result.csv
notepad result.csv

新しいヘッダーはこれ。

filename,new_filename,document_date,title,category,discard_paper,keep_original,action_required,task,due_date,summary

今後はCSV書き込み時にクォートもちゃんと入れたい。

writer = csv.DictWriter(
    f,
    fieldnames=FIELDNAMES,
    quoting=csv.QUOTE_ALL,
)

17. GitHubに上げた

ここまで来たので、GitHubにPrivate repositoryを作りました。

リポジトリ名は、

home-paper-inbox

上げるもの。

app.py
todo.py
requirements.txt
README.md
.env.example
.gitignore

上げないもの。

.env
result.csv
receipt_log.txt
PDF
.venv

最初は git add . したくなります。

でも今回は危険。

PDFとかCSVとか .env とか、うっかり上げたら終わりです。

なので明示的に追加しました。

git add app.py todo.py requirements.txt README.md .env.example .gitignore
git commit -m "Initial home paper inbox automation"
git push

18. 今後やりたいこと

まだまだやりたいことがあります。

  • レシートPDFを一定期間後に削除候補にする
  • チラシはPDF保存せずCSVだけ残す
  • 503 / 429 のリトライ処理を入れる
  • CSVではなくFirestoreに保存する
  • Cloud Run化する
  • ブラウザで見られる管理画面を作る
  • 「今月やること」だけ表示する
  • 医療費控除用の一覧を作る
  • 原本保管が必要なものだけ一覧化する

最終的にはこうしたい。

DR-P208II
↓
PC
↓
Cloud Run
↓
Gemini
↓
Firestore
↓
Web画面

ワクワクして待て、未来の自分。


まとめ

家に届く紙の問題って、実は「保存」ではなく「判断」が重いんだなと気づきました。

これは捨てていい?
原本いる?
何かやることある?
期限ある?

ここをAIに任せられるだけで、かなりラクになります。

今回のMVPでは、

紙
↓
スキャン
↓
AI判定
↓
CSV記録
↓
review / processed / receipts に分類
↓
Todo表示

までできました。

まだ荒削りですが、すでに実用感があります。

紙をPDF化するだけではなく、AIに「捨てていいか」と「やるべきこと」を聞く。

この方向、自宅の紙管理にかなり効きそうです。

それではまた〜。

0
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
0
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?