PyInstallerによるPythonスクリプトのexe化とエラー対処方法まとめ

# はじめに
　この記事では、PythonモジュールのひとつであるPyInstallerを用いたPythonスクリプトのexe化方法および各種エラー対処方法について説明します。PyInstallerでexe化することで、Pythonをインストールしていない環境においてもPythonスクリプトを実行させることができるようになります。業務においてちょっとしたツールを作成しチームメンバーに配布したり、Tkinterと組み合わせてGUIを実装すれば、スタンドアロンで動くデスクトップアプリケーション[（前回記事を参考）](https://qiita.com/kunishou/items/dfa0f5482bebb30ed064)を作成することも可能です。

# 目次
- [はじめに](#はじめに)
- [作成の動機](#作成の動機)
- [PyInstallerとは](#PyInstallerとは)
- [exe化の一連の流れ](#exe化の一連の流れ)
 - [1.仮想環境作成](#1.仮想環境作成)
 - [2.PyInstaller実行](#2.PyInstaller実行)
 - [3.exeファイルの軽量化](#3.exeファイルの軽量化)
 - [4.各種エラー対処方法](#4.各種エラー対処方法)
- [その他](#その他)
- [注意点](#注意点)
- [最後に](#最後に)

# 作成の動機
　PyInstallerに関する記事についてはQiita内やWEB上を探せばいくつか見つけることができますが、exe化の一連の流れや注意点、作成したexeファイルの軽量化方法についてひとつの記事にまとめているものはなく、過去PyInstallerの使い方を調べる際に点在した情報を探すのにとても苦労したことから、今回PyInstallerの使用方法をまとめた記事を作成することにしました。
 
また、以前書いた記事[「Python未導入環境においてPandasGUIとpandas-profilingを使用可能なEDAツール『Pandas Anywhere』を作ってみた」](https://qiita.com/kunishou/items/dfa0f5482bebb30ed064)ではPandasGUI、pandas-profillingモジュールを含むスクリプトのexe化に取り組みましたが、多くのエラーに遭遇し、解消に苦労したので、この際に出てきたエラーとその対処方法についても併せて述べたいと思います。

# PyInstallerとは
* Pythonスクリプトをexe化するためのPythonモジュール。ver4.3ではPython3.9までがサポートされています。
* 実行時のオプション指定で1つのexeファイルにまとめたり、exeファイル実行時にコンソール画面を非表示にすることができる（詳細は後述）。
* スクリプトをexe化するモジュールとしては、PyInstaller以外にも「cx_Freeze」がある。PyInstallerとの違いは、PyInstallerはオプション指定で1つのexeファイルにまとめられるが、cx_Freezeでは複数ファイルに分かれた形でのexe化となる。exeファイルの起動速度についてはcx_Freezeのほうが速い模様。

# exe化の一連の流れ
1. 仮想環境の作成
2. PyInstallerの実行
3. exeファイルの軽量化
4. エラー対応

※ 3.以降は必要に応じて実行

## 1.仮想環境作成
　PyInstallerを用いたexe化の初期手順として必ず仮想環境を作成して、仮想環境上で行います。  **PyInstallerでexe化する際は、スクリプトに記載されているモジュールがexeファイルに同封されるわけではなく、開発環境上に存在しているすべてのモジュールが同封されます。**  つまり、exe化するスクリプト上ではpandasやnumpyしか使用していないとしても、例えば開発環境上にTensorFlowやPyTorchがインストールされているのであればこれらのモジュールも同封されてしまいます（その結果、ものすごい大容量のexeファイルができあがります）。そのため、exe化する際は必ず仮想環境上で実行するようにして下さい。

**※作者実行環境**
OS: Windows10
Python: Python3.7.3
Pyinstaller: ver4.2
PCユーザ名: username

例で「testvenv」という名前の仮想環境を作成

```python:console
python -m venv testvenv
```

作成した仮想環境の起動

```python:console
C:\Users\username\testvenv\Scripts\activate
# Windows10環境
# PCユーザ名: username
```

pipの最新化

```python:console
pip install -U pip
```

Pythonスクリプト上で使用するモジュールのインポート
※ファイル容量を少なくするために、exe化するスクリプトで使用しない余計なモジュールはインストールしないこと。

```python:console
pip install pyinstaller   # pip  installの一例
```



## 2.PyInstaller実行
　Desktop 上の「test」フォルダ内に作成した「main.py」というスクリプトをexe化する場合

```python:console
cd C:\Users\username\Desktop\test
pyinstaller main.py
```
※実行環境によっては「PyInstaller」のように「P」と「I」を大文字で指定しないと上手く実行できない場合があるようです。

上記操作の実行によりtestフォルダ内に以下の通りフォルダ、ファイルが作成されます。

### ディレクトリ構成
test
　├ main.py　…　スクリプトファイル（自身で用意したファイル）
　├ main.spec …　PyInstaller実行後に作成される中間ファイル、エラー発生時等にPyInstaller実行時の設定をここに記述
　├ dist …　ここにexeファイルや関連ファイルが格納されたフォルダが作成、配布時はこのフォルダごと配布するイメージ
　└ build …　このフォルダの中身を触ることは特にありません

以下のようにdistフォルダ内にexeファイルと関連ファイルが作成されます。
![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692165/a011eeae-6eb7-c2b5-ce41-6d9f44d6342b.png)


### オプションの指定
### --onefile
onefileオプションを指定することで、スクリプト実行に関連するファイルを1つのexeファイルにまとめてくれます。私がツールを配布する際は基本的にこのオプションを指定しています。

```python:console
pyinstaller main.py --onefile
```
以下のようにdistフォルダ内に1つのexeファイルが作成されます。
![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692165/e21c4fab-93cf-1abe-3b70-19939c3881fe.png)




### --noconsole
noconsoleオプションを指定することで、exeファイル実行時に表示されるコンソール画面を非表示にできます。ツール配布時に余計な画面を表示させたくない場合には指定すると良いですが、コンソール画面に表示されるエラー内容が見れなくなるのでデバッグ中は指定しないほうが良いです。

```python:console
pyinstaller main.py --noconsole
```

## 3.exeファイルの軽量化
　仮にスクリプト内でpandas、numpyしか使用しない簡単なデータ集計ツールを作ったとしても、作成されたexeファイルは300MB程度にはなってしまうと思います。原因としてはpandasやnumpyモジュール内に含まれているIntel MKLというIntel CPUに特化した高速線形演算ライブラリが原因。これを除外することでexeファイルの容量を30MB程度にすることができます。

specファイルに以下の破線の範囲を追記します。

```python:main.spec
# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(['main.py'],
             pathex=['C:\\Users\\username\\Desktop\\test'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             hooksconfig={},
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)

#---------------追加ここから---------------

Key = ['mkl']

def remove_from_list(input, keys):
    outlist = []
    for item in input:
        name, _, _ = item
        flag = 0
        for key_word in keys:
            if name.find(key_word) > -1:
                flag = 1
        if flag != 1:
            outlist.append(item)
    return outlist

a.binaries = remove_from_list(a.binaries, Key)

#---------------追加ここまで---------------

exe = EXE(pyz,
          a.scripts, 
          [],
          exclude_binaries=True,
          name='main',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True,
          disable_windowed_traceback=False,
          target_arch=None,
          codesign_identity=None,
          entitlements_file=None )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas, 
               strip=False,
               upx=True,
               upx_exclude=[],
               name='main')

```

★追記後にbuild、distフォルダを一度削除し、以下の通りPyInstallerを実行するとdistフォルダ内に新しくexeファイルが作成されます（pyファイルではなく、specファイルを指定していることに注意）。

```python:console
pyinstaller main.spec --onefile
```
MKLを除外したことによりexeファイルが軽量化されていることが確認できるはずです。

## 4.各種エラー対処方法

### ①ModuleNotFoundError
```
ModuleNotFoundError: No module named 'pandas_profiling.describe'  # エラーの一例
```
このエラーが出た場合は、エラーに記載のモジュールが上手くexeファイルに同封できていません。そのため、specファイル内の「hiddenimports」の項に以下の通り、モジュールインポートが上手くできていないことを明示的に記述してあげます。

```
hiddenimports=['pandas_profiling.describe']
```
記述後に、上述の★の操作でPyInstallerを再度実行すると新しくexeファイルが作成されます。

このexeファイルを実行して、再度別のModuleNotFoundErrorが出る場合は、先ほどのhiddenimportsの項目にカンマ区切りでエラーに該当するモジュールを付け足していって下さい。

### ②FileNotFoundError
```
FileNotFoundError: [Errno 2] No such file or directory:
'C:\Users\username\AppData\Local\Temp\_MEI103442\wordcloud\ipaexg.ttf'  # エラーの一例
```
このエラーが出た場合は、エラーに記載のファイルが上手くexeファイルに同封できていません。そのため、specファイル内の「datas」の項に以下の通り、ファイルインポートが上手くできていないことを明示的に記述してあげます。

```
datas = [('C:\\Users\\username\\testvenv\\lib\\sitepackages\\wordcloud\\ipaexg.ttf','\\wordcloud\\')]
```
記述後に、上述の★の操作でPyInstallerを再度実行すると新しくexeファイルが作成されます。

このexeファイルを実行して、再度別のFileNotFoundErrorが出る場合は、先ほどのdatasの項目にカンマ区切りでエラーに該当するファイルを付け足していって下さい。

### ③Recursion error
```
Recursion error : maximum recursion depth exceeded
```


このエラーは、関数内で再帰のしすぎでPythonのデフォルト再帰回数の1000回を超えてしまう際に発生します。このエラーは、スクリプト内で再帰関数を使っていない場合にも発生するため、PyInstaller内で再帰関数が使われていると考えられます。対処方法としては、スクリプト内に以下の記述を追加し、許容する再帰回数上限を1000回から上げます。

```python
import sys
sys.setrecursionlimit(10000) # 再帰回数の上限を10000回に設定
```

**エラーについては①と②がほとんどだと思われるので、この2つのエラーの対処方法を押さえておけばまずは大丈夫だと思います。**

# その他
以前書いた記事[「Python未導入環境においてPandasGUIとpandas-profilingを使用可能なEDAツール『Pandas Anywhere』を作ってみた」](https://qiita.com/kunishou/items/dfa0f5482bebb30ed064)では、ModuleNotFoundErrorが100回以上発生したため、ModuleNotFoundErrorが発生する特定モジュール内のサブモジュール名をすべて取得するスクリプトを作成して対応しました（エラー発生に切りがなかったので取得したすべてのサブモジュール名をhiddenimportsに記述。具体的にはPlotlyモジュール内のlayout配下のサブモジュールがすべてModuleNotFoundErrorになりました）。

参考に、サブモジュール名を取得するスクリプトを以下に載せておきます。

※plotly.validators配下のlayoutフォルダ内のサブモジュール名をすべて取得する場合の例で、スクリプトと同一ディレクトリ内にlayoutフォルダを丸ごとコピーして実行すると結果がprint出力されます。

```python
import os

sbfolder = []

for fd_path, sb_folder, sb_file in os.walk('layout'):  # os.walk()の括弧内は格納するモジュールのフォルダ名を記載
    for fol in sb_folder:
        path = fd_path + '\\' + fol
        path2 = 'plotly.validators.' + path  # 必要に応じて書き換え
        path2 = path2.replace("\\" , ".")
        
        if 'pycache' not in path2:
            sbfolder.append(path2)
print(sbfolder)
# 以下のprint結果を、specファイルのhiddenimportsに記入
# ['plotly.validators.layout.activeshape', 'plotly.validators.layout.angularaxis', 
# ...
# 'plotly.validators.layout.yaxis.title', 'plotly.validators.layout.yaxis.title.font']
```

# 注意点
以下はPyInstallerを用いて作成したexeファイルの使用時の注意点です。

:::note warn
・exeファイルを実行できるOS環境は、PyInstallerを実行したOS環境のみ
例えば、Window10 64bit環境で作成したexeファイルを実行できるのはWindows10 64bit環境のみです。
・起動に時間がかかる  
実行するPCのスペックやexeファイルの中身にも依りますが、exeファイルを起動するのに簡易なもので30秒、重いもので1分以上かかることがあります。

:::

# 参考
* [【Python】TIPS：pyinstallerの軽量化【exe化】](https://qiita.com/snuow/items/c9c48c3095a6a8be5871)
* [pyinstaller によるexe化のエラーメッセージと対処方法](https://qiita.com/tobesan/items/12189abc5adbda4a49bd)


# 最後に
以上、PyInstallerの使用方法になります。他にもいろいろとエラーはあったので、思い出し次第、追記していきたいと思います。