更新履歴: 2026 年 9 月 3 日 — 6.22.2 を venv に入れ直して再計測し、exe の容量が何で決まるかの実測(tkinter を 1 つ import しただけで約 9MB 増える/--exclude-module の落とし穴)を 5 章に追加。あわせてよくある質問(FAQ)、exe 化しない配布方法との比較(1.2)、onedir と onefile の違いを示す図解(3.1)を新設し、6 章の見出しに扱う問題(文字化け・ウイルス誤検知・依存不足)を明示しました。2026 年 8 月 31 日 — PyInstaller 6.22.2(2026-08-17 リリース)で全体を再検証し、「使い方の全体像」「onefile と onedir の実測比較(ビルド時間・配布サイズ・起動時間)」「主要オプション早見表」「spec ファイルの位置づけ」を新設。あわせて、--add-data の区切り文字が公式では :(コロン)に変わっていること、同梱リソースの参照方法は sys._MEIPASS から __file__ 基準が公式の推奨に変わっていることを反映しました(どちらも PyInstaller 5.x 時代の解説記事とは食い違う点です)。
1. PyInstaller とは — 何ができて、何ができないのか
製造業の現場 PC には、たいてい Python がインストールされていません。Windows 10 / 11 がプリインストールされた業務用 PC であり、勝手に python.exe を入れるのは情報システム部門のポリシーで禁止されているケースも多いはずです。
こうした環境に Python アプリを届ける現実解が、PyInstaller での .exe 化です。PyInstaller は、スクリプトと、それが import しているライブラリ、そして Python ランタイム一式をまとめて 1 つの実行ファイル(または 1 つのフォルダ)にパッケージします。処理は大きく 3 段構えです。
- 依存の収集: スクリプトの import を解析して、必要なモジュール・DLL・データを集める
- バンドル: Python ランタイムと集めたものを 1 つのアーカイブにまとめる
- ブートローダの付与: 起動用の小さなネイティブ実行ファイル(ブートローダ)を付ける。オペレーターがダブルクリックするのはこれで、ブートローダが同梱の Python を起動してスクリプトを実行する
この 3 段目があるおかげで、配布先に Python が無くてもアプリが起動します。逆に言えば、「Python 本体を持ち歩いている」だけなので、exe にしたからといってスクリプトが別物になるわけではありません。
| 項目 | 値(2026 年 9 月 3 日時点) |
|---|---|
| 最新安定版 | 6.22.2(2026-08-17 リリース) |
| 対応 Python | 3.8 以上 3.16 未満(パッケージメタデータの requires_python が >=3.8,<3.16) |
| 対応 Windows | Windows 8 以降(公式の要件ページ) |
| ライセンス | GPLv2 以降+例外条項(商用・非フリーのプログラムをビルドして配布することが認められている) |
| 既定の出力形式 | onedir(1 フォルダ)。--onefile を付けると単一 exe |
| 本記事の検証環境 | Windows 11 Pro / Python 3.12.10 / PyInstaller 6.22.2(venv) |
できないことも先に押さえておくと、後の手戻りが減ります。
- クロスコンパイルはできない: 配布先の OS ごとに、その OS 上で PyInstaller をインストールしてビルドする必要があると公式が明記しています。Windows の現場 PC に配るなら、Windows 実機か Windows 仮想マシンでビルドします
- ソースコードの秘匿手段としては不完全: PyInstaller に付属する
pyi-archive_viewerで、exe に同梱されたアーカイブの中身を一覧できます(筆者環境で実測。同ツールは中身の取り出しにも対応していますが、そちらは当サイトでは未実測です)。機密ロジックを守る目的で exe 化するのは筋が悪い、と考えてください - 速くなるわけではない: 実行しているのは同じ Python コードです。exe 化は「配れる形にする」工程であって、高速化とは無関係です
なお、本記事が配布対象として想定しているのは、tkinter で作る現場用監視アプリのような、オペレーターが操作するデスクトップアプリです。実装側の設計はそちらの記事にまとめています。
1.1 他の exe 化ツールとの使い分け
Python の exe 化ツールは PyInstaller だけではありません。本記事で実測したのは PyInstaller 6.22.2 だけです。下表の「位置づけ」は各ツールの公式ドキュメントが掲げる説明の整理、右列の「現場配布での判断」は筆者の判断です(PyInstaller 以外は当サイトでは未検証です)。ツール名から各公式ドキュメントに移動できます。
| ツール | 位置づけ | 現場配布での判断 |
|---|---|---|
| PyInstaller | 依存を収集し、ブートローダを付けて 1 つのフォルダ/ファイルに固める。Windows / macOS / Linux 対応 | 参考情報が見つけやすく、詰まったときに調べられる。本記事の推奨 |
| cx_Freeze | 同種のフリーズツール。ビルド構成を setup スクリプト(Python コード)で書く | ビルド設定をコードで管理したい場合の選択肢 |
| Nuitka | Python を C に変換してコンパイルする。方式が根本的に違う | 高速化が目的ならこちら。C コンパイラの用意が要る |
| auto-py-to-exe | PyInstaller の GUI フロントエンド。画面でオプションを組み立てる | 出力は PyInstaller と同じ。コマンドを覚える前の入口として |
迷ったら PyInstaller で構いません。筆者が調べた範囲では、日本語・英語ともに解説記事や Q&A の蓄積が多く、現場で詰まったときの復帰が速いためです(当サイトで件数を計測したものではなく、筆者の経験に基づく判断です)。本記事も以降は PyInstaller だけを扱います。
1.2 exe 化しない配布方法(zip+venv・共有フォルダ)と比べる
そもそも exe にする必要があるのか、を先に確認しておきます。配布方法は大きく 3 通りです。
- スクリプトを zip で渡す(+配布先で venv を作る): 渡すものが小さく、中身を読んで直せます。ただし配布先の PC に Python と、依存ライブラリを入れる手段(社内ミラーやプロキシ経由の
pip)が要ります - 共有フォルダに置いて
.batからpython app.pyを起動する: 更新はサーバー側のファイルを差し替えるだけで済みます。これも配布先に Python が必要で、しかも全台で同じバージョン・同じライブラリ構成に保つ運用が付いてきます - exe 化して配る: 配布物は大きくなりますが、配布先に Python を入れる話が消えます
判断軸はここだけです。配布先の PC に Python を入れられるか、入れた後もその環境を維持できるか。情報システム部門の許可が下りない、あるいは許可は下りても十数台の環境を揃え続ける担当が置けない——製造現場ではこちらが普通なので、exe 化が現実解になります。逆に、相手が開発者で Python が入っている前提なら、zip で渡すほうが軽くて速いです。
1.3 ライセンス — 商用配布と GPL の扱い
PyInstaller のライセンスは、1 章冒頭の表のとおり GPLv2 以降+例外条項です。例外条項が付いているので、PyInstaller で固めたこと自体を理由にソースコードの開示を求められることはありません(社内ツールでも市販ソフトでも配布できます)。ただし条件が 2 つあります。ひとつは、PyInstaller 本体のソースコードに変更を加え、その変更を配布する場合、変更部分は GPL の条件で配布する必要があること(公式ライセンスページの記述。6.2 で触れる「未改変のソースからブートローダをビルドし直す」行為は、ソースコードの変更には当たりません)。もうひとつは、exe に同梱した依存ライブラリのライセンスは、別途こちらの責任で満たす必要があることです。公式のライセンス解説も「PyInstaller が生成した実行ファイルバンドルは、依存ライブラリのライセンスに適合する限り、好きなライセンスで出荷できる」と書いています。GPL のライブラリを同梱した製品を出すなら、そこは PyInstaller とは別問題として確認してください。
2. 使い方の全体像 — 3 ステップで exe を作る
最小構成は「入れる → ビルドする → dist/ を確認する」の 3 ステップです。まずここまでを通してから、オプションを足していくのが早道です。本記事は PyInstaller 6.x を前提にしています。6.0 でフォルダ構成が大きく変わっているため、5.x 時代の解説記事とはスクリーンショットも説明も食い違う点に注意してください(詳細は 3.1)。
前提: exe を作る PC には Python が必要です。1 章で「入っていない」と書いたのは配布先の現場 PC の話で、ビルドする開発用 PC は別に用意します。開発機にも Python を入れられない場合は、情シスに開発用 PC か仮想マシンを 1 台用意してもらうところから始めてください。クロスコンパイルはできないため、Windows に配るなら Windows(実機か仮想マシン)でビルドします(1 章)。
2.1 ステップ 1: インストール(pip install pyinstaller)
python -m venv .venv
.venv\Scripts\activate
python -m pip install pyinstaller
pyinstaller --version
最後の行で 6.22.2 のようにバージョンが表示されれば導入完了です(筆者環境の実測値)。仮想環境(venv)に入れることを強く推奨します。グローバル Python に入れると、依存ライブラリの取り込みでバージョン衝突を起こすことがあります。1 行目と 2 行目が venv の作成と有効化です。
この venv への pip install に管理者権限は要りません。書き込み先が自分のユーザー領域にある .venv\ フォルダだからです。会社支給の PC で管理者権限が無くても、Python さえ入っていればここまでは進められます(ただし、その PC に Python 本体を新規インストールできるか、社内プロキシや閉域網で pip が外に出られるかは別問題です)。
PowerShell で activate が「スクリプトの実行が無効になっているため…」と弾かれる場合は実行ポリシーの緩和が必要です(コマンドプロンプトならそのまま通ります)。もうひとつ、社内プロキシで pip install が通らないケースもあります。
この 2 つが、社内のビルド用 PC で最初に詰まる定番です。どちらも 1 行で回避できます(以下は PowerShell での実行例です)。なお実行ポリシーの変更は会社支給 PC のセキュリティ設定に触れる操作なので、社内の IT 利用規程・情シスの承認の範囲内で行ってください(グループポリシーで強制されている環境では、そもそもこのコマンドは反映されません)。
# activate が「スクリプトの実行が無効…」で弾かれる場合
# この PowerShell の窓の中だけ緩和する。管理者権限は不要
Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned
# 社内プロキシで pip install が通らない場合(コマンドプロンプトでも同じコマンドです)
python -m pip install --proxy http://<プロキシのホスト>:<ポート> pyinstaller
プロキシの認証が必要な環境や、そもそも外部に出られない閉域網では、社内ミラーやオフラインインストールの手順を情シスに確認してください(閉域網での構築手順そのものはオフライン環境での Python セットアップで別途扱います)。実行ポリシーとプロキシの背景は、学習ロードマップ STEP 2 にまとめています。
2.2 ステップ 2: ビルドする
pyinstaller app.py
これだけで exe ができます。オプションを何も付けない場合、出力は onedir(1 フォルダ形式)です——公式ヘルプでも -D, --onedir に「(default)」と明記されています。「PyInstaller といえば単一 exe」という印象があるかもしれませんが、既定は逆です。
筆者環境(Windows 11 Pro / Python 3.12.10 / PyInstaller 6.22.2)で、Label を 1 つ置いただけの最小 tkinter アプリをビルドしたときの所要時間は 8 秒でした。依存の多いアプリではこれより時間がかかります(当サイトで実測したのはこの最小アプリだけです)。
2.3 ステップ 3: 生成物を確認する
ビルドが終わると、スクリプトと同じ場所に 3 つのものができます。ここを取り違えると「exe だけコピーしたら動かない」という定番のトラブルになります。
| 生成物 | 中身 | 配布するか |
|---|---|---|
dist/ | 配布物。onedir なら dist/app/ フォルダ、onefile なら dist/app.exe | する(onedir はフォルダごと) |
build/ | 依存解析の結果やログなどの中間ファイル | しない(消してよい。--clean で毎回作り直せる) |
app.spec | ビルド設定ファイル。ビルドのたびに自動生成される | しない(ただしソース管理には入れる。4.3 参照) |
onedir の dist/app/ の中身は、PyInstaller 6.0 以降では app.exe と _internal/ フォルダだけです。依存 DLL・Python ランタイム・データ類はすべて _internal/ に入ります(5.x までは exe と同じ階層にフラットに並んでいました)。前述の最小 tkinter アプリでは、app.exe が 1.7MB、_internal/ の中身が 941 ファイルで、フォルダ合計 26MB でした。
ここまでで成功と判断してよい状態は、dist\app\ の中に app.exe と _internal\ が並んでいて、その app.exe をダブルクリックするとアプリの画面(GUI アプリならウィンドウ、コンソールアプリならコンソール窓)が出る、というところまでです。
動作確認は、ビルドに使った PC ではなく、Python の入っていない環境で行ってください。開発機では「たまたま入っているランタイム」に助けられて動いてしまうことがあります(6.5 参照)。
3. onefile と onedir — 実測で比較して選ぶ
PyInstaller で最初に迷うのがこの選択です。結論から書くと、現場 PC に置いて長く使うアプリは onedir(1 フォルダ形式)を推奨します。理由を実測値で示します。
3.1 仕組みの違い
onedir(既定・-D)は、exe と依存ファイルがそのままフォルダに並んだ状態で配布されます。実行時に展開処理は発生しません。6.0.0(2023-09-22 リリース)から、実行ファイル以外は _internal というサブフォルダに隠される構成に変わりました。
onefile(-F / --onefile)は、すべてを 1 つの exe に詰め込みます。ただし Python は「1 つのファイルの中身」を直接実行できないため、起動のたびに一時フォルダへ自己展開してから実行します。展開先は _MEIxxxxxx という名前のフォルダで(xxxxxx は乱数)、筆者環境での実測値は次のような場所でした。
sys._MEIPASS = C:\Users\<ユーザー名>\AppData\Local\Temp\_MEI000048ac2
この自己展開には、現場運用で効いてくる副作用が 2 つあります。1 つは起動が毎回遅くなること(公式の動作モード解説も「単一 exe は 1 フォルダ形式より少し起動が遅い」と説明しています)。もう 1 つは、アプリがクラッシュしたり強制終了されたりすると _MEIxxxxxx フォルダが消えずに残ることです(これも公式に明記されています)。異常終了を繰り返すアプリでは、現場 PC の %TEMP% に残骸が積み上がっていきます。
3.2 実測比較(PyInstaller 6.22.2 / Python 3.12.10 / Windows 11 Pro)
検証アプリは、Label を 1 つ表示して 200 ミリ秒後に自動終了する最小の tkinter GUI です。起動時間は「起動してから自動終了するまで」を各 2 回計測した値で、依存が少ない条件での測定である点にご注意ください。
| 項目 | onedir(既定) | onefile |
|---|---|---|
| コマンド | pyinstaller --noconfirm --clean hello.py | pyinstaller --noconfirm --clean --onefile --noconsole hello.py |
| ビルド時間 | 8 秒 | 10 秒 |
| 配布物 | dist/hello/(hello.exe 1.7MB + _internal/ 941 ファイル) | 単一 exe 1 ファイル |
| 合計サイズ | 26MB | 9.9MB |
| 起動時間(1 回目 / 2 回目) | 1.93 秒 / 0.50 秒 | 2.97 秒 / 1.51 秒 |
| 実行時の一時展開 | なし | 毎回 %TEMP%\_MEIxxxxxx へ展開 |
この実測から読み取れることは 2 つあります。
- onefile が必ず大きいわけではない: バンドルされたアーカイブは圧縮されるため、この条件では onefile 9.9MB に対し onedir は 26MB。サイズで不利なのはむしろ onedir でした。一方「配るファイルの数」は 942 個対 1 個で、こちらは onefile が圧倒的に有利です
- onefile は起動が毎回およそ 1 秒遅い: 差は 1 回目が +1.04 秒、2 回目が +1.01 秒で、ほぼ一定でした(2 回目が速いのは OS のファイルキャッシュが効くため)。この差は自己展開の時間なので、依存が増えるほど広がる方向です。当サイトで実測したのは上記の最小アプリだけで、numpy や Qt を含む大きなアプリの数値は測っていません
3.3 現場配布ではどちらを選ぶか
1 秒程度の起動差は、単発で使うツールなら誤差です。判断が変わるのは「毎日、複数台で、長く使う」場合です。
onedir を選ぶ理由:
- 起動が速い: 実測で毎回およそ 1 秒の差(1.93 秒 vs 2.97 秒、0.50 秒 vs 1.51 秒)。1 日に何度も開くアプリでは体感が変わります
- 差分更新ができる: アプリ本体だけ差し替え、巨大な依存 DLL は据え置く配布が可能です(9.1 の robocopy 方式)
- 切り分けが速い:
_internal/を見れば、必要な DLL やデータが同梱されているか目視で確認できます %TEMP%に残骸が出ない: 異常終了時の_MEIxxxxxx残留(3.1)が構造的に起きません
onefile が向く場面: メールや USB メモリで 1 回だけ渡す、社内チャットに添付して「試してみて」と配る、といった単発配布です。フォルダごと渡すより事故が少なく、「exe だけコピーして動かない」も起きません。
ひとつ補足します。「onefile はウイルス対策ソフトに引っかかりやすく、onedir なら誤検知が減る」という説明をよく見かけますが、当サイトでは検証しておらず、PyInstaller 公式ドキュメントにもその記述は見つけられませんでした。そのため、onedir を勧める根拠には入れていません(誤検知そのものへの対処は 6.2 で扱います)。
3.4 _internal の名前を変える・5.x の構成に戻す
_internal という名前は、6.0.0 で導入された既定値にすぎません。--contents-directory で変更できます。
_internal ではなく lib という名前にしたい場合:
pyinstaller --contents-directory lib app.py
5.x 以前のフラットな構成(exe と依存ファイルが同じ階層に並ぶ形)に戻したい場合:
pyinstaller --contents-directory . app.py
公式ヘルプにも「. を指定すると contents ディレクトリの無い旧来の onedir レイアウトに戻せる」と書かれています。使いどころは、既存の社内手順書やインストーラのスクリプトが 5.x 時代のフォルダ構成を前提にしているケースです。手順書を全部書き換えるより、ビルド側を合わせたほうが安全なこともあります。逆に言えば、新規に作るなら既定の _internal のままで構いません。このオプションは onedir 専用です。
4. 主要オプション早見表(6.22.2 実測)
PyInstaller のオプションは数が多く、公式ヘルプを頭から読むのは効率が悪い作業です。ここでは、筆者環境の pyinstaller --help(6.22.2)で実在を確認したもののうち、現場配布で実際に使うものだけを挙げます。
4.1 主要オプション一覧(早見表)
| オプション | 何をするか / いつ使うか |
|---|---|
| バンドル方式 | |
--onedir(-D) | 1 フォルダ形式で出力する。既定。現場に置いて長く使うアプリはこちら(3.3) |
--onefile(-F) | 単一 exe にまとめる。単発配布向け。起動時に自己展開が入る |
--contents-directory NAME | onedir で _internal に相当するフォルダ名を変える。. で 5.x 構成に戻る |
| 出力とビルド制御 | |
--name NAME(-n) | exe と spec の名前を指定する(既定はスクリプトのファイル名) |
--distpath DIR | 配布物の出力先を変える(既定は dist) |
--specpath DIR | spec ファイルの出力先を変える(既定はカレントディレクトリ) |
--noconfirm(-y) | 既存の出力ディレクトリを確認なしで上書きする。バッチや CI でビルドを回すときに必須 |
--clean | ビルド前にキャッシュと一時ファイルを削除する。「直したのに反映されない」ときの第一手 |
| 見た目・実行形態 | |
--noconsole / --windowed(-w) | コンソール窓を出さない。GUI アプリ用。コンソールアプリに付けてはいけない(画面が一切出なくなる) |
--icon FILE(-i) | exe のアイコンを指定する(Windows は .ico)。付けないと PyInstaller 既定のアイコンのまま配ることになる |
| 同梱と依存 | |
--add-data SRC:DEST | データファイルを同梱する。複数回指定可。区切りは : が公式表記(6.3) |
--hidden-import MODULE | 静的解析で拾えない import を明示する。複数回指定可。ModuleNotFoundError の定番対処(6.3) |
--exclude-module MODULE | 指定したモジュールを収集対象から外して配布物を小さくする。複数回指定可。使っているモジュールを外してもビルドは成功し、起動時に ModuleNotFoundError で落ちる(5.2) |
--noupx | UPX による圧縮を使わない(5.3) |
--python-option "X utf8" | Python の実行時オプション(X オプション)を exe に焼き込む。文字化け対策の UTF-8 モードはこれで固定する(6.1)。実行時の PYTHONUTF8=1 は exe 化したアプリには効かない |
| Windows 固有 | |
--uac-admin | 起動時に管理者への昇格を要求するマニフェストを埋め込む(8 章) |
--version-file FILE | exe にバージョン情報リソースを埋め込む。プロパティ画面に版数を出したいとき |
この表は 6.22.2 の --help 出力で実在を確認したものだけを載せています(各オプションの正式な説明は公式の使い方ページにあります)。ひとつだけ例外があり、--python-option は筆者の --help 控えに残っていなかったため、6.0.0 の変更履歴(X オプションのパススルー実装)と 6.22.2 のブートローダのソースで確認したものです。オプションはバージョンによって増減し、古い記事には現行版に無いものも混ざっています。手元の環境で pyinstaller --help を一度流して確認するのが確実です。
4.2 現場でよく使う組み合わせ(--onefile / --noconsole の定番)
使う場面ごとに、そのままコピーして使える形で挙げます。
(1) GUI アプリを 1 ファイルで手渡しする(--onefile --noconsole)
pyinstaller --onefile --noconsole --icon app.ico app.py
(2) 現場 PC に常設する(本記事の推奨形)
pyinstaller --noconsole --noupx --python-option "X utf8" --icon app.ico app.py
(3) 作り直しでゴミを残さない(ビルドを何度も回すとき)
pyinstaller --noconfirm --clean --onefile --noconsole app.py
(2) の --python-option "X utf8" は、文字化け対策として UTF-8 モードをビルド時に焼き込むオプションです。理由は 6.1 で説明します。オプションはすべて 1 行で書いてください。Web でよく見る \ での行継続は Unix シェルの記法で、PowerShell / コマンドプロンプトではエラーになります(PowerShell で複数行に分けたい場合は行末にバッククォート ` を置きます)。
4.3 spec ファイルの位置づけ
ビルドを 1 回でも実行すると、app.spec というファイルが自動生成されます(筆者環境でも hello.spec / hello_one.spec が生成されることを確認しました)。中身は Python スクリプトで、コマンドラインで指定したオプションが Analysis() や EXE() の引数として書き出されたものです。
使い方は次の 2 段階で考えると分かりやすくなります。
- 試行錯誤の段階:
pyinstaller app.pyにオプションを足しながら回す。spec は毎回自動生成されるので気にしない - 設定が固まった段階: 生成された spec をソース管理に入れ、以降は
pyinstaller app.specでビルドする。長大なコマンドを申し送りする必要がなくなり、別の担当者や別の PC でも同じ成果物を作れます
ここに罠があります。pyinstaller app.py のように .py を指定して実行すると、そのたびに app.spec が生成(上書き)されます。spec を手で編集した後にうっかり .py 指定でビルドすると編集内容が消えます。spec 運用に切り替えたら、以降は必ず pyinstaller app.spec でビルドしてください。
5. exe のサイズと UPX 圧縮
「Hello, world に近いアプリなのに数十 MB になった」というのは、PyInstaller では正常な結果です。Python ランタイムと標準ライブラリを丸ごと抱えるため、筆者環境の最小 tkinter アプリでも onedir で 26MB / onefile で 9.9MB でした。何がその容量を作っているのかを、実測で分解します。
5.1 exe のサイズは何で決まるか(実測: tkinter を 1 つ import しただけで約 9MB 増える)
2026 年 9 月 3 日に、同じ環境(PyInstaller 6.22.2 / Python 3.12.10 / Windows 11 Pro / onedir)で 2 つのスクリプトをビルドして比べました。サイズは 3.2 の表と同じ基準(配布フォルダの合計)で、ファイル数は app.exe を含む配布フォルダ内の総数です(tkinter 版の 942 は、3.2 の _internal/ 941 ファイル + app.exe 1 個と一致します)。
| ビルド対象 | コマンド | 配布フォルダ合計 | ファイル数 |
|---|---|---|---|
print("hello") だけのスクリプト(import なし) | pyinstaller --noconfirm --clean app_min.py | 17MB | 12 |
| Label 1 つの最小 tkinter GUI | pyinstaller --noconfirm --clean --noconsole app_tk.py | 26MB | 942 |
読み取れることは 2 つです。
- import が 0 でも 17MB が下限: Python ランタイムと標準ライブラリを抱える分で、ここは削れません。「数 MB の exe を作る」という期待は最初に捨ててください
- tkinter を 1 つ import しただけで +9MB、ファイル数は 12 → 942: 増えた 930 個の大半は Tcl/Tk のデータファイルです。サイズを決めているのは自分のコードではなく、import した先が連れてくるものだと分かります
numpy や pandas、Qt のような大きなライブラリを入れれば、この増え方はさらに大きくなる方向です(当サイトで実測したのは上記の 2 パターンだけで、それらの数値は測っていません)。「1 つ import を足す」という何気ない変更が配布物の容量に直結する、という感覚だけ持っておくと見積もりを外しません。
5.2 exe の容量を削減する方法と --exclude-module の落とし穴
最初に試すべきはアプリ専用の venv でビルドすることです(2.1)。開発機のグローバル環境には使わないライブラリが同居しがちで、PyInstaller は import を辿って集めるため、依存の起点が絞られるほど結果は小さくなります。効果は環境次第ですが、リスクがなく最初にやるべき手です。
次の手として --exclude-module(6.22.2 の --help で実在を確認)があります。指定したモジュールを収集対象から外すオプションで、効果は確かにあります。5.1 の tkinter アプリに付けて計測した結果がこちらです。
| ビルド対象 | コマンド | 配布フォルダ合計 | ファイル数 |
|---|---|---|---|
| 5.1 と同じ最小 tkinter GUI | 上記 + --exclude-module tkinter | 17MB | 12 |
26MB が 17MB になり、ファイル数も 942 から 12 に戻りました。9MB 削れています。ただし、この exe は起動しません。ビルドは警告で止まらず正常に終わり、成果物もできあがるのに、起動すると次のトレースバックを出して終了します。--noconsole 付きでビルドした exe を、そのままコマンドプロンプトから起動して取得した実測ログです(終了コード 1 は、プロセスの終了を待つシェル(Git Bash)から起動して観測した値です。コマンドプロンプトは GUI サブシステムの exe の終了を待たないため、直後に %errorlevel% を見ても 0 が返ります——切り分けのときはここに注意してください)。--noconsole でビルドした exe でも、未処理例外でスクリプトが終了する場合は、コンソールが無い状態のままダイアログ(タイトル「Unhandled exception in script」)にこのトレースバックが表示されます。エクスプローラーからダブルクリックした場合も同じダイアログが出ることを、当サイトで実機確認しています(PyInstaller 6.22.2)。
Traceback (most recent call last):
File "app_tk.py", line 1, in <module>
import tkinter as tk
ModuleNotFoundError: No module named 'tkinter'
当然といえば当然で、アプリが実際に使っているモジュールを除外したのだから動くはずがありません。怖いのはビルドが成功してしまうことです。--exclude-module の指定ミスは、ビルドログでは分からず、exe を起動して初めて分かります。ビルドが通ったから大丈夫だろうと現場 PC に配ってしまうと、オペレーターの前で起動しない exe が完成します(前述のとおり、--noconsole でビルドした exe でも、ダブルクリックで起動した時点でこのトレースバックはダイアログに表示されます。ただしコンソールに残る記録ではないため、オペレーターがダイアログを閉じてしまうと後から追えなくなります。切り分けは 6.5)。
結論はこうです。容量削減の第一手は専用 venv でのビルド(2.1)。--exclude-module は本当に使っていないモジュールにだけ使い、付けたら必ず配布先相当の環境(Python の入っていないクリーンな Windows)で起動確認する。数 MB を削る作業に、配布事故のリスクを載せる価値があるかは毎回考えてください。
5.3 UPX 圧縮を使うか、使わないか
UPX 圧縮も整理しておきます。UPX は実行ファイルを圧縮する外部ツールで、PyInstaller は UPX が見つかった場合にこれを使って収集済みバイナリを圧縮します。公式ドキュメントの UPX 節の記述から確実に言えるのは次の 3 点です。
- UPX が適用されるのは Windows のみ。他の OS では、UPX が見つかっても収集済みバイナリは処理されない
- Control Flow Guard(CFG)が有効な Windows DLL と、Qt5 / Qt6 のプラグインは、圧縮によって壊れうるため PyInstaller が自動的に除外する
- 無効化は
--noupx(全面)、個別除外は--upx-exclude
一方で断定しないことも明記しておきます。「UPX 圧縮するとウイルス対策ソフトに引っかかりやすくなる」という説明は日本語の解説記事で広く見かけますが、PyInstaller の公式ドキュメントにこの記述は見つかっていません。当サイトでも検証していないため、事実としては扱いません。
それでも本記事が --noupx を推奨形に入れているのは、配布物を毎回同じ状態にするためです。ビルドする PC の PATH に UPX があるかどうかで成果物が変わる状態は、現場配布では避けたい。加えて圧縮による破損リスク(CFG・Qt が自動除外されているのは、実際に壊れうるからです)も確実にゼロにできます。数 MB 削るより、毎回同じものが出てくることのほうが運用では効きます。
6. 製造現場特有の落とし穴と対策 — 文字化け・ウイルス誤検知・依存不足
動く exe を作るところまでは公式ドキュメント通りで進みます。ここからが、現場固有の問題です。
6.1 CP932(Shift_JIS)絡みの文字化け
Windows の現場 PC は、いまだに既定の文字コードが CP932(Shift_JIS の Microsoft 拡張)であるケースが大半です。Python の標準入出力・ログファイル・ファイル読み書きで文字コードを明示しないと、現場で文字化けや UnicodeDecodeError が頻発します。筆者も今回の検証中、CP932 のコンソールに UTF-8 の文字列を print して文字化けを再現しました。この問題は exe 化とは無関係に起きます。
対策の基本:
- ログファイルは
open(path, "w", encoding="utf-8")のように明示 - Python 自体を UTF-8 モードで動かす。重要: exe 化したアプリには実行時の環境変数
PYTHONUTF8=1は効きません(PyInstaller 6.0 以降、frozen アプリは隔離された埋め込みインタープリタとして動作します。6.0.0 の変更履歴は「frozen アプリはPYTHONUTF8環境変数の影響を受けなくなった」と明記しており、他の PYTHON* 系変数も同様に無視されます)。UTF-8 モードはビルド時に焼き込みます:pyinstaller --python-option "X utf8" --noconsole app.py(spec ファイルならoptions = [('X utf8', None, 'OPTION')]をEXE()に渡す。X オプション対応は PyInstaller 6.0 以降) - 外部から渡される CSV / 設定ファイルは、UTF-8 / CP932 / UTF-8 with BOM のいずれもありえると想定し、デコード側で柔軟に扱う
ひとつ注記です。6.0.0 の変更履歴は例として X utf8_mode=1 という書き方を載せていますが、6.22.2 のブートローダのソースが照合している X オプション名は utf8 です(当サイトでソースを確認)。本記事は X utf8 で統一しています。
このトピックは深いので、CP932 と UTF-8 の文字化け対策に体系的にまとめています。
6.2 ウイルス対策ソフトの誤検知(Windows Defender に exe を消される)と、実行制御によるブロック
PyInstaller でビルドした exe は、Microsoft Defender(旧 Windows Defender)や法人向けセキュリティソフトに「未知のプログラム」として警告される、最悪の場合は自動削除される、という事例がよくあります。これは個人ブログの体験談だけの話ではなく、PyInstaller 公式も認識している問題です。公式のブートローダ再ビルド手順のページでは、自分でブートローダをビルドし直す動機のひとつとして「事前ビルド済みブートローダが広く使われていることに起因するアンチウイルスの誤検知を避けたい場合」が挙げられています。
つまり誤検知の主因はあなたのコードではなく、世界中の PyInstaller 製 exe が同じブートローダを共有していることにあります。根本対処は公式手順に沿ったブートローダの再ビルドですが、C コンパイラ(Windows なら Visual C++ 2017 以降か MinGW)を用意して waf でビルドし、PyInstaller を入れ直す必要があります。社内の標準手順にするには重いので、まずは以下から検討してください。
- PyInstaller を最新版に保つ: ブートローダが更新されれば、検知パターンとの一致も変わります
--noupxを付ける: 圧縮の有無で配布物が変わる状態を避けます(5.3)- 情シスに事前申請する: 配布物のハッシュ・配置パス・用途を添えて例外登録を依頼します。現場で自動削除が起きてから動くと、信用の回復に時間がかかります
- コード署名する: ただし署名は誤検知そのものには直接効きません。効く可能性があるのは下の 6.2.1 の実行制御側で、それも信頼されたルートの CA が発行した証明書に限られます。費用対効果が立つのも、複数アプリ・複数拠点に継続的に配る場合です(コード署名の完全ガイドで実測込みで検証しています)
- 検疫されたときの復旧手順を決めておく: 誰に連絡し、誰が隔離を解除するかを配布前に決めます
ここで詰まる原因の多くは exe そのものではなく、社内での通し方です。「試作は動いたのに現場 PC に置けない」を先回りで潰す進め方は、製造業システム内製化の判断基準と進め方の 8.5「情報システム部門の承認が取れず、現場 PC に置けない」に整理しています。
6.2.1 もうひとつのブロッカー: 実行制御ポリシー(WDAC / SmartScreen)
ウイルス対策ソフトの「検知」とは別に、実行そのものを止める仕組みも現場配布のブロッカーになります。系統は 2 つあり、対処法が違います。まず、画面に出るメッセージから系統と依頼先を引いてください。
| 画面に出るメッセージ | 系統 | 誰に何を頼むか |
|---|---|---|
| 「ウイルスを検出しました」/exe が消える | ウイルス対策の検知 | 情シスに例外登録(ハッシュ・配置パス・用途を添える)→ 6.2 |
| 「組織のポリシーによりブロックされました」/exe は消えず、実行だけ拒否される | 実行制御(WDAC=App Control for Business / Smart App Control) | 情シスに許可登録。ビルドのたびに申請しなくて済むよう署名ベースか配置パスベースのルールを相談 |
| 「発行元不明」の警告が出るが、実行を続けるか選べる | 評判ベース(SmartScreen) | コード署名と実績の蓄積。ウイルス対策側の除外設定では消えません |
実行制御と評判ベースのどちらも、ウイルス対策側に除外設定を足しても解決しません。
検証に使った Windows 11 環境でも、新規ビルド直後の exe が実行制御機能に弾かれる事象を確認しました。直前まで同種の exe は実行できていたので、「ビルドし直してハッシュが変わった未知の exe が弾かれる」挙動です。ビルドのたびにバイナリが変わる PyInstaller とは相性の悪い仕組みだと分かります。
- 見分け方: 「ウイルスを検出しました」ではなく「組織のポリシーによりブロックされました」系のメッセージが出る。なお Smart App Control は個人用 PC でも同系統の文言を出すため、この文言が出た=会社が管理している端末、とは限りません。exe は削除されず、実行だけが拒否される
- 対応: 情シス経由で許可を得るしかありません。個別ハッシュでの許可はビルドのたびに申請が要るため、コード署名の証明書ベース、または配置パスベースのルールで許可してもらえるかを最初に相談するのが現実的です
- 巻き添えにも注意: 同じ理由で、PyInstaller 付属の
pyi-archive_viewer.exeのような署名のない補助 exe まで起動できなくなることがあります(実際に起動できない状態を確認しました)。この場合はpython -m PyInstaller.utils.cliutils.archive_viewerとモジュールとして呼ぶと、同じ処理を実行できます。ポリシーを無効化するものではなく、exe を起動しない別経路で同じ機能を使うだけですが、会社支給の PC では、この操作を含め社内の IT 利用規程・情シスの承認の範囲内で行ってください
配布計画の段階で、「更新のたびに許可を取り直す必要があるか」を情シスに確認しておいてください。ここを詰めずに作り込むと、完成してから配れないと分かる最悪の順番になります。
名称についての補足: Microsoft の現行ドキュメントでは、WDAC は App Control for Business という名称で説明されています。もともと Device Guard の一部として提供された機能で、公式の「アプリ コントロールと AppLocker の概要」は「“Device Guard” と “構成可能なコードの整合性” という用語は、グループ ポリシーを介してポリシーをデプロイする場合を除き、App Control では使用されなくなりました」としています。情シスに相談するときは、社内でどの名前が使われているかを先に確認しておくと話が早く進みます。
6.3 隠れた依存(hidden imports)とデータ同梱(--add-data)— ModuleNotFoundError の主因
PyInstaller は静的解析で依存を辿りますが、動的 import(importlib 経由、プラグイン構造など)は検出できません。また、テンプレートや CSV など「コードでは import していないがランタイムで読み込むファイル」は自動では含まれません。開発機では元のファイルがそこにあるので動いてしまい、配布先で初めて発覚します。
対策: 明示的に指定します。
pyinstaller --noconsole --hidden-import pymodbus.transaction --add-data "config.yaml:." --add-data "templates:templates" app.py
--add-data の書式は SRC:DEST で、DEST は配布物の中での置き場所です。. を指定するとアプリのトップレベル(onefile なら一時展開先の直下、onedir なら _internal/ 直下)に置かれます。
区切り文字の最新事情(ここは古い記事と食い違います)
日本語の解説記事の多くは「Windows はセミコロン ;、Mac / Linux はコロン :」と説明しています。これは 5.x 時代の話です。PyInstaller 6.0.0 で POSIX 形式の source:dest が正式な表記として採用され(6.0.0 の変更履歴)、公式ヘルプも「両方のパスはコロン(:)で区切る」と書いています。Windows の source;dest は後方互換のために動き続けるものの、公式には非推奨(discouraged)という位置づけです。
筆者環境(6.22.2 / Windows 11)で両方を実測したところ、次の結果でした。
--add-data "config.txt;."(セミコロン): ビルド成功。実行時に同梱ファイルを読み込めることまで確認--add-data "config.txt:."(コロン): ビルド成功。pyi-archive_viewerでアーカイブの中身を実査したところ、セミコロン版とまったく同じ位置に同梱されていました
「コロンだと C:\data のドライブレターが区切りと誤認されるのでは」と心配になりますが、PyInstaller の実装は先頭のドライブレターを区切り判定から除外しています(makespec.py の正規表現で確認)。
結論はシンプルです。これから書くなら : に統一してください。既存の社内ビルドスクリプトの ; は当面動きますが、公式が非推奨とする表記に依存し続ける理由はありません。
指定が増えてコマンドが長くなってきたら、spec ファイルに切り出してください(4.3)。
6.4 同梱リソースのパス解決 — 公式は __file__ 推奨に変わっている
exe 化したアプリでパスを扱うときは、種類の違う 3 つの場所を混同しないことが重要です。ここを取り違えると「開発中は動くのに exe だと設定ファイルが見つからない」「onefile では動くのに onedir では読めない」が起きます。
| 求めたいもの | 使うもの | onefile での実体 | onedir での実体 |
|---|---|---|---|
--add-data で同梱した読み取り専用リソース | __file__ 基準(公式推奨) | 一時展開先 _MEIxxxxxx の配下 | _internal/ の配下 |
| exe の隣(現場が編集する設定・ログ) | sys.executable の親フォルダ | exe を置いた場所 | exe を置いた場所 |
| exe 化されているかの判定 | sys.frozen | True | True |
公式の推奨が変わった点を押さえてください。公式のランタイム情報の解説はかつて sys._MEIPASS を使う方法を案内していましたが、現在は「__file__ は常に絶対パスに設定されるようになったので、こうしたリソースの場所を求めるには __file__ のほうが望ましい」と明記しています。日本語の解説記事はいまだに sys._MEIPASS 一択で書かれていることが多く、ここは差分の大きいポイントです(sys._MEIPASS が使えなくなったわけではありません。onefile では一時展開先、onedir では _internal を指します)。
import sys
from pathlib import Path
def resource_path(name: str) -> Path:
"""--add-data で同梱した読み取り専用リソースの場所を返す。"""
return Path(__file__).resolve().parent / name
def app_dir() -> Path:
"""exe の隣(現場が編集する設定・ログの置き場)を返す。"""
if getattr(sys, "frozen", False):
return Path(sys.executable).resolve().parent
return Path(__file__).resolve().parent
TEMPLATE = resource_path("template.csv") # 同梱リソース(--add-data "template.csv:.")
CONFIG_PATH = app_dir() / "config.yaml" # exe の隣に外置き
LOG_DIR = app_dir() / "logs"
resource_path() を使うときの注意点として、__file__ はその関数が書かれているモジュールの位置を指します。パッケージのサブモジュールに置くと基準がサブフォルダにずれるため、--add-data の DEST をモジュールと同じ相対位置に合わせるか、この関数をエントリスクリプト側に置いてください。
使い分けの原則はこうです。読み取り専用の同梱リソース(テンプレート・アイコンなど)は同梱して resource_path() で参照し、現場が書き換える設定・ログ・出力データは同梱せず app_dir() で「exe の隣」に外置きします。同梱側は onefile だと実行のたびに作り直される一時フォルダなので、そこへ書き込んでも次の起動には残りません。
6.5 他の PC で exe が起動しない — 謎エラーの切り分け(DLL 不足、VC++ ランタイム)
「開発機では動くのに、現場 PC で起動した瞬間にエラー、もしくは何も起きない」というケース。かつては配布先に Visual C++ 再頒布可能パッケージが無いことが定番の原因でしたが、Python 3.5 以降は OS 同梱の Universal CRT(UCRT)ベースになり、PyInstaller も vcruntime140.dll 等を自動同梱するため、配布先が Windows 10 / 11 ならこの問題はほぼ起きません。現在の主な原因は、特定バージョンの DLL に依存するライブラリや、開発機にだけ入っている追加ランタイムです。
切り分けの定石は、まずエラーを見えるようにすることです。
- まずビルドし直さずに、その exe をコマンドプロンプトから起動する。
--noconsole付きでも、呼び出し元のコンソールに例外が出ることがあります(5.2 はこの方法で取得した実測ログです)。それでも何も出ない場合は--noconsoleを外してビルドし直し、同じくコマンドプロンプトから実行する(ダブルクリックだと窓が一瞬で閉じて読めません) - クリーンインストール直後の Windows 仮想マシンを 1 台用意し、配布物を置いて起動するだけのテストを毎回行う
- 長期間更新されていない古い Windows が残る現場では、VC++ 再頒布可能パッケージ(最新版)を配布物の
install/に同梱し、初回セットアップで入れてもらう - 起動失敗時にログを残せるよう、
.batランチャー経由で起動する設計にする(7 章)
なお、ModuleNotFoundError が出る場合は依存の取りこぼしなので 6.3 の --hidden-import を、「組織のポリシーによりブロックされました」が出る場合は 6.2 の実行制御を見てください。エラーメッセージの種類で原因の系統が分かれます。
配布先の画面に出た文言(一瞬で閉じる・Failed to execute script・DLL が見つからない、など)から原因を逆引きしたい場合は、症状別に整理したexe が他の PC で動かない — PyInstaller のエラー別 原因逆引きガイドにまとめています。
7. .bat ランチャーで起動を堅牢にする
exe を直接ダブルクリックさせるのではなく、.bat 経由で起動する設計にすると、現場運用が一気に楽になります。オペレーターの操作は「このアイコンをダブルクリック」のまま変わらないのに、ログ・作業ディレクトリ・再起動をこちらで制御できるようになるためです。
(画面を持たない常駐処理で、ログオンしていなくても動き続けてほしい場合は、Windows サービス化という選択肢もあります。オペレーターが画面を見るアプリなら、本章のランチャー方式のほうが扱いやすいです。)
7.1 ランチャー .bat のサンプル
@echo off
chcp 65001 > nul
setlocal
REM 作業ディレクトリを .bat のあるフォルダに固定
REM (UTF-8 モードは exe には環境変数で効かないため、6.1 のとおりビルド時に焼き込む)
cd /d "%~dp0"
REM ログフォルダ
if not exist logs mkdir logs
REM 起動(標準出力・標準エラーともログへ)
"%~dp0app\app.exe" 1>> "logs\stdout.log" 2>> "logs\stderr.log"
if errorlevel 1 (
echo [%date% %time%] app exited with errorlevel %errorlevel% >> logs\stderr.log
exit /b %errorlevel%
)
endlocal
ポイント:
"%~dp0app\app.exe"のapp\は、.batと同じ場所に置いたアプリ本体のフォルダです(フォルダ構成は 9.3 の配置を前提にしています)chcp 65001でコンソールを UTF-8 に。> nulは出力を抑止%~dp0は.batファイルが置かれたフォルダ。常にここを基準にすることで「ショートカットから起動した」「別フォルダから呼ばれた」場合でも作業ディレクトリが安定- 標準出力・標準エラーを別ファイルに記録。「現場で何も起こらず終了した」現象の原因はだいたいここに残ります
--noconsoleでビルドしている場合、stderr.logに Python のトレースバックが出ないこともあります。その場合はアプリ側でloggingをファイルに向けておく設計が重要(24 時間動くアプリのロギング設計を参照)
7.2 自動再起動付きランチャー
監視アプリのように 24 時間動かし続けたい場合、異常終了時に自動再起動するランチャーが便利です。
@echo off
chcp 65001 > nul
setlocal
cd /d "%~dp0"
if not exist logs mkdir logs
:loop
"%~dp0app\app.exe" 1>> "logs\stdout.log" 2>> "logs\stderr.log"
echo [%date% %time%] restarted (errorlevel %errorlevel%) >> logs\stderr.log
timeout /t 5 /nobreak > nul
goto loop
このランチャーは正常終了でも再起動します(オペレーターがアプリを閉じても再び立ち上がる)。止めるときは .bat のコンソール窓ごと閉じてください。意図的な終了でループを抜けたい場合は、app.exe の直後に if %errorlevel%==0 goto :eof の分岐を入れます。また、無限再起動はアプリ側のバグで暴走する危険もあるため、本番では「N 回連続で失敗したら停止」のようなガード(カウンタ管理)を加えてください。あるいは Windows のタスクスケジューラから「失敗時に再実行」する方法でも代用できます。
ガードまで入れると、本番で使える形はこうなります。正常終了ではループを抜け、異常終了のときだけ再起動し、5 回連続で失敗したら止まります。
@echo off
chcp 65001 > nul
setlocal enabledelayedexpansion
cd /d "%~dp0"
if not exist logs mkdir logs
set /a FAIL=0
:loop
"%~dp0app\app.exe" 1>> "logs\stdout.log" 2>> "logs\stderr.log"
REM 直後に終了コードを退避する(set /a を挟むと errorlevel が変わるため)
set RC=%errorlevel%
if "%RC%"=="0" goto :end
set /a FAIL+=1
echo [%date% %time%] abnormal exit errorlevel=%RC% fail=!FAIL! >> "logs\stderr.log"
if !FAIL! GEQ 5 (
echo [%date% %time%] 5 回連続で失敗したため再起動を停止しました >> "logs\stderr.log"
goto :end
)
timeout /t 5 /nobreak > nul
goto loop
:end
endlocal
この .bat は UTF-8(BOM なし)で保存してください。先頭で chcp 65001 を実行しているため、以降の行は UTF-8 として解釈され、日本語のメッセージも化けずにログへ書き込まれます(logs\stderr.log 側も UTF-8 になるので、開くときは UTF-8 対応のエディタで)。BOM 付きで保存すると 1 行目が認識されず起動しません。エディタの既定が Shift_JIS の場合は、保存時に文字コードを UTF-8(BOM なし)に変えるか、日本語のメッセージを英数字(例: restart guard stopped after 5 failures)に置き換えてください。(.bat と .ps1 で BOM の扱いが変わる話は、PowerShell と .bat の文字コードで扱います。)
goto によるループでは 1 行ずつ読み直されるため、%FAIL% と書いても値は取れます。ここで setlocal enabledelayedexpansion を有効にし !FAIL! で書いているのは、後から for ループや括弧ブロックに組み替えたときに壊れないようにするためです(ブロックの内側では %FAIL% がブロック開始時の値に固定されます)。%errorlevel% のほうは、set RC=%errorlevel% でアプリ終了直後の値を退避しているので通常の展開で問題ありません。停止したあとは、logs\stderr.log を見て原因を潰してから再開してください。24 時間動かし続ける構成そのものについては、設備・保全部門の承認と、停止時の影響範囲の合意を取ったうえで導入してください。
8. 管理者権限の制御
レジストリ(HKLM 配下)への書き込みやドライバ操作を伴う処理など、管理者権限が必要な処理を含むアプリでは、起動時に UAC(ユーザーアカウント制御)プロンプトを出す必要があります。
方法 1: PyInstaller の --uac-admin オプション
pyinstaller --noconsole --uac-admin app.py
マニフェスト(exe に埋め込む実行条件の宣言ファイル)に「常に管理者として実行」が埋め込まれ、起動のたびに UAC プロンプトが出るようになります。⚠️ 7.2 の自動再起動ランチャーとは組み合わせないでください——再起動のたびに UAC プロンプトが出て、夜間・休日は誰も「はい」を押せず無人運用が止まります。24 時間無人運用のアプリに --uac-admin は不向きです。
方法 2: 自前のマニフェストファイル
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
このマニフェストを app.manifest として保存し、.spec ファイル経由で組み込みます。生成された app.spec の EXE(...) に引数を 1 つ足してから、pyinstaller app.spec でビルドしてください(4.3)。
exe = EXE(
...,
manifest='app.manifest',
)
6.22.2 のソースでは、EXE() の manifest 引数はファイル名でもマニフェストの XML 文字列でも受け取り、内容を exe に埋め込む実装になっています(当サイトではソースで確認しました。この方法 2 のビルドは未実測で、実測したのは方法 1 の --uac-admin のほうです)。
本当に管理者権限が必要かは慎重に判断してください。多くの業務アプリは「ユーザー領域に書き込む」設計に変えれば管理者権限なしで運用できます。常時 UAC プロンプトが出るアプリは、現場の心理的負担が大きいです。また、管理者権限を要求する exe の配布は多くの企業で情シスの管理対象なので、配布前に社内の IT 利用規程を確認してください。
9. アップデート配布の戦略
exe 化したアプリは、リリース後にバグ修正や機能追加が発生します。「現場の数十台にどうやって新版を届けるか」という現実問題を、運用開始前に決めておくべきです。
9.1 共有フォルダ方式
社内のファイルサーバーに最新バージョンを置き、ランチャー .bat で「ローカル版とサーバー版を比較し、新しければコピー → 起動」というフローにします。onedir 形式(3.3)だと、更新のあったファイルだけが転送されるため、この方式との相性が良くなります。
@echo off
setlocal
set REMOTE=\\fileserver\apps\monitor
set LOCAL=%~dp0app
REM タイムスタンプを比較して新しければ更新
robocopy "%REMOTE%" "%LOCAL%" /E /XO /R:1 /W:1 /NFL /NDL /NJH /NJS > nul
REM 起動
"%LOCAL%\app.exe"
endlocal
robocopy /E /XO は「サブフォルダ込みで、コピー先より新しいファイルだけ上書き」の組み合わせです。運用に入る前に、次の 5 点を押さえてください。
/MIR(ミラーリング)は使わない — コピー元に無いファイルを配布先から削除するため、サーバー側のフォルダを誤って空にすると、全現場 PC のapp/が消えます- サーバーが落ちていても止まらない — robocopy が失敗するだけで、ローカルの旧版でそのまま起動は続行されます(更新だけが止まる)
- 更新が反映されるのはランチャーを起動し直したときだけ — 24 時間動き続けるアプリでは「いつ再起動して更新を取り込むか」(段取り替え時・計画停止時など)を現場と合意しておきます
- 更新はアプリ停止中が原則 — 起動中は exe や DLL がロックされ、一部だけ更新された「混在状態」になり得ます。心配なら二重起動防止も組み込みます
- robocopy は正常時も終了コード 1 を返す — 7.1 の
if errorlevel 1のようなエラー判定と、そのまま組み合わせないでください
9.2 バージョン情報を付ける
アプリ起動時に「バージョン番号」をログとタイトルバーに必ず出すようにしておくと、現場からの問い合わせ時に「どの版で起きた問題か」が即特定できます。
__version__ = "1.4.0"
logger.info("starting app version %s", __version__)
root.title(f"監視アプリ v{__version__}")
これは断片です。logger は別の場所で logger = logging.getLogger(__name__) のように作ったロガー、root は tkinter のルートウィンドウを指します。
現場で「起動しないんだけど」と相談された際に、まず聞くべきは「バージョンは何ですか」です。タイトルバーに出ていればこの確認が一瞬で済みます。exe のプロパティ画面にも版数を出したい場合は、--version-file(4.1)でバージョン情報リソースを埋め込みます。
9.3 設定ファイルとアプリ本体を分離する
現場ごとに異なる設定(IP アドレス、しきい値、保存先パス)を config.yaml や config.ini に切り出しておくと、アップデート時に設定が消えるトラブルを避けられます。
- アプリ本体:
app/フォルダ — 上書き更新 - 設定ファイル:
config/フォルダ — 上書きしない - ログ・データ:
logs/,data/— 上書きしない
このように更新対象と保持対象を物理的に分けるのが、運用ミスを防ぐ最も効果的な方法です。設定ファイルを --add-data で同梱してはいけないのも同じ理由です(6.4)。配布フォルダの全体像はこうなります。
monitor\ ← 配布フォルダ(現場 PC に丸ごと置く)
├── launcher.bat ← オペレーターはこれをダブルクリック
├── app\ ← ビルドした dist\app\ の中身をここへ(上書き更新の対象)
│ ├── app.exe
│ └── _internal\ ← 依存 DLL・ランタイム(PyInstaller 6.x が自動生成)
├── config\ ← 設定ファイル(上書きしない)
└── logs\ ← ログ(上書きしない・ランチャーが自動作成)
10. 現場配布チェックリスト
本記事の内容を、配布前のチェックリストとしてまとめます。本番ビルドの雛形コマンドはこの 1 行です(必要に応じて --hidden-import / --add-data を足してください)。
pyinstaller --noconsole --noupx --python-option "X utf8" app.py
- クリーンな Windows 環境(仮想マシン推奨)で動作確認した
- 配布方式を意図して選んだ(常設アプリは 1 フォルダ形式= onedir、単発の手渡しは onefile)
--noupxを付け、ビルドする PC の環境によって配布物が変わらないようにした- UTF-8 モードをビルド時に焼き込んだ(
--python-option "X utf8"。実行時のPYTHONUTF8=1は exe には効かない) - 標準出力・標準エラー・アプリログがファイルに残る設計にした
- 同梱リソースは
__file__基準、exe の隣のファイルはsys.executable基準で参照している --add-dataの区切りを:に統一した- ウイルス対策ソフトの例外登録に加え、実行制御ポリシー(WDAC など)で新しい exe が実行できるかを情シスに事前確認した
- バージョン番号をログとタイトルに出している
- 設定ファイルとアプリ本体を物理的に別フォルダに分けた
- アップデート手順(誰がいつどう配るか)を文書化した
- 異常終了時の復旧フロー(再起動、問い合わせ先、ログ送付方法)を現場に共有した
11. よくある質問(FAQ)
Python で作ったアプリを配布するには exe 化が必須ですか?
必須ではありません。zip でスクリプトを渡す、共有フォルダに置いて .bat から起動する、という選択肢もあります。分かれ目は配布先の PC に Python を用意して維持できるかどうかで、それが難しい現場 PC 向けには exe 化が現実解になります(1.2)。
PyInstaller のインストール方法は?
python -m pip install pyinstaller の 1 行です。プロジェクト専用の venv を作ってその中に入れてください(2.1)。venv 内へのインストールに管理者権限は不要です(筆者環境で確認。Python 本体を入れられるか、プロキシや閉域網で pip が通るかは別問題です)。社内プロキシや PowerShell の実行ポリシーで詰まる場合の回避コマンドも 2.1 に置いています。
onefile と onedir はどちらを選ぶべき?
現場 PC に置いて毎日使うアプリは onedir、メールや USB で 1 回だけ渡すなら onefile が向きます。判断材料になる起動時間・差分更新・一時展開の有無は 3.3 にまとめています。
exe のサイズが大きい。小さくするには?
Python ランタイムの分だけで下限があるため、数 MB にはなりません(5.1)。まずアプリ専用の venv でビルドし直し、それでも足りなければ --exclude-module を検討しますが、使用中のモジュールを外すとビルドは通るのに起動しなくなります(5.2)。
ウイルス対策ソフト(Windows Defender)に検知・削除されたら?
PyInstaller 公式も認識している既知の問題で、原因は自分のコードではなくブートローダの共有にあります(6.2)。現場に配る前に、配布物のハッシュと配置パスを添えて情報システム部門へ例外登録を申請しておくのが実務的な対処です。
exe が起動せず ModuleNotFoundError が出る場合は?
動的 import が静的解析で拾われていないケースが主因で、--hidden-import モジュール名 で明示すると解決します(6.3)。--exclude-module を付けている場合は、使用中のモジュールを外していないかも確認してください(5.2)。
12. おわりに
PyInstaller の使い方そのものは、公式ドキュメントと解説記事が大量にあります。ただ、日本語の情報は 5.x 時代のまま止まっているものが多く、_internal フォルダ、--add-data のコロン区切り、__file__ 基準のリソース参照といった 6.x の変更点は、まだ十分に共有されていません。本記事の 2・3・5 章の数値(ビルド時間・配布サイズ・ファイル数・起動時間、および 5.1・5.2 の容量実測と --exclude-module の挙動)は、6.22.2 で実際にビルドして確かめた結果です。4 章のオプションは --help 出力とソースでの実在確認です。1 章の版数・対応 Python は公式のパッケージ情報、5.3 の UPX の挙動は公式ドキュメントの記述に基づく整理で、こちらは当サイトでは未検証です。
そして後半(6 章以降)で扱ったこと——文字化け、誤検知と実行制御、パスの罠、ランチャー、アップデート配布——は、「動く exe を作る」段階では出てこないのに、「現場 PC に配って長期運用する」段階では必ず出てくる問題です。「動く exe」と「現場で頼られる exe」の差は、この設計の細部に宿ります。配布前のチェックリストを 1 度通すだけで、初動トラブルの大半は防げます。