Pythonで業務アプリを作る|Excelの台帳を社内で使える小さなアプリに
貸出・在庫・顧客の台帳をExcelで回していると、上書きや入力ゆれが起きます。tkinterとSQLiteで113行の台帳アプリを作り、EXEにして台帳の管理担当に渡すところまでを、テストと実測で確かめた範囲でまとめました。
1. 結論:表1つで作り、管理担当1人のPCで使う
Excelの台帳では、誰かが行を消す・日付の書き方がばらつく・最新版が分からない、といった事故が起きます。入力欄と一覧だけのアプリなら、決まった形でしか書き込めません。
画面はtkinter、保存は標準のsqlite3で作れ、追加インストールは要りません。EXEにするならDBをEXEと同じフォルダに置くコードにします。配るときはDBを共有フォルダに置かず、管理担当1人のPCで使う台帳として渡します。
GUIライブラリの選び方はPython GUIライブラリの比較にあります。
Windows 11 Pro。コードはPython 3.14.7と3.12.10で、テストコードから貸出・返却・バックアップ・画面の組み立てを確認しました。EXE(Python 3.12.10・PyInstaller 6.22.3)はonefile版を起動し、マウスとキー入力で貸出・返却・終了→再起動してデータが残ることを確認しました。
2. Excelの台帳をテーブル1つに写す
題材は「備品・借りた人・貸出日・返却日」の4列で回す備品の貸出台帳です。最初は1シート=1テーブル、1行=1レコードと考えれば、そのまま写せます。
| Excelの列 | テーブルの列 | 決め方 |
|---|---|---|
| (行番号) | id INTEGER | 自動の通し番号 |
| 備品 | item TEXT NOT NULL | 空欄を許さない |
| 借りた人 | borrower TEXT NOT NULL | 空欄を許さない |
| 貸出日 | lent_on TEXT NOT NULL | 2026-10-19 形式の文字列 |
| 返却日 | returned_on TEXT | 空(NULL)なら未返却 |
- 必須の列は NOT NULL:空欄の行をDBが受け付けません。
- 「状態」の列を作らない:貸出中かどうかは返却日が空かで決まります。別の列にすると、片方だけ直して食い違います。
- 日付は年-月-日の文字列:文字列の並びが日付順と一致します。date型を渡したときの既定の変換は3.12で非推奨になりました(公式)。
既存のExcelからデータを移す処理は入れていません。読み込み方はPythonでExcel作業を自動化する記事にあります。
3. コード全文:備品貸出台帳
lending.py として保存し、python lending.py で起動します。同じフォルダに lending.db ができます。
"""備品貸出台帳(tkinter × SQLite)"""
import sqlite3
import sys
import tkinter as tk
from contextlib import closing
from datetime import date, datetime
from pathlib import Path
from tkinter import messagebox, ttk
# EXE化しても「EXEと同じフォルダ」にDBを置く
if getattr(sys, "frozen", False):
BASE = Path(sys.executable).parent
else:
BASE = Path(__file__).resolve().parent
DB = BASE / "lending.db"
def connect():
con = sqlite3.connect(DB)
con.execute("""CREATE TABLE IF NOT EXISTS loans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item TEXT NOT NULL,
borrower TEXT NOT NULL,
lent_on TEXT NOT NULL,
returned_on TEXT)""")
return con
def lend(item, borrower):
# closingで必ず閉じる・with conで成功ならcommit、例外ならrollback
with closing(connect()) as con, con:
con.execute(
"INSERT INTO loans (item, borrower, lent_on) VALUES (?, ?, ?)",
(item, borrower, date.today().isoformat()))
def give_back(loan_id):
with closing(connect()) as con, con:
con.execute(
"UPDATE loans SET returned_on = ? WHERE id = ? AND returned_on IS NULL",
(date.today().isoformat(), loan_id))
def fetch(open_only):
sql = "SELECT id, item, borrower, lent_on, returned_on FROM loans"
if open_only:
sql += " WHERE returned_on IS NULL"
with closing(connect()) as con:
return con.execute(sql + " ORDER BY id DESC").fetchall()
def backup():
folder = BASE / "backup"
folder.mkdir(exist_ok=True)
dest = folder / f"lending_{datetime.now():%Y%m%d_%H%M%S}.db"
with closing(connect()) as src, closing(sqlite3.connect(dest)) as dst:
src.backup(dst) # 使用中でも安全にコピーできる
return dest
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("備品貸出台帳")
form = ttk.Frame(self, padding=8)
form.pack(fill="x")
ttk.Label(form, text="備品").grid(row=0, column=0)
self.item = ttk.Entry(form, width=16)
self.item.grid(row=0, column=1)
ttk.Label(form, text="借りる人").grid(row=0, column=2)
self.who = ttk.Entry(form, width=12)
self.who.grid(row=0, column=3)
ttk.Button(form, text="貸出", command=self.on_lend).grid(row=0, column=4)
self.open_only = tk.BooleanVar(value=True)
ttk.Checkbutton(form, text="未返却のみ", variable=self.open_only,
command=self.refresh).grid(row=1, column=1, sticky="w")
cols = ("id", "item", "borrower", "lent_on", "returned_on")
self.tree = ttk.Treeview(self, columns=cols, show="headings", height=10)
for c, label in zip(cols, ("No", "備品", "借りた人", "貸出日", "返却日")):
self.tree.heading(c, text=label)
self.tree.column(c, width=90)
self.tree.pack(fill="both", expand=True, padx=8)
bar = ttk.Frame(self, padding=8)
bar.pack(fill="x")
ttk.Button(bar, text="返却にする", command=self.on_return).pack(side="left")
ttk.Button(bar, text="バックアップ", command=self.on_backup).pack(side="right")
self.refresh()
def refresh(self):
self.tree.delete(*self.tree.get_children())
for row in fetch(self.open_only.get()):
self.tree.insert("", "end", values=[v or "" for v in row])
def on_lend(self):
item, who = self.item.get().strip(), self.who.get().strip()
if not item or not who:
messagebox.showwarning("入力不足", "備品と借りる人を入れてください")
return
lend(item, who)
self.item.delete(0, "end")
self.refresh()
def on_return(self):
for iid in self.tree.selection():
give_back(int(self.tree.item(iid, "values")[0]))
self.refresh()
def on_backup(self):
messagebox.showinfo("バックアップ", f"保存しました:\n{backup()}")
if __name__ == "__main__":
App().mainloop()
備品と借りる人を入れて「貸出」、行を選んで「返却にする」。「未返却のみ」を外すと返却済みも出ます。「バックアップ」は backup フォルダに日時つきのコピーを作ります。部品の使い方はtkinterウィジェット一覧へ。
4. コードの要点4つ
① 入力値は ? で渡す(SQLインジェクション対策)
入力値を文字列でSQLにつなげると、SQLを紛れ込ませる攻撃(SQLインジェクション)を受けます。Python公式もプレースホルダーを使うよう書いています。テストで備品名に x'); DROP TABLE loans;-- を入れると、文字列のまま保存され、テーブルは消えませんでした。
② 閉じる closing と、確定・巻き戻しの with con を重ねる
with con: は正常に抜ければcommit、例外ならrollbackします。ただし公式のとおり接続は閉じないので、closing() と2段に重ねて必ず閉じています。テストで備品名を空(None)にして NOT NULL に違反させると IntegrityError になり、先に入れた3件のまま残りました。
返却では、一覧から取ったidを int() にして渡し、UPDATE に AND returned_on IS NULL を付けています。返却済みのidで give_back() をもう一度呼んでも更新されません。2段重ねは配当金管理ツールの5章へ。
③ DBの場所は sys.executable 基準
冒頭の sys.frozen はPyInstallerのEXEで動くときだけ真になります。そのときはEXEのフォルダ、.py のときはスクリプトのフォルダにDBを置きます。sqlite3.connect("lending.db") のような相対パスだと、DBの場所は起動時のカレントフォルダ次第です。
④ バックアップは Connection.backup
書き込み中にDBファイルを普通にコピーすると、壊れたコピーになることがあります(SQLite公式)。Connection.backup() は使用中でも安全に複製でき、テストでは3件そろったコピーを確認しました。保存先はDBと同じPCの backup フォルダなので、故障に備えて会社が認める保存先へ定期的に移してください。
5. EXEにする:サイズとDBの場所を実測
Pythonが入っていないPCでも使えるよう、PyInstallerでEXEにします。基本はPyInstallerのEXE化ガイドにあります。
pip install pyinstaller
pyinstaller --onefile lending.py
pyinstaller --onefile --windowed lending.py
2行目でできた dist\lending.exe を起動して動作とエラー表示を確かめ、問題がなければ3行目でコンソール窓のない版を作ります(--windowed はエラーも見えなくなります。ガイドの3章)。下の表は --windowed 付きの版です(onedirは --onefile を --onedir に替えたもの)。
サイズとビルド時間(実測)
| 形式 | サイズ | ビルド時間 |
|---|---|---|
| --onefile | 11,310,797〜11,311,802バイト(約10.8MB) | 10.5秒・11.2秒 |
| --onedir | 26,923,655バイト(約25.7MB・944ファイル) | 未計測 |
値は build と dist を消して2回ビルドした結果です(サイズの幅はその揺れ)。onefileのEXEを別のフォルダから起動しても lending.db はEXEの隣にでき、貸出2件・返却1件のあと×で閉じて再起動すると2件とも残っていました(2026-10-19・Windows 11 Pro)。
onefileでは __file__ が一時フォルダを指す(実測)
パスを表示するだけのコンソールEXEを --onefile で作り、別のフォルダにコピーして実行しました。Path(__file__).parent と sys._MEIPASS は C:\Users\…\AppData\Local\Temp\_MEI000034b82 という一時フォルダを、Path(sys.executable).parent はEXEを置いたフォルダを指しました。
onefileは起動のたびに一時フォルダへ展開し、通常終了でそのフォルダを消します(PyInstaller公式)。__file__ 基準だと、DBの置き場所が終了時に消えるフォルダになります。sys.executable 基準のDBはEXEの隣にでき、2回実行すると行数が1から2に増えて残りました。onedirでも sys.executable はフォルダ内のEXEを指す仕様で、DBもEXEの隣にできました(保持の確認はonefileのみ)。
毎日使うならonedirも候補
起動してウィンドウが出るまでは、onefileがビルド直後の初回3.68秒・2回目以降の中央値1.15秒(7回)、onedirが起動できた初回3.69秒・以降の中央値0.22秒(4回)でした。ガイドの値とは計測方法が違うので比べません。
onefileは警告もブロックも出ず、onedirは最初の起動要求1回が「アプリケーション制御ポリシーによってこのファイルがブロックされました」(WinError 4551)で拒否され、すぐ起動し直すと動きました。どちらもビルドしたPCでの結果で、ダウンロードなどで受け取ったPCとは条件が違います。止められ方はPCで変わりうるので6章②へ。
毎日開く台帳ならonedirも候補です。ただしフォルダごと新しい版に差し替えると中の lending.db と backup も消えるので、先に退避してください。
7. 次に作るなら:中級のSQLiteアプリ
中級アプリには、tkinterと標準のsqlite3だけで動くDBアプリがあります。DBブラウザ以外の5本は、CREATE TABLE IF NOT EXISTS でテーブルを作り、値を ? で渡す点が今回と同じです。
| アプリ | 増える学びどころ |
|---|---|
| 在庫管理システム | 商品と入出庫履歴の2テーブル |
| 顧客管理CRM | 顧客と対応履歴の2テーブル・CSV書き出し |
| タスク管理(DB付き) | 状態・カテゴリ・優先度で絞り込む検索 |
| ユーザー管理システム | パスワードのハッシュ化と権限 |
| 時間追跡アプリ | 作業時間の記録と週次の集計 |
| SQLiteデータベースブラウザ | 任意のDBの中身表示とSQL実行 |
台帳の次は、在庫管理かCRMで「テーブル2つ」の設計に進めます。ほかは中級アプリ一覧へ。なお5本はDBの場所を __file__ 基準で決めているので、EXEにするなら4章③の sys.executable 基準に書き換えてください。
8. よくある質問(FAQ)
Q. 共有フォルダにDBを置いて、部署の全員で使えますか?
おすすめしません。SQLite公式は、ネットワーク越しに複数のPCから同じDBへ直接アクセスする使い方を避けるよう書いています。台帳は管理担当1人のPCで使ってください。
Q. EXEにしたら、DBファイルはどこにできますか?
この記事のコードではEXEと同じフォルダです。__file__ 基準だと、onefileでは一時フォルダを指しました(5章の実測)。
Q. 「WindowsによってPCが保護されました」と出て起動できません。
署名のないEXEに出るSmartScreenの警告です。会社のPCではポリシーで続行できないことがあります。設定変更ではなく、配り方を情報システム部門に相談してください。
Q. SQLiteのDBファイルは暗号化されていますか?
Python標準のsqlite3には暗号化の機能がありません。中身は読めるものとして扱い、個人情報を入れるなら置き場所と見られる人を会社の規程に沿って決めてください。
9. 次の一歩
- 自分の台帳に置き換える:列名と画面の項目を手元のExcelに合わせます。入力ミスを直すボタンはないので、当面はアプリを閉じ、バックアップを取ってからSQLiteデータベースブラウザで直し、慣れたら修正ボタンを足すのが最初の改造です。
- EXEで渡す前に:配布前チェックリストで、再起動してもデータが残るかを確かめます。
- ほかの題材を探す:Pythonで作れるもの一覧へ。
- SQLiteを体系的に学ぶ:『退屈なことはPythonにやらせよう 第3版』は16章が標準のsqlite3の章で、更新・削除やテーブルの結合まで扱います(参考書ランキングでの紹介。リンク先はアフィリエイト広告を含むページです)。
本記事は、生成AIを活用して下書きし、運営者が内容を確認・編集したうえで公開しています。AIの利用方針は免責事項をご覧ください。