BUSINESS APP

Pythonで業務アプリを作る|Excelの台帳を社内で使える小さなアプリに

貸出・在庫・顧客の台帳をExcelで回していると、上書きや入力ゆれが起きます。tkinterとSQLiteで113行の台帳アプリを作り、EXEにして台帳の管理担当に渡すところまでを、テストと実測で確かめた範囲でまとめました。

🎯 対象: Excelの台帳をアプリにしたい会社員(Python初級〜中級) ⏱️ 読了: 約10分 🧪 実行確認: Windows 11(EXEも起動)・Linux Mint 22.3

1. 結論:表1つで作り、管理担当1人のPCで使う

Excelの台帳では、誰かが行を消す・日付の書き方がばらつく・最新版が分からない、といった事故が起きます。入力欄と一覧だけのアプリなら、決まった形でしか書き込めません。

✅
先に結論

画面はtkinter、保存は標準のsqlite3で作れ、追加インストールは要りません。EXEにするならDBをEXEと同じフォルダに置くコードにします。配るときはDBを共有フォルダに置かず、管理担当1人のPCで使う台帳として渡します。

GUIライブラリの選び方はPython GUIライブラリの比較にあります。

🧪
検証環境(2026-10-19 時点)

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 NULL2026-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ウィジェット一覧へ。

備品貸出台帳の画面。上に備品・借りる人の入力欄と貸出ボタン、一覧にノートPC 03・プロジェクター・延長コード・Webカメラ 2の4行があり、延長コードだけ返却日が入っている。下に返却にする・バックアップのボタン。人名は架空
▲ 4件を貸し出し、1件を返却して「未返却のみ」を外した状態(Linux Mint 22.3の仮想マシンで撮影・データは架空)

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 に替えたもの)。

サイズとビルド時間(実測)

形式サイズビルド時間
--onefile11,310,797〜11,311,802バイト(約10.8MB)10.5秒・11.2秒
--onedir26,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 も消えるので、先に退避してください。

6. 社内で配る前に確認すること

① 会社の規程と情報システム部門

会社のPCにソフトを入れたり配ったりしてよいかは、会社ごとの規程で決まります。配る前に情報システム部門へ規程と手続きを確認してください。止められたときにウイルス対策の設定を変えてもらう依頼は、相手のPCの防御を弱めるので避けます。

② SmartScreen・スマートアプリコントロール・誤検知

MicrosoftのSmartScreenの説明では、署名のないEXEは「WindowsによってPCが保護されました」と警告され、利用者が「実行」を選ぶまで動きません。企業のポリシーでは続行自体を禁止できます。

  • Windows 11のスマートアプリコントロールは、評判が十分でない未署名ファイルをブロックする
  • 署名しても、評判がたまるまで警告は出うる(基準は非公開)
  • 誤検知はファイル提出ページから報告できる

Microsoftがストア外配布向けに勧める署名サービスはArtifact Signingです。2026-10-19時点のMicrosoft Learnでは、日本の対象は組織のみで、個人は米国・カナダ在住者に限られます。署名の要否は情報システム部門と相談して決めてください。

③ DBを共有フォルダに置かない

このコードはDBをEXEの隣に作ります。EXEを共有フォルダに置いて各自が起動すると、複数のPCが共有フォルダの同じDBを書き換える形になります。

SQLite公式はこの使い方を避けるよう書いています。多くのネットワークファイルシステムでロックに不具合があり壊れうること、WALモードはそこでは動かないこと(WAL)、ネットワーク越しの利用は試験されていないこと(公式)が理由です。

SQLiteは同時に書き込めるのが1つだけです。部署の台帳なら、管理担当1人のPCに置き、その人が貸出・返却を入力する運用にしてください。全員が書き込みたいなら、Excelの共同編集のままにするか、サーバーのDB(PostgreSQLなど)とWebアプリの組み合わせを検討します。

④ 個人情報を入れるなら

Python標準のsqlite3には暗号化の機能がなく、SQLiteの暗号化はSEEという有償の追加機能です(SQLite公式)。DBファイルを手に入れた人は中身を読めるものとして扱ってください。

個人情報保護委員会のガイドライン(通則編)は、技術的安全管理措置としてアクセス制御やアクセス者の識別と認証などを挙げています。顧客や社員の情報を入れるなら、置き場所と見られる人の範囲を決め、会社の安全管理の規程に従ってください。

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の利用方針は免責事項をご覧ください。