Python スクリプトを EXE ファイルに変換する

Python

この記事では、Python スクリプトを EXE ファイルに変換するプロセスについて説明します。 EXE ファイルを作成するには、PyInstaller というツールを使用します。 PyInstaller は、Python スクリプトをスタンドアロンの実行可能ファイルにパッケージ化するために広く使用されているツールです。

PyInstaller のインストール

PyInstaller をインストールするには、コマンド ラインから次のコマンドを実行します。

pip install pyinstaller

記事作成時点の開発環境:

  • PyInstaller: 6.6.0, contrib hooks: 2024.6
  • Python: 3.10.6

EXE 化のためのポイント

ポイント 1: 実行可能ファイルのディレクトリの取得

EXE 化する場合、ライブラリ フォルダーを指定する必要があるかもしれません。 os.path.dirname(__file__) は実行可能ファイル内では機能しないため、コードを変更する必要があります。

実行可能ファイル(.exe)を使用する場合、file変数は定義されません。これは、スクリプトが直接実行されるのではなく、コンパイルされた実行可能ファイルとして実行されるためです。そのため、file変数を使用してファイルパスを取得するコードは、実行可能ファイル内では機能しません。

実行ファイルのディレクトリを取得するだけであれば、os.path.dirname(sys.executable)を使用するのが良いでしょう。

ポイント 2: ドラッグ&ドロップのためのライブラリ

まずはTkDNDのDLLを取得します。

TkDND Files
https://sourceforge.net/projects/tkdnd/files/

現時点のバージョンは2.8が最新です。以下の階層からダウンロードします。

Windows Binaries\TkDND 2.8\tkdnd2.8-win32-x86_64.tar.gz

取得したtarを解凍します。
tkdnd2.8というフォルダの中に拡張子tclのファイルとtkdnd28.dllが存在すればOKです。

EXE化するpyファイルと同じ階層にtkdnd2.8フォルダを配置します。

次にEXE化するpyファイルの中にdllを読み込むためのコードを記述します。

import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinterdnd2 import TkinterDnD, DND_FILES

# 実行可能ファイルまたはスクリプトのディレクトリを取得
if getattr(sys, 'frozen', False):
    # 実行可能ファイルの場合
    application_path = os.path.dirname(sys.executable)
else:
    # スクリプトの場合(デバッグ/開発時)
    application_path = os.path.dirname(os.path.abspath(sys.argv[0]))

tkdnd_path = os.path.join(application_path, 'tkdnd2.8')
print(f"tkdnd_path is set to: {tkdnd_path}")
try:
    from tkinterdnd2 import TkinterDnD
    print("TkinterDnD successfully imported.")
except Exception as e:
    print(f"Error importing TkinterDnD: {e}")
    
os.environ['TKDND_LIBRARY_PATH'] = tkdnd_path

ドラッグ&ドロップのためのライブラリ tkinterdnd2 を使用する場合、実行ファイルを生成する際にオプションを指定して、ライブラリを実行ファイルに含める必要があります。

pyinstallerで実行ファイル作成時に次のパラメータでライブラリを明示的に含める必要があります。

–collect-data tkinterdnd2

pyinstaller test.py --onefile --collect-data tkinterdnd2

ポイント 3: 仮想環境の作成

PyInstaller は、スクリプトに必要なものだけでなく、開発環境上に存在するすべてのモジュールを EXE ファイルに組み込みます。そのため、EXE ファイルのサイズが大きくなる可能性があります。なんでやねん。

これを回避するには、EXE 化するための最低限のモジュールで構築された仮想環境で作業します。

python -m venv workvenv

仮想環境をアクティベイトします(個人的にターミナルよりcmdが好き):

ここから先、Cドライブにworkフォルダを作成しそこで作業することを前提とします。

C:\Users\xxx> cd C:\work\workvenv\Scripts
C:\work\workvenv\Scripts> activate

次に、必要なモジュールをインストールします:

作成した仮想環境にpyinstallerを導入します。

(workvenv) C:\work\workvenv\Scripts> python.exe -m pip install -U pip
(workvenv) C:\work\workvenv\Scripts> pip install pyinstaller

ここからの作業は必要に応じて行います。workvenv直下にEXE化するコードを入れるtempフォルダなどを適当に作り、そこにコードを入れてスクリプトを直接動かして見ます。そこでライブラリが足りないエラーなどを確認しながら最低限のモジュールを組み込んでいきます。

(workvenv) C:\work\workvenv\Scripts> pip install tkinterdnd2

今回はtkinterdnd2だけが必要なのでインストールします。再度スクリプトを実行して動作すればOKです。
ではEXEファイルの作成の本番です。

EXE ファイルの作成

次に、PyInstaller を使用して EXE ファイルを作成します。

(workvenv) C:\work\workvenv\temp> pyinstaller diffdat.py --onefile --noconsole --collect-data tkinterdnd2

※2024/12/13現在、ここから先の一部のエラーが発生しなくなりました。pyinstallerが改善されたのかもしれません。–noconsoleでコンソール画面無しのEXE生成が可能になってます。以降は必要に応じて参考にしてください。

*おおっと*次のエラーが発生しました。
pyinstaller実行時のパラメータで —noconsoleを含めると「ファイルにウイルスまたは望ましくない可能性のあるソフトウェアが含まれているため、操作は正常に完了しませんでした。」が表示されて実行できません。
ウィルス認定されました。

トラブルシューティング

調べてみるとpyinstaller/bootloaderを再構築すると良いらしいですがすこぶる面倒な作業が必要です。
ウィルス認定されるだけなので、ここはサクッとセキュリティを一時的に止めるのが早いです。

一時的にリアルタイム保護を無効にすることで、この問題を解決できます。

  • ウィンドウズの検索窓に「セキュリティ」を入力すると[Windows セキュリティ]が出てきます。
  • クリックすると左サイドのメニューに「ウイルスと脅威の防止」があるのでクリック。
  • 右枠に「ウイルスと脅威の防止の設定」というタイトルがあり、そこの中に「設定の管理」があるのでクリック。
  • リアルタイム保護があるのでそこをオフにする。

この状態で再度pyinstallerを実行すればEXEの出来上がり。
実行ファイルが出来上がったらセキュリティはもとにもどしておきましょう。

さて、一時的に回避した問題ですが、実際に配布するとなるとそうはいきません。

他の環境で動作させようとするとマルウェア判定されてEXEがウィルスソフトに消されてしまいます。

面倒ですが回避するためにはbootloaderを再コンパイルして対処するしかありません。

Bootloaderのリコンパイル

PyInstallerのBootloaderのリコンパイルの手順です。

bootloaderを再構築すると回避できる理由

ブートローダーを独自の環境でコンパイルすることで、生成される実行ファイルのバイナリが異なるものになります。
アンチウイルスソフトウェアは、既知のバイナリパターンを基にスキャンを行うため、異なるバイナリパターンを持つファイルは誤検出されにくくなります。
という訳でウィルス検出されなくなるかもよという何とも中途半端な対処方法です。
実質、とある2台のPCでは回避出来ましたが、別のPCで動かしたらマルウェア判定されました。それを踏まえた上での対処になります。

まずは構築のためのフォルダを任意で作成します。今回は以下のフォルダ配下で作成します。

C:\pythonApps\pyinst_work\

bootloaderを再構築するために上記のフォルダに移動してpyinstallerをcloneします。

git clone https://github.com/pyinstaller/pyinstaller.git

クローン環境が出来たらboodloader配下に移動します。

cd pyinstaller/bootloader

次のコマンドでCコンパイラでリコンパイルします。

python ./waf distclean all

*おおっと*ここでまた問題が発生しました。

pyinstallerのブートローダーをビルドしようとしている際に、Cコンパイラが見つからないため、ビルドが失敗しているようです。
この問題を解決するためには、以下の手順を行います。
※Cコンパイラがインストールされてれば、この問題は通り抜けてすんなり入るはずです。

1.必要なコンパイラをインストールする

MinGWをインストールして、GCCコンパイラを使用します。 MinGW-w64 – for 32 and 64 bit Windows

MinGW
https://github.com/niXman/mingw-builds-binaries/releases

個人的にWindows以外の環境でまで使うつもりが無いのでWindows限定で対処します。

問題発生の時点の資産は次のものを使いました。
x86_64-13.2.0-release-mcf-seh-ucrt-rt_v11-rev1.7z
上記をダウンロードして解凍します。

インストーラーでは無いので解凍するとそのまんまフォルダが出来ます。

2.環境変数を設定する

解凍したフォルダ名を適当に変えてPathを通します。
フォルダ名をwingw-w64にしときました。

MinGWのbinディレクトリ(例:C:\mingw-w64\bin)をシステムのPATH環境変数に追加します。

Windowsの検索まどで環境変数を入力し、「システム環境変数の編集」をクリックして環境変数ボタンを押します。
システム環境変数の中のPathを編集して先ほどの解凍したフォルダを指定してパスを通して置きます。

3.再度ビルドを試みる

環境変数の反映はアプリ起動時などに変わるため、cmdプロンプトは新規で起動します。

C:\Users\XXXXX>cd C:\pythonApps\pyinst_work\pyinstaller\bootloader

C:\pythonApps\pyinst_work\pyinstaller\bootloader>python ./waf distclean all

Cコンパイラがインストールされていればコマンドが正常終了するはずです。

4.wheelのインストール

リコンパイルしたbootloaderのpyinstallerを導入するためにwheelをいれます。

C:\pythonApps\pyinst_work\pyinstaller\bootloader>cd ..

C:\pythonApps\pyinst_work\pyinstaller>cd ..

C:\pythonApps\pyinst_work>pip install wheel
Collecting wheel
Using cached wheel-0.43.0-py3-none-any.whl.metadata (2.2 kB)
Using cached wheel-0.43.0-py3-none-any.whl (65 kB)
Installing collected packages: wheel
Successfully installed wheel-0.43.0

C:\pythonApps\pyinst_work>cd pyinstaller

C:\pythonApps\pyinst_work\pyinstaller>pip install .

これで再度pyinstallerでEXEファイルを作成すればすんなりEXEが出来上がります。

糞判定再び

一部の環境では問題無くインストールできるものの、他の環境にもっていくと相変わらずマルウェア認定されてダウンロードすら出来ないです。
まるで糞です。

で、pyinstallerを利用する際に—noconsoleオプションを使用しないとすんなりEXE化出来たりします。
まったくもって糞すぎて草生える。

それならコンソールを出せばいいじゃない。(by マリーアントワネット)と、いうことで糞ダサいですがアプリ起動時にコンソールだせば回避できるのですが、せめて出したコンソールを閉じればいんじゃね?
ってことで対処その2です。

アプリ起動時に出るコンソールをWindows APIを利用して閉じます。
はい。このアプリを使いたいならメジャーなウィンドウズを使っとけやの精神です。
他の環境のことなど知らん。そんな対処です。

import ctypes
import time

# コンソールウィンドウのハンドルを取得
kernel32 = ctypes.WinDLL('kernel32')
user32 = ctypes.WinDLL('user32')
SW_HIDE = 0

hWnd = kernel32.GetConsoleWindow()
if hWnd:
    user32.ShowWindow(hWnd, SW_HIDE)

time.sleep(0.1)

pythonのコードの先頭に上記コードを追加します。
このコードを含めたEXEは実行すると0.1秒経過したらコンソールを自動で隠します。

これでもまだ駄目ならまた別の手を考えなきゃ。

補足:実行ファイルを独自のアイコンに置き換える

pyinstallerのオプションに指定することで置き換えることが出来ます。
以下のサンプルはpyファイルと同じ場所にicoファイルが存在する場合です。必要に応じてフルパスで指定します。
ポイントとなるのはモジュールを含めるパラメータ–collect-dataなど複数のパラメータを指定する場合ですが、以下の通り3つで1つのオプションのセットになるので先頭にpyファイルを記述したりするとエラーになるので注意が必要です。
–collect-data [対象モジュール] [対象pyファイル]

pyinstaller --onefile --noconsole --windowed --icon=convertIcon3.ico --collect-data tkinterdnd2 imageConverter.py

補足2:実行に必要なdllを補完する

exeの実行に必要なdllが存在する場合はpyinstallerでdllを指定します。

pyinstaller --onefile --noconsole --windowed --icon=convertIcon3.ico --add-binary "C:\mingw64\bin\libmcfgthread-1.dll;." --collect-data tkinterdnd2 imageConverter.py

--add-binary "C:\mingw64\bin\libmcfgthread-1.dll;."というオプションの意味を詳しく説明します。

  • C:\mingw64\bin\libmcfgthread-1.dll これは、追加したいDLLファイルのフルパスです。
  • ; このセミコロンは、DLLファイルのソースパスと、出力先のパスを区切るために使用されます。
  • . これは、現在の作業ディレクトリを指します。PyInstallerが生成する実行ファイル(EXEファイル)が置かれるフォルダを指しています。dllフォルダを作成し、そこに必要なdllを配置するのであれば./dllと設定します。

最後に

この記事では、Python スクリプトを EXE ファイルに変換するプロセスについて説明しました。 PyInstaller を使用して EXE ファイルを作成する方法、実行可能ファイルのディレクトリを取得する方法、必要なライブラリを EXE ファイルに含める方法、仮想環境を作成して不要なモジュールを回避する方法など、EXE 化のためのポイントを紹介しました。

コメント

タイトルとURLをコピーしました