1. はじめに — なぜ exe 化が必要なのか

製造業の現場 PC には、たいてい Python がインストールされていません。Windows 10 / 11 がプリインストールされた業務用 PC であり、勝手に python.exe を入れるのは情報システム部門のポリシーで禁止されているケースも多いはずです。

こうした環境に Python アプリを届ける現実解が、PyInstaller での .exe 化です。スクリプトと依存ライブラリを 1 つの実行ファイル(または 1 つのフォルダ)にまとめ、Python 環境がなくても動くようにします。

とはいえ、開発機で動いた exe が現場 PC では動かない——これは PyInstaller を業務で使う人なら誰しも一度は遭遇する罠です。本記事では、「動く exe を作る」と「現場で運用に耐える exe を配る」の間にある具体的な差を埋めます。

2. exe 化の最小手順

まずは最小構成を確認します。これだけで、ほとんどのスクリプトは動く exe になります。

2.1 PyInstaller のインストール

python -m pip install pyinstaller

仮想環境(venv)に入れることを強く推奨します。グローバル Python に入れると、依存ライブラリの取り込みでバージョン衝突を起こすことがあります。

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/ 以下に 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") のように明示
  • 環境変数 PYTHONUTF8=1 を設定して Python 自体を UTF-8 モードで動かす(後述の .bat ランチャーで設定)
  • 外部から渡される CSV / 設定ファイルは、UTF-8 / CP932 / UTF-8 with BOM のいずれもありえると想定し、デコード側で柔軟に扱う

このトピックは深いので、別記事に体系的にまとめています。

3.2 ウイルス対策ソフトでの誤検知

PyInstaller でビルドした exe は、Microsoft Defender や法人向けセキュリティソフトに「未知のプログラム」として警告される、最悪の場合自動削除される、という事例がよくあります(PyInstaller 公式の動作仕様も参照)。

対策:

  • 1 フォルダ形式で配る: 単一 exe より誤検知率が下がる
  • UPX 圧縮を切る: --noupx オプションを付ける(誤検知の主原因の 1 つ)
  • 署名する: コード署名証明書を取得して exe に署名(中長期で運用するなら投資価値あり)
  • 情シスに事前申請: 法人ポリシーで未署名 exe が拒否される環境では、配布前に IT 部門に許可を取る

3.3 隠れた依存ファイル(hidden imports / data files)

PyInstaller は静的解析で依存を辿りますが、動的 import(importlib 経由、プラグイン構造など)は検出できません。また、テンプレートや CSV など「コードでは import していないがランタイムで読み込むファイル」は自動では含まれません。

対策: 明示的に指定します。

pyinstaller --noconsole \
  --hidden-import pymodbus.transaction \
  --add-data "config.yaml;." \
  --add-data "templates;templates" \
  app.py

多数の指定が必要になったら、.spec ファイル(PyInstaller の設定ファイル)に切り出すと管理しやすくなります。pyinstaller コマンドは初回実行時に app.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.5 起動時の謎エラー(DLL 不足、VC++ ランタイム)

「開発機では動くのに、現場 PC で起動した瞬間にエラー、もしくは何も起きない」というケース。原因の多くは、開発機にだけ入っている Visual C++ 再頒布可能パッケージや、特定バージョンの DLL に依存しているライブラリです。

対策: 「現場 PC を模した環境」での動作確認を必ず行います。

  • クリーンインストール直後の Windows 仮想マシンを 1 台用意し、配布物を置いて起動するだけのテストを毎回行う
  • VC++ 再頒布可能パッケージ(最新版)を配布物の install/ に同梱し、初回セットアップで入れてもらう
  • 起動失敗時にログを残せるよう、.bat ランチャー経由で起動する設計にする(次節)

4. .bat ランチャーで起動を堅牢にする

exe を直接ダブルクリックさせるのではなく、.bat 経由で起動する設計にすると、現場運用が一気に楽になります。

4.1 ランチャー .bat のサンプル

@echo off
chcp 65001 > nul
setlocal

REM UTF-8 モードと作業ディレクトリ
set PYTHONUTF8=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
set PYTHONUTF8=1
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

無限再起動はアプリ側のバグで暴走する危険もあるため、本番では「N 回連続で失敗したら停止」のようなガード(カウンタ管理)を加えてください。あるいは Windows のタスクスケジューラから「失敗時に再実行」する方法でも代用できます。

5. 管理者権限の制御

レジストリ書き込みや一部の COM デバイスアクセスなど、管理者権限が必要な処理を含むアプリでは、起動時に UAC(ユーザーアカウント制御)プロンプトを出す必要があります。

方法 1: PyInstaller の --uac-admin オプション

pyinstaller --noconsole --uac-admin app.py

マニフェストに「常に管理者として実行」が埋め込まれ、起動のたびに UAC プロンプトが出るようになります。

方法 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%" /MIR /XO /R:1 /W:1 /NFL /NDL /NJH /NJS > nul

REM 起動
"%LOCAL%\app.exe"
endlocal

robocopy /XO は「コピー先より新しいファイルだけ上書き」のオプションです。シンプルですが、ファイルサーバーが落ちたら起動できなくなる依存リスクはあります。

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/ — 上書きしない

このように更新対象と保持対象を物理的に分けるのが、運用ミスを防ぐ最も効果的な方法です。

7. 現場配布チェックリスト

本記事の内容を、配布前のチェックリストとしてまとめます。

  • クリーンな Windows 環境(仮想マシン推奨)で動作確認した
  • 1 フォルダ形式・--noupx でビルドし、UPX 由来の誤検知リスクを避けた
  • UTF-8 モード(PYTHONUTF8=1)を .bat で設定した
  • 標準出力・標準エラー・アプリログがファイルに残る設計にした
  • sys.frozen 分岐で実行ファイル基準のパス取得を実装した
  • バージョン番号をログとタイトルに出している
  • 設定ファイルとアプリ本体を物理的に別フォルダに分けた
  • アップデート手順(誰がいつどう配るか)を文書化した
  • 異常終了時の復旧フロー(再起動、問い合わせ先、ログ送付方法)を現場に共有した

8. おわりに

PyInstaller 自体の使い方は、公式ドキュメントとブログ記事が大量にあります。しかし、「現場 PC に配って長期運用する」段階で必要になる知識は、断片的にしか出回っていません。本記事は、Windows の現場 PC に Python アプリを長期運用してきた経験を、自宅環境で再現可能な形に一般化したものです。

「動く exe」と「現場で頼られる exe」の差は、設計の細部に宿ります。配布前のチェックリストを 1 度通すだけで、初動トラブルの大半は防げます。

関連記事