【悲報】PyInstallerさん、300MBのexeファイルを吐き出すようになる



表題の通り、PyInstallerを使って出力したexeファイルがとにかく巨大に膨れ上がる現象に悩まされました。

これ、めちゃくちゃ起動遅くなるし、原因不明だし、ほんと参ってたんですが、解決しましたので、メモ書き。

さっと作って、さっと配布したいのに一々こんなところで悩みたくない方向けにその他のPyInstallerのトラブルもまとめてます。

PyInstaller出力のexeファイルの巨大化

これはマジでヤバいです。普通に作ったはずなのに出力したら300MBもあります。 --onefile を指定した場合の話なんですが、普通 --onefileしますよね。

どう考えても使っているモジュールの合計容量をはるかに超えてるので最初はサッパリ意味が分かりませんでした。

exeファイルが巨大化する原因

これはPyInstaller自体の仕様の問題です。

コマンドを出した環境のPathにあるモジュールは根こそぎバンドルしに行くという仕様が原因のようです。

解決策その①

一応回避方法として --exculude コマンドが用意されているようです。

pyinstaller --onefile --exclude-module numpy example.py

--exculude は複数回指定できるオプションなので、複数のモジュールのバンドルを拒否したい場合はこうなります。

pyinstaller --onefile --exclude-module numpy --exclude-module pandas example.py

あんまり普段から環境の分離とか意識してない方は恐らくこう思うはずです。

やってられるかっ!

解決策その②

上でもう答え書きましたが、PyInstallerを実行する環境だけ、最低限のモジュールのみが含まれた仮想環境を用意してやると解決できます。

一々全部除外していくのとかめんどくさすぎますから、普通にこの方法が最短だと思います。

venvなら、

python -m venv minimam
.minimamScriptsactivate

condaなら

conda create -n minimam python=3.7
activate minimam

仮想環境のアクティベートが完了したら必要なものだけインストールします。

注意が必要なのは必ず「pipで入れる」ということです。
pipじゃなくてもいいのかもしれませんが、とりあえず conda install だけはやめましょう。(理由は後述)

pip install pyinstaller pypiwin32 # この二つは必須です。
pip install hogehoge fugafuga sukosuko # 最低限のモジュールたち

ここまで来たら普通にPyInstallerでパッケージングするだけですね。

Anaconda環境はヤバい

さて、肥えに肥えた超絶デブexeファイル、300MBというとこですが、どう考えても道理に合わない肥え方です。

明らかに目に見える(pip list とかで確認できる)範囲のモジュールの容量を超えているんじゃないのか?という疑問。

結論から言うと、犯人はAnaconda環境でpandasをインストールした時にdependencies(依存関係)として勝手についてくるMKLというモジュールがビビるくらい容量を取っています。

つまりAnaconda環境でpandas (もしくはnumpyも?)をインストールしている方はまず間違いなくPyInstallerを使うと、出力されるexeは軽く200MBは超えていきます。

こいつはpipでインストールするとついてきません

Anaconda環境のDLLエラー

更に厄介なことにクリーンな状態の仮想環境を用意したとしても、Anacondaに依存した環境のPythonモジュールを使って各モジュールを再度インストールするとPyInstallerでexe化した後にdll import error 的なヤツに陥ることがあります。

PS C:Usershoge> python
Python 3.7.0 (default, Jun 28 2018, 08:04:48) [MSC v.1912 64 bit (AMD64)] :: Anaconda custom (64-bit) on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy
Traceback (most recent call last):
  File "C:UsershogeAnaconda3libsite-packagesnumpycore__init__.py", line 16, in <module>
    from . import multiarray
ImportError: DLL load failed: 指定されたモジュールが見つかりません。

こんなヤツです。

もうこうなったらそれこそエラー回避するだけで一日かかりますので、Anacondaを捨てた方がいいです。

具体的にはAnacondaじゃないPython環境を一個作っておくという事ですね。

システムの環境変数関係からもAnacondaの配下Pathは全て無効にします。
消したら戻すときめんどくさいので、適当に余分なマーク付けてpathを無効化するくらいでいいと思います。

PyInstallerで陥りがちなその他の問題

TypeError: an integer is required

Traceback (most recent call last):
  ~~
co.co_stacksize,
TypeError: an integer is required (got type bytes)

3.8upのPythonを使っていた際に遭遇。

解決:Python3.8を捨てる

特にこだわりがなければ3.8を一旦捨てましょう。3.8独自の書き方を使ってたら直してください。3.7台は全然使えます。

海外フォーラムでも「直ったよ」って言ってるヤツもいれば「まだエラー出るやんか!」って言ってるヤツもいてまだまだカオスです。

一応公式アナウンス的には直ったと言ってるようですが、僕はそう言われてる後にこのエラーに遭遇しました。

ImportError: DLL load failed

PS C:Usershoge> python
Python 3.7.0 (default, Jun 28 2018, 08:04:48) [MSC v.1912 64 bit (AMD64)] :: Anaconda custom (64-bit) on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy
Traceback (most recent call last):
  File "C:UsershogeAnaconda3libsite-packagesnumpycore__init__.py", line 16, in <module>
    from . import multiarray
ImportError: DLL load failed: 指定されたモジュールが見つかりません。

Anaconda環境で遭遇。
上でも述べましたがPyInstallerはAnacondaから切り離された環境で使うのがベストです。

敢えていばらの道を進みたい方がいたら下に解決法を載せておきます。

解決:C:~Anaconda3Librarybin をPathに追加

とりあえず、Pathが通ってないだけです。

これに関しては、ね。

AttributeError: 'str' object has no attribute 'items'

何を言っているのかサッパリわからない系の問題

解決:setuptoolsが古い

pip install --upgrade setuptools

アップグレードしましょう。

Cannot find existing PyQt5 plugin directories

GUIアプリを作ってるときに出てくる問題。

解決:PyQt5インストール

pip install PyQt5

そもそもPyQt5がなかっただけならこれで解決しますが、たまにあるのにこのエラーが出る時があります。

これもね、Anaconda環境の話ですが、絶対PyQtあるのにこのエラー出て悩みました。

Cannot find existing PyQt5 plugin directories
Paths checked: C:/~~/~~/qt_1535195524645/_h_env/Library/plugins

その場合多分こんな感じで下にPathが書いていると思います。

とりあえず、なんも考えず、その通りのディレクトリ構造を作ったら解決しました。

C:/~~/~~/qt_1535195524645/_h_env/Library/plugins

という階層に何も入れず、ただ空のフォルダを作成するだけです。

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x83 in position 130: invalid start byte

見飽きましたね。

文字コード系のエラーです。

解決①:Shift-JIS 系でファイルを保存しない

Shift-JISはWindows標準の文字コードですので、メモ帳とかで編集したり保存するとディフォルトでその形式で保存されます。

心当たりがあったらやめましょう。

解決②: compat.py の修正

compat.py がエラーを起こしてますので、そこを修正すると治ります。

C:~~devLibsite-packagesPyInstallercompat.py

こんな感じのディレクトにcompat.pyがあります。

out = out.decode(encoding)

の箇所を探します。2か所あります。

僕のファイルの状況では427行目、574行目の2か所でした。

該当箇所を以下のコードに置き換えます。

out = out.decode(encoding, errors='ignore')

これで文字コードエラーは無視されます。

Failed to Execute ○○

Failed to Execute

exeファイル化したアプリを起動しようとするとなんか立ち上がらないパターン。これに関してはあらゆる可能性が考えられるのですが、「こんなん普通わからんやろw」ってヤツだけ紹介します。

subprocessモジュールを組み込んだアプリの --noconsole

windowsを対象としたアプリケーションでsubprocessモジュールによる処理を入れた時に遭遇。

それまでは普通に動いていたアプリケーションが、subprocessモジュールを組み込んだ直後のコンパイルからこの現象が発生し立ち上がらなくなりました。

むしろ、「直前まで動いていた」という状況がなければ原因特定不可能レベルの案件です。

解決:OsErrorをハンドリングする

参考:https://github.com/pyinstaller/pyinstaller/wiki/Recipe-subprocess

原因を読むには読んだんですが、ちょっといまいち意味は分かりませんでした。

本来subprocessで立ち上がるはずのコンソールをPyInstallerが --noconsoleで封じ込めようとしてエラーが出る?

的な感じですかね。で、そのエラーをハンドリング出来てなかったのでクラッシュしたみたい。僕が使っていたのはsubprocess.check_output()だったのですが、一例として。

def subprocess_args(include_stdout=True):
    # The following is true only on Windows.
    if hasattr(subprocess, 'STARTUPINFO'):
        # On Windows, subprocess calls will pop up a command window by default
        # when run from Pyinstaller with the ``--noconsole`` option. Avoid this
        # distraction.
        si = subprocess.STARTUPINFO()
        si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        # Windows doesn't search the path by default. Pass it an environment so
        # it will.
        env = os.environ
    else:
        si = None
        env = None

    # ``subprocess.check_output`` doesn't allow specifying ``stdout``::
    #
    #   Traceback (most recent call last):
    #     File "test_subprocess.py", line 58, in <module>
    #       **subprocess_args(stdout=None))
    #     File "C:Python27libsubprocess.py", line 567, in check_output
    #       raise ValueError('stdout argument not allowed, it will be overridden.')
    #   ValueError: stdout argument not allowed, it will be overridden.
    #
    # So, add it only if it's needed.
    if include_stdout:
        ret = {'stdout': subprocess.PIPE}
    else:
        ret = {}

    # On Windows, running this from the binary produced by Pyinstaller
    # with the ``--noconsole`` option requires redirecting everything
    # (stdin, stdout, stderr) to avoid an OSError exception
    # "[Error 6] the handle is invalid."
    ret.update({'stdin': subprocess.PIPE,
                'stderr': subprocess.PIPE,
                'startupinfo': si,
                'env': env })
    return ret

上記の関数を作って、subprocess呼び出し時のメソッド引数にキーワードで渡して、OsErrorをハンドリングすると回避できます。

# 実際のsubprocess 呼び出し例
# windowsマシンのプロダクトキーを取得する
try:
    output = subprocess.check_output(
        'wmic csproduct get name',
        shell=True, **subprocess_args(False)
        ).decode('utf-8')
    product_key = output.strip().split()[-1]
except (subprocess.CalledProcessError, IndexError, OSError):
    pass

とりあえずこれでエラー回避できました。

参考ページのコード自体はpython2向けに書かれているようですがpython3.7.4で動くのを確認しています。

PyInstaller 問題 - 総括 -

余分な時間取りたくない方はメモ必須です。

また何か起きたら