手書きの日報をスマホで撮るだけで、いつものExcelに追記される仕組みを作る(Claude API + openpyxl)
この記事で作るもの
中小企業最適化ラボの吉川です。普段は大手メーカーでDX推進と最適化を担当していて、個人では中小企業の業務の自動化をお手伝いしています。
中小の製造業の現場でよく伺うのが、こんな流れです。
- 作業者が紙の日報・検査記録に手で書く
- 事務の方が翌日、それをExcelに転記する
- 月末にまとめて集計する
現場の帳票を紙やExcelで管理している製造業は約7割で、不満の1位は「データの抽出・分析がしにくい」だそうです(製造業の現場管理者への調査、2026年)。転記の手間そのものより、転記が終わるまで数字が見えないのが困りどころだと感じています。
この記事では、紙の様式もExcelの様式も変えずに、スマホで撮った写真から生成AIで項目を読み取り、いつものExcelの最終行に追記する仕組みを、Python 100行くらいで作ります。
気をつけているのは次の3点です。
- 現場の入力方法は変えない(手書きのまま)
- 事務のExcelの様式も変えない(列はそのまま使う)
- AIの読み取り結果は、人が確認してから確定する
全体の流れ
スマホで撮影 → 共有フォルダに保存
↓
Pythonスクリプト(定期実行)
├ 画像を Claude API に送り、JSONで項目を抽出
├ 抽出結果を「確認待ち」CSVに書き出す
└ 確認済みの行だけ、いつものExcelに追記(openpyxl)
↓
翌朝、集計シートが自動で更新される
「確認待ち」を挟むところがポイントです。読み取りの精度は高いのですが、数字の読み間違いが1件でも集計に混ざると、現場の信頼を失ってしまいます。確認は、事務の方にCSVを開いて目を通していただき、OK列に印を付けてもらうだけにしています。
1. 読み取る項目を先に決める
AIに「この日報を読んで」とだけ頼むと、出力が毎回ぶれます。なので先に、Excelの列と一対一になる項目を決めて、その形で返してもらいます。
例として、こんな日報を想定します。
| 項目 | 例 |
|---|---|
| 日付 | 2026/11/05 |
| ライン | ライン2 |
| 品番 | A-1032 |
| 良品数 | 480 |
| 不良数 | 3 |
| 不良内容 | 寸法不良 |
| 停止時間(分) | 15 |
| 備考 | 金型交換あり |
これをJSONスキーマにします。
SCHEMA = {
"type": "object",
"properties": {
"date": {"type": "string", "description": "YYYY/MM/DD"},
"line": {"type": "string"},
"part_no": {"type": "string"},
"good_qty": {"type": "integer"},
"defect_qty": {"type": "integer"},
"defect_note": {"type": "string"},
"stop_min": {"type": "integer"},
"remarks": {"type": "string"},
"confidence": {"type": "number", "description": "0〜1。読み取りに自信がない項目があれば低くする"},
"unclear": {"type": "array", "items": {"type": "string"}, "description": "読み取れなかった、または自信のない項目名"},
},
"required": ["date", "line", "part_no", "good_qty", "defect_qty", "confidence", "unclear"],
}
confidence と unclear を入れているのは、自信がない行を人の確認に回すためです。読めなかったら空にして、どれが読めなかったかを申告してもらう、という約束をAIと先にしておきます。
2. 画像をClaude APIに送って抽出する
import base64, json, os
from pathlib import Path
import anthropic
from schema import SCHEMA
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
MODEL = "claude-sonnet-5-5" # 使う時点の最新モデル名に合わせる
PROMPT = f"""これは製造現場の手書き日報の写真です。
次のJSONスキーマに従って、記入内容を抽出してください。
- 読み取れない、または自信のない項目は空にし、unclear にその項目名を入れてください
- 数字は推測で補わないでください
- JSON以外は出力しないでください
スキーマ:
{json.dumps(SCHEMA, ensure_ascii=False)}
"""
def extract(image_path: Path) -> dict:
media = "image/jpeg" if image_path.suffix.lower() in (".jpg", ".jpeg") else "image/png"
data = base64.b64encode(image_path.read_bytes()).decode()
msg = client.messages.create(
model=MODEL,
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": media, "data": data}},
{"type": "text", "text": PROMPT},
],
}],
)
text = "".join(b.text for b in msg.content if b.type == "text").strip()
# コードブロックで囲まれて返ることがあるので外す
if text.startswith("```"):
text = text.strip("`").split("\n", 1)[1].rsplit("```", 1)[0]
return json.loads(text)
スマホで撮ったままの写真だと、解像度が大きすぎることがあります。長辺1,600pxくらいに縮小してから送ると、速度もコストも落ち着きます(Pillowで十分です)。
3. 「確認待ち」CSVに書き出す
import csv, shutil
from pathlib import Path
from extract import extract
INBOX = Path(r"\\nas\nippo\inbox") # スマホから写真が入る共有フォルダ
DONE = Path(r"\\nas\nippo\done")
PENDING = Path(r"\\nas\nippo\pending.csv") # 事務の方が確認するファイル
COLS = ["file", "date", "line", "part_no", "good_qty", "defect_qty",
"defect_note", "stop_min", "remarks", "confidence", "unclear", "ok"]
def main():
new = not PENDING.exists()
with PENDING.open("a", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=COLS)
if new:
w.writeheader()
for img in sorted(INBOX.glob("*.jp*g")) + sorted(INBOX.glob("*.png")):
r = extract(img)
row = {c: r.get(c, "") for c in COLS}
row["file"] = img.name
row["unclear"] = "、".join(r.get("unclear", []))
# 自信がある行は ok に ○ を入れておき、事務の方は×にするだけでよくする
row["ok"] = "○" if r.get("confidence", 0) >= 0.9 and not r.get("unclear") else ""
w.writerow(row)
shutil.move(img, DONE / img.name)
if __name__ == "__main__":
main()
ok 列の初期値を少し工夫しています。自信がある行は最初から○にしておくので、事務の方の作業は「全部に○を付ける」から「怪しい行だけ見て×にする、または直す」に変わります。これだけでも確認の時間はかなり変わります。
4. 確認済みの行を、いつものExcelに追記する
ここが「様式を変えない」の本体です。既存のExcelをテンプレートにするのではなく、今使っているファイルそのものに行を足します。
import csv
from pathlib import Path
from openpyxl import load_workbook
PENDING = Path(r"\\nas\nippo\pending.csv")
EXCEL = Path(r"\\nas\nippo\生産日報.xlsx") # 事務の方が今使っているファイル
SHEET = "日報"
# Excelの列順に合わせる(1列目=日付 … の並び)
ORDER = ["date", "line", "part_no", "good_qty", "defect_qty", "defect_note", "stop_min", "remarks"]
INT_COLS = {"good_qty", "defect_qty", "stop_min"}
def main():
rows = list(csv.DictReader(PENDING.open(encoding="utf-8-sig")))
todo = [r for r in rows if r["ok"] == "○"]
if not todo:
return
wb = load_workbook(EXCEL)
ws = wb[SHEET]
for r in todo:
ws.append([int(r[c]) if c in INT_COLS and r[c] else r[c] for c in ORDER])
wb.save(EXCEL)
# 追記した行は pending から除く(履歴は done フォルダの写真で追える)
rest = [r for r in rows if r["ok"] != "○"]
with PENDING.open("w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys() if rows else [])
w.writeheader(); w.writerows(rest)
if __name__ == "__main__":
main()
注意点が2つあります。
-
openpyxlはExcelのマクロや一部の書式を落とします。 集計シートに数式があるくらいなら問題ありませんが、マクロ付き(.xlsm)やピボットがある場合は、
keep_vba=Trueを付けるか、追記専用の入力シートを別ファイルにして、集計側から参照する形にしています。 - 誰かがファイルを開いていると保存に失敗します。 定期実行は事務所が閉まっている時間(早朝5時など)にしておきます。
5. 定期実行と「翌朝に届く」
Windowsのタスクスケジューラで、run_extract.py を1時間おき、append_excel.py を毎朝5時に回しています。集計シートが数式で組まれていれば、朝にExcelを開くだけで前日分が反映されています。
もう一歩進めるなら、集計結果を画像にしてLINE WORKSやTeamsに送ると、朝に数字が届く状態にできます。こちらはまた別の記事で書こうと思います。
精度と現場の話
この形で回してみると、ずれやすいのは「ライン名の略し方」や「品番の0とO」のような、人でも迷うところです。対策は2つで、どちらもAI側というより運用側の工夫です。
- 品番とライン名のマスタを渡して、このリストのどれかに正規化して返すよう指示する
- 用紙の記入欄を少し広げる、品番欄だけ活字のラベルを貼る、など紙の側を直す
紙をやめるより、紙のまま転記だけやめるほうが、現場には受け入れてもらいやすいと感じています。入力のやり方が変わらないので、教育もほとんど要りません。
まとめ
- 読み取る項目を先にスキーマで決めて、AIにはその形で返してもらう
- 自信がない項目は空にして申告してもらい、人の確認に回す
- 今のExcelの様式は変えずに、最終行に追記する
- 定期実行で、翌朝には反映されている状態にする
同じやり方で、検査記録や建設業の作業日報、介護記録(音声メモ)、FAXの注文書にも使えます。製造業向けの事例と料金はこちらにまとめております。
中小企業最適化ラボ 吉川
Discussion