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.yamlconfig.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 度通すだけで、初動トラブルの大半は防げます。

関連記事