1. はじめに — なぜ exe 化が必要なのか
製造業の現場 PC には、たいてい Python がインストールされていません。Windows 10 / 11 がプリインストールされた業務用 PC であり、勝手に python.exe を入れるのは情報システム部門のポリシーで禁止されているケースも多いはずです。
こうした環境に Python アプリを届ける現実解が、PyInstaller での .exe 化です。スクリプトと依存ライブラリを 1 つの実行ファイル(または 1 つのフォルダ)にまとめ、Python 環境がなくても動くようにします。
とはいえ、開発機で動いた exe が現場 PC では動かない——これは PyInstaller を業務で使う人なら誰しも一度は遭遇する罠です。本記事では、「動く exe を作る」と「現場で運用に耐える exe を配る」の間にある具体的な差を埋めます。
2. exe 化の最小手順
まずは最小構成を確認します。これだけで、ほとんどのスクリプトは動く exe になります。本記事は PyInstaller 6.x(執筆時点の最新は 6.21)+ Python 3.12 を前提にしています。6.0 でフォルダ構成やオプションの挙動が大きく変わっているため、古い解説記事とは差分がある点に注意してください。
2.1 PyInstaller のインストール
python -m venv .venv
.venv\Scripts\activate
python -m pip install pyinstaller
仮想環境(venv)に入れることを強く推奨します。グローバル Python に入れると、依存ライブラリの取り込みでバージョン衝突を起こすことがあります。上の 2 行が venv の作成と有効化です。PowerShell で activate が「スクリプトの実行が無効になっているため…」と弾かれる場合は実行ポリシーの緩和が必要です(コマンドプロンプトならそのまま通ります。詳しくは 学習ロードマップ STEP 2 を参照)。社内プロキシで pip install が通らない場合の対処も同記事にまとめています。
2.2 1 ファイル exe の生成
pyinstaller --onefile --noconsole app.py
完了すると、dist/app.exe が生成されます。--onefile は単一ファイル化、--noconsole は GUI アプリ用に DOS 窓を抑止するオプションです。コンソールアプリの場合は --noconsole を外してください。
2.3 1 フォルダ exe の生成(推奨)
pyinstaller --noconsole app.py
--onefile を外すと、dist/app/ フォルダに展開されます。PyInstaller 6.0 以降では、フォルダ直下に置かれるのは app.exe だけで、依存 DLL・Python ランタイム・データ類はすべて _internal/ サブフォルダに格納されます(5.x までは exe と同じ階層にフラットに並んでいました)。配布時は exe 単体ではなくフォルダごとコピーしてください。実は、現場運用では 1 フォルダ形式のほうが扱いやすい場面が多くあります。
- 起動が速い:
--onefileは実行のたびに一時フォルダへ展開するため、起動に数秒かかる - ウイルス対策ソフトと相性が良い: 単一 exe は誤検知されやすい。フォルダ形式だと中身が見えるため誤検知が減る傾向
- 差分更新がしやすい: アプリ本体だけ差し替え、Qt や numpy の DLL は据え置き、といった配布が可能
3. 製造現場特有の落とし穴と対策
動く exe を作るところまでは公式ドキュメント通りで進みます。ここからが、現場固有の問題です。
3.1 CP932(Shift_JIS)絡みの文字化け
Windows の現場 PC は、いまだに既定の文字コードが CP932(Shift_JIS の Microsoft 拡張)であるケースが大半です。Python の標準入出力・ログファイル・ファイル読み書きで文字コードを明示しないと、現場で文字化けや UnicodeDecodeError が頻発します。
対策の基本:
- ログファイルは
open(path, "w", encoding="utf-8")のように明示 - Python 自体を UTF-8 モードで動かす。重要: exe 化したアプリには実行時の環境変数
PYTHONUTF8=1は効きません(PyInstaller 6.0 以降、frozen アプリは隔離された埋め込みインタープリタとして動作し、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 のいずれもありえると想定し、デコード側で柔軟に扱う
このトピックは深いので、文字コード問題の記事に体系的にまとめています。
3.2 ウイルス対策ソフトでの誤検知
PyInstaller でビルドした exe は、Microsoft Defender や法人向けセキュリティソフトに「未知のプログラム」として警告される、最悪の場合自動削除される、という事例がよくあります(PyInstaller 公式の動作仕様も参照)。
対策:
- 1 フォルダ形式で配る: 単一 exe より誤検知率が下がる
- UPX 圧縮を使わない: UPX はシステムにインストールされていて PATH で見つかった場合のみ自動使用されます(入れていなければ何もしなくても使われません)。UPX 圧縮された exe は誤検知されやすいことが UPX / PyInstaller の issue で報告されているため、確実に無効化する保険として
--noupxを付けておくのが定石です。なお誤検知はブートローダー自体が原因のこともあり、PyInstaller を最新版に保つことも有効です - 署名する: コード署名証明書を取得して exe に署名(中長期で運用するなら投資価値あり)
- 情シスに事前申請: 法人ポリシーで未署名 exe が拒否される環境では、配布前に IT 部門に許可を取る
3.3 隠れた依存ファイル(hidden imports / data files)
PyInstaller は静的解析で依存を辿りますが、動的 import(importlib 経由、プラグイン構造など)は検出できません。また、テンプレートや CSV など「コードでは import していないがランタイムで読み込むファイル」は自動では含まれません。
対策: 明示的に指定します。オプションが多くなりますが、1 行で入力してください(Web でよく見る \ での行継続は Unix シェルの記法で、PowerShell / コマンドプロンプトではエラーになります。PowerShell で複数行に分けたい場合は行末にバッククォート ` を置きます)。
pyinstaller --noconsole --hidden-import pymodbus.transaction --add-data "config.yaml;." --add-data "templates;templates" app.py
多数の指定が必要になったら、.spec ファイル(PyInstaller の設定ファイル)に切り出すと管理しやすくなります。注意点として、pyinstaller app.py のように .py を指定して実行すると、そのたびに app.spec が生成(上書き)されます。spec を編集した後にうっかり .py 指定でビルドすると編集内容が消えるため、spec 運用に切り替えたら以降は必ず pyinstaller app.spec でビルドしてください。
3.4 ファイルパス(__file__)の罠
PyInstaller でバンドルした実行時、__file__ は意図しない場所(--onefile なら一時展開フォルダ)を指します。設定ファイルや出力先を「exe と同じディレクトリ」に置きたい場合は、sys.executable を基準にする必要があります。
import sys
from pathlib import Path
def app_dir() -> Path:
"""exe 実行時は exe があるフォルダ、開発時はスクリプトのフォルダを返す。"""
if getattr(sys, "frozen", False):
return Path(sys.executable).resolve().parent
return Path(__file__).resolve().parent
CONFIG_PATH = app_dir() / "config.yaml"
LOG_DIR = app_dir() / "logs"
sys.frozen は PyInstaller でビルドされた状態で True になります。これを基準に分岐するのが定番です。
混同しやすいのが、3.3 の --add-data で同梱したファイルの場所です。同梱リソースは exe の隣ではなく、PyInstaller が管理する場所——onefile では一時展開先、6.x の onedir では _internal/——に置かれ、いずれも sys._MEIPASS で参照できます。使い分けの原則はこうです: 読み取り専用の同梱リソース(テンプレート・アイコンなど)は sys._MEIPASS 基準、ユーザーや現場が編集する設定ファイルは同梱せず、上記 app_dir() で「exe の隣」に外置きします。
3.5 起動時の謎エラー(DLL 不足、VC++ ランタイム)
「開発機では動くのに、現場 PC で起動した瞬間にエラー、もしくは何も起きない」というケース。かつては配布先に Visual C++ 再頒布可能パッケージが無いことが定番の原因でしたが、Python 3.5 以降は OS 同梱の Universal CRT(UCRT)ベースになり、PyInstaller も vcruntime140.dll 等を自動同梱するため、配布先が Windows 10 / 11 ならこの問題はほぼ起きません。現在の主な原因は、特定バージョンの DLL に依存するライブラリや、開発機にだけ入っている追加ランタイムです。
対策: 「現場 PC を模した環境」での動作確認を必ず行います。
- クリーンインストール直後の Windows 仮想マシンを 1 台用意し、配布物を置いて起動するだけのテストを毎回行う
- 長期間更新されていない古い Windows が残る現場では、VC++ 再頒布可能パッケージ(最新版)を配布物の
install/に同梱し、初回セットアップで入れてもらう - 起動失敗時にログを残せるよう、
.batランチャー経由で起動する設計にする(次節)
4. .bat ランチャーで起動を堅牢にする
exe を直接ダブルクリックさせるのではなく、.bat 経由で起動する設計にすると、現場運用が一気に楽になります。
4.1 ランチャー .bat のサンプル
@echo off
chcp 65001 > nul
setlocal
REM 作業ディレクトリを .bat のあるフォルダに固定
REM (UTF-8 モードは exe には環境変数で効かないため、3.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
ポイント:
chcp 65001でコンソールを UTF-8 に。> nulは出力を抑止%~dp0は.batファイルが置かれたフォルダ。常にここを基準にすることで「ショートカットから起動した」「別フォルダから呼ばれた」場合でも作業ディレクトリが安定- 標準出力・標準エラーを別ファイルに記録。「現場で何も起こらず終了した」現象の原因はだいたいここに残ります
--noconsoleでビルドしている場合、stderr.logに Python のトレースバックが出ないこともあります。その場合はアプリ側でloggingをファイルに向けておく設計が重要
4.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. 管理者権限の制御
レジストリ(HKLM 配下)への書き込みやドライバ操作を伴う処理など、管理者権限が必要な処理を含むアプリでは、起動時に UAC(ユーザーアカウント制御)プロンプトを出す必要があります。
方法 1: PyInstaller の --uac-admin オプション
pyinstaller --noconsole --uac-admin app.py
マニフェスト(exe に埋め込む実行条件の宣言ファイル)に「常に管理者として実行」が埋め込まれ、起動のたびに UAC プロンプトが出るようになります。⚠️ 4.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 ファイル経由で組み込む方法もあります。本当に管理者権限が必要かは慎重に判断してください。多くの業務アプリは「ユーザー領域に書き込む」設計に変えれば管理者権限なしで運用できます。常時 UAC プロンプトが出るアプリは、現場の心理的負担が大きいです。
6. アップデート配布の戦略
exe 化したアプリは、リリース後にバグ修正や機能追加が発生します。「現場の 30 台にどうやって新版を届けるか」という現実問題を、運用開始前に決めておくべきです。
6.1 共有フォルダ方式
社内のファイルサーバーに最新バージョンを置き、ランチャー .bat で「ローカル版とサーバー版を比較し、新しければコピー → 起動」というフローにします。
@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 は「サブフォルダ込みで、コピー先より新しいファイルだけ上書き」の組み合わせです。/MIR(ミラーリング)は使わないでください——コピー元に無いファイルを配布先から削除するため、サーバー側のフォルダを誤って空にすると、全現場 PC の app/ が消えます。なお、ファイルサーバーが落ちていても robocopy が失敗するだけで、ローカルの旧版でそのまま起動は続行されます(更新だけが止まる)。また、更新が反映されるのはランチャーを起動し直したときだけなので、24 時間動き続けるアプリでは「いつ再起動して更新を取り込むか」(段取り替え時・計画停止時など)を現場と合意しておきます。更新はアプリ停止中に行うのが原則です——起動中は exe や DLL がロックされ、一部だけ更新された「混在状態」になり得るためで、心配なら二重起動防止も組み込みます。細かい注意として、robocopy は正常にコピーできた場合も終了コード 1 を返す仕様のため、4.1 の if errorlevel 1 のようなエラー判定とそのまま組み合わせないでください。
6.2 バージョン情報を付ける
アプリ起動時に「バージョン番号」をログとタイトルバーに必ず出すようにしておくと、現場からの問い合わせ時に「どの版で起きた問題か」が即特定できます。
__version__ = "1.4.0"
logger.info("starting app version %s", __version__)
root.title(f"監視アプリ v{__version__}")
現場で「起動しないんだけど」と相談された際に、まず聞くべきは「バージョンは何ですか」です。タイトルバーに出ていればこの確認が一瞬で済みます。
6.3 設定ファイルとアプリ本体を分離する
現場ごとに異なる設定(IP アドレス、しきい値、保存先パス)を config.yaml や config.ini に切り出しておくと、アップデート時に設定が消えるトラブルを避けられます。
- アプリ本体:
app/フォルダ — 上書き更新 - 設定ファイル:
config/フォルダ — 上書きしない - ログ・データ:
logs/,data/— 上書きしない
このように更新対象と保持対象を物理的に分けるのが、運用ミスを防ぐ最も効果的な方法です。配布フォルダの全体像はこうなります。
monitor\ ← 配布フォルダ(現場 PC に丸ごと置く)
├── launcher.bat ← オペレーターはこれをダブルクリック
├── app\ ← ビルドした dist\app\ の中身をここへ(上書き更新の対象)
│ ├── app.exe
│ └── _internal\ ← 依存 DLL・ランタイム(PyInstaller 6.x が自動生成)
├── config\ ← 設定ファイル(上書きしない)
└── logs\ ← ログ(上書きしない・ランチャーが自動作成)
7. 現場配布チェックリスト
本記事の内容を、配布前のチェックリストとしてまとめます。本番ビルドの雛形コマンドはこの 1 行です(必要に応じて --hidden-import / --add-data を足してください)。
pyinstaller --noconsole --noupx --python-option "X utf8" app.py
- クリーンな Windows 環境(仮想マシン推奨)で動作確認した
- 1 フォルダ形式・
--noupxでビルドし、UPX 由来の誤検知リスクを避けた - UTF-8 モードをビルド時に焼き込んだ(
--python-option "X utf8"。実行時のPYTHONUTF8=1は exe には効かない) - 標準出力・標準エラー・アプリログがファイルに残る設計にした
sys.frozen分岐で実行ファイル基準のパス取得を実装した- バージョン番号をログとタイトルに出している
- 設定ファイルとアプリ本体を物理的に別フォルダに分けた
- アップデート手順(誰がいつどう配るか)を文書化した
- 異常終了時の復旧フロー(再起動、問い合わせ先、ログ送付方法)を現場に共有した
8. おわりに
PyInstaller 自体の使い方は、公式ドキュメントとブログ記事が大量にあります。しかし、「現場 PC に配って長期運用する」段階で必要になる知識は、断片的にしか出回っていません。本記事は、Windows の現場 PC に Python アプリを長期運用してきた経験を、自宅環境で再現可能な形に一般化したものです。
「動く exe」と「現場で頼られる exe」の差は、設計の細部に宿ります。配布前のチェックリストを 1 度通すだけで、初動トラブルの大半は防げます。