【Python】業務効率化ツールを配布するためのexe化手段について検討してみた


やりたいこと

pythonで作った業務効率化ツールを社内で配布したい。メンバーにはPCに疎い人も多いので、exeファイルとして配布する。
exe化は何通りかのやり方がある。「起動時間」「ファイルサイズ」「配布・導入のし易さ」などの観点で、どの方法でやれば良いか、自分なりに考えてみる。

先に結論

pyinstallonefile化 + 仮想環境で必要最低限のライブラリをインストールした状態でexe化するのが無難と思った。
起動時間が求められる状況では、onefile化は諦めるのも必要かもしれない。

環境

Windows10 Home
Python3.8.2(32bit)
Visual Studio Code 1.45.1
PyInstaller3.6

勉強・検証した内容

0.検証するコード
1.PyInstallerによるexe化
 1-1.onefileにしない(デフォルト)
 1-2.onefileにする
2.PyInstaller以外の方法
3.仮想環境下でexe化する

0.検証するコード

超簡単なtest.pyを作成し、このファイルを各方法でexe化した。
importしているpyautoguiに特に意味はない(外部ライブラリが一つもないのも寂しいと思ったので)

test.py
import pyautogui
print("Hello,World!")

尚、起動時間の測定は本稿末尾に記載したツールで測定した。

1.PyInstallerによるexe化

現在一番メジャーと思われるexe化の手段。標準ライブラリではないのでpipでインストールする。
ターミナルから> pip install pyinstallerでインストール。
その後、ターミナルから > pyinstaller <変換するファイル.py> <-オプション>を実行すると、exe化が始まる。
オプションで後述するワンファイル化や、exeファイルの名前の変更、起動したときにコンソール(黒い画面)を表示させなくする等を設定できる。オプションの「-」(ハイフン)は一本だったり二本だったりするので注意。

> pyinstaller test.py --name test2 --noconsole --onefile

~INFO: Building EXE from EXE-00.toc completed successfully.と表示されたら完了。exe化したファイルはカレントディレクトリの中の「dist」というフォルダ内に生成される。オプションで名前を指定しなかった場合、変換するファイル名.exeのファイルが生成される。

1-1.onefileにしない(デフォルト)

オプションに--onefileを指定しない(=デフォルト)と、distフォルダにファイル名のフォルダが生成されており、その中にexeファイルが格納されている。メリットは起動時間の早さ。後述のワンファイル化する方法だと起動に5秒以上かかってしまうのに対し、ワンファイル化しないと1秒未満で起動できる。ただし、ディレクトリ内はこんな感じ。

うちの職場の場合、このフォルダを「この中にある〇〇〇.exeをクリックして起動してくださいね」と言って配布しても、しばらくして使われなくなることが容易に想像できる。(ショートカットを作成してもらえればワンチャンあるかも)

1-2.onefileにする

やはりツールは配布したあとにシンプルに使ってもらうため、一つのファイルにまとめたい。
そのやり方は簡単。pyinstallerを実行する際に--onefile(もしくは-F)のオプションを追加するだけ。これでdistフォルダにexeのみの単一のファイルが追加されるようになる。
デメリットは起動時間。何も対策をしないと、test.pyみたいな超単純なコードでも起動に5秒以上かかってしまうプログラムとなる。

2.PyInstaller以外の方法

Windows対応だと「cx-Freeze」「py2ex」が有名っぽい。
py2exはPython3.4までしか対応していないとのこと。(一応、直接GithubからダウンロードすればPython3.7まで対応できるらしい。トライしてみたけどPythonのバージョン切替が良くわからなくて諦めた)
ということでcx-Freezeの方を試してみた。導入及びexe化の仕方は下記のサイトが参考になったので割愛。
cx_FreezeでPythonをexeにしてみた
変換するスクリプトとは別にsetup.pyという設定用ファイルを作成する必要あるため、ひと手間かかるのが気になる。(py2exも同様らしい)
起動時間はそこそこ早い(0.6秒程度)が、onefile化できないみたいで、ファイルサイズも大して変わらないので、PyInstallerと比較してメリットは少ないと感じた。

3.仮想環境下でexe化する

PyInstaller + onefile化でtest.pyを変換すると、ただ「Hello,World!」と表示するだけのファイルが36M程度になった。これはまだマシな方で、環境によっては100MBを超すことも多々あるらしい。
どうやらPyInstallerはファイルが肥大化しやすく、その原因は、使用しているPythonにインストールされているライブラリ等をすべてパッケージングしてしまうことにある模様。
そこで、対策として何もインストールされていないまっさらな仮想環境を構築し、そこに必要最低限のライブラリをいれてpyinstallerでexe化してあげればよい、と以下の記事で勉強させていただいた。
【悲報】PyInstallerさん、300MBのexeファイルを吐き出すようになる

仮想環境構築について、自分なりにやり方を整理したので、備忘録も兼ねて記しておく。

3-1.仮想環境を作成する

総合ターミナルから「venv」コマンドを実行して仮想環境を構築する(Vertial ENVironmentの略?)

> py -m venv <任意の仮想環境名(ここではpyautogui_only)>

⇒ カレントディレクトリに任意の仮想環境名のフォルダが作成される

3-2.作成した仮想環境をアクティブにする。

先ほど作成されたフォルダの「Scripts」フォルダから「Activate.ps1」を右クリック ⇒ パスとしてコピー ⇒ 総合ターミナルに貼り付け ⇒前後の"(ダブルクォーテーション)を削除 ⇒ ENTERで実行(cdで移動してして実行でもOK)

> C:\Users\aaa\Desktop\python\pyautogui_only\Scripts\Activate.ps1

ここで「このシステムではスクリプトの実行が無効になっているため~」というエラーが出る人は、多分セキュリティの設定上スクリプトが実行できない状態になっている。なので、総合ターミナル上でまず以下コマンドを実行する。

> Set-ExecutionPolicy RemoteSigned -Scope Process

その後、もう一度Activate.ps1を実行すると多分アクティブ化できるはず。
この設定はターミナルを立ち下げるたびに再度設定が必要。参考ページ↓にはVSCode恒久的に設定を変えるやり方も書いてあった。
Windows版VisualStudioCodeで、スムーズvenvを使うための設定まとめ
アクティブ化に成功するとハイパーターミナルのカレントディレクトリの左側に仮想環境名が表示される。

(pyautogui_only) PS C:Users\~\ > _
3-3.pipコマンドで必要なライブラリのみをインストール。

今回のtest.pyの場合はpyautoguiのみインストール。PyInstallerも忘れずインストールしておく。
(ちなみになぜか最新のpyautoguiのインストールでエラー出る場合はこちらも見てみてください)

3-4.PyInstallerを実行

仮想環境でないときと同じようにでOK。

pyautogui_only) PS C:Users\~\ > pyinstaller test.py --onefile

⇒ distフォルダ内にexeファイルが出力される。
(ちなみに仮想環境を抜け出すには総合ターミナルでdeactivateコマンドを実行する)

結果、自分の場合、そのままだと36MB程度だったファイルサイズが10MB程度まで抑制できた。
起動時間も6秒程度かかっていたのが2.3秒程度まで短縮できた。

まとめ

今回検証した数値を並べると以下のような感じ。
前述したとおり、業務改善ツールの配布目的だったらPyInstaller + onefile化 + 仮想環境での最低限ライブラリインストールで良いかな、と思います。(数秒程度の起動なら、毎回でもそこまで苦にはならないはず)
いちいち仮想環境作るのはやや面倒ですが、exe化はそこまで頻発する作業でもなさそうですし。

もちろん、この結果はtest.pyみたいな超簡単なプログラムのケース。使用する外部ライブラリが増えてきたりすると状況が変わる可能性もあるので、都度判断が必要ですね。

おまけ:起動時間を測定するため作ったプログラム


import time
import subprocess
import sys
import csv
import pyautogui
import os

if len(sys.argv) >= 2:

    start_time = time.time()
    subprocess.run(sys.argv[1]) # ←ドラッグ&ドロップしたファイルのパスを受け取り、実行
    end_time = time.time()
    process_time = end_time - start_time

    pyautogui.alert(str(sys.argv[1]) + " " + str(process_time)) # ただの結果表示

    # 結果をこのファイルと同じディレクトリ内にcsvで出力
    file_name = os.path.join(os.path.dirname(sys.argv[0]), "check_time.csv")
    with open(file_name, "a", newline="") as fo:
        csv_file = csv.writer(fo)
        csv_file.writerow([sys.argv[1], process_time])

↑これをexeファイルにして、ドラッグ&ドロップでプログラムを起動し、終了するまでかかった時間を測定⇒出力。