LoginSignup
42
44

More than 1 year has passed since last update.

【PyInstaller】Pythonで作成したアプリをexe化して配布する

Last updated at Posted at 2021-11-23

0. はじめに

前回に引き続き連載ネタ第3弾です。
作成したGUIもPythonの環境構築している人しか使えません。技術者が何かしらのアプリを作成してもいざユーザーに使ってもらう際に、そのユーザーに難解な環境構築をさせるのもひと手間ですね。
そこで今回はPyrhon環境がないユーザーにexe化してアプリ提供する部分を書いていきます。
exe化もいくつかあります(Py2exeとかcx_freeze)が、今回はメジャーなPyInstallerを紹介します。

【第1回】Pythonで簡単に日本語OCR
【第2回】PythonでオリジナルGUIアプリを作成  ※前回記事
【第3回】Pythonで作成したアプリをexe化して配布する ←今回はこの記事

  • 動作環境
    • OS : Windows10 pro
    • Python : 3.8.3 // Miniconda 4.9.1
    • PyInstaller : 4.6 ※今最新は4.7になってるらしい
    • PySimpleGUI :4.55.1 (GUI部分)
    • Tesseract : 5.0.0 (OCR部分)
    • pyocr : 0.8 (OCR部分)
    • Pillow : 8.4.0

※今回はjupyter notebookは使用しまぜん

1. PyInstallerでexe化を行う前に知っておくべきこと

まずPyInstallerはpip install pyinstallerで導入可能です。
そしてexe化したい.pyファイルがある階層でコマンドプロンプトを開き、pyinstaller [exe化したいファイル名] --onefileというコマンドを実行すればいいだけです。

ただそれだけなのでこれで記事を終わってもいいんですが、せっかくなのでこの記事ではPyInstallerに関してかなり色々掘り下げていきます

1-1. コマンドオプション

上のコマンドでも「–onefile」というものがついているのがお分かりになると思いますが、PyInstallerにはいくつかオプションが存在します。全部は紹介できないものの、その中からいくつかを紹介します。

オプション 効果
--onefile プログラムを一つにまとめるためのコマンド
--noconsole コマンドプロンプトを表示させないコマンド
--icon exe化したときのアイコンを指定するコマンド
--debug all exe実行時にデバッグするコマンド(--noconsoleは指定しないこと)
--clean PyInstallerキャッシュを消去するコマンド
--exclude 不要なライブラリを指定するコマンド
--key 暗号化に使用されるキーを指定する(難読化)コマンド

1-2. アイコンの作成

コマンドオプションで--iconというものがあるが、これはjpgやpngを指定できず.icoファイルを指定する必要があるので、ここではすでにある画像を使用して簡単に.icoをpythonから作成する。皆さんはもっとかっこいいアイコンを設定してみてください。

※今回のアイコンは以下(パワポで適当に作成しただけ・・・)

(コマンドプロンプトでそのままpythonを起動)
>>> from PIL import Image
>>> filename = r'OCR.png' #適当な画像を読み込む
>>> img = Image.open(filename)
>>> img.save('OCR.ico') #.icoで保存する

★あとでこのアイコンをexeの画像として指定する

1-3. 仮想環境の構築

↑のオプションはさておき、何も考えずにexe化コマンドを実行するとどうなるか・・・
無茶苦茶重いexeファイルが出来上がる可能性があります。そりゃそうですよね、今使ってる開発環境のpythonのライブラリを全部使えるように盛り込んだexeなので、不要なライブラリとかも入ってる為です。

そこで新規でexe用の仮想環境を立ち上げて、必要なライブラリだけ導入してexeを作成することにする。※今回はcondaの仮想環境ケースを書くが、別にvenvでもなんでもOKである。

コマンドプロンプト
#--pyinstallerという名前の仮想環境作成--
conda create -n pyinstaller python=3.8.3

#--作成した仮想環境に切り替える--
conda activate pyinstaller

#--以下自作アプリで使用している必要ライブラリをpipで入れる--
pip install PySimpleGUI
pip install pillow
pip install pyocr
pip install pyinstaller

2. 実際に前回までのGUIアプリをexe化する

※参考(前回記事)

2-1.プログラムのPath指定部分を変更する

プログラムの中にpathを指定するような箇所がある場合は修正が必要になる。

sample.py
import PySimpleGUI as sg
from PIL import Image, ImageTk, ImageEnhance
import io
import os
import pyocr

#テーマカラーを設定
sg.theme('Purple')

#TesseractのPath情報登録 ※ここをFULLpathから変更する
"""
exeが置かれるpathにTesseractを入れてもらえばOK
※配布側がそのままTesseractと同梱で配布するのも可能(同じような環境のユーザーの場合のみ)
"""
path = os.getcwd() #exe実行ディレクトリpathを取得するのが重要
TESSERACT_PATH = path + '/Tesseract-OCR' #今回はexeと同じディレクトリに配置させる前提とする
TESSDATA_PATH = path + '/Tesseract-OCR/tessdata' #tessdataのpath
os.environ["PATH"] += os.pathsep + TESSERACT_PATH
os.environ["TESSDATA_PREFIX"] = TESSDATA_PATH

"""以下全く同じでいいので略。"""

2-2.PyInstallerでexe化

sample.pyの配置されているディレクトリへcdコマンドで移動し、exe化コマンドを打つ。
今回使用するオプションは「onefile、noconsole、clean、icon 」の4つとする。

コマンドプロンプト
#--作成した仮想環境に切り替える--
conda activate pyinstaller

#--sample.pyのディレクトリ移動--
cd C:\Users\・・・\sample.pyのディレクトリ

#--exe化実行-- ※さっき作成した.icoを同じディレクトリに配置する
pyinstaller sample.py --onefile --noconsole --clean --icon OCR.ico

#--アイコンキャッシュを削除(★これしないとアイコンが反映されないので注意★)--
ie4uinit.exe -show

exeは「dist」フォルダの中に出来るため、プログラム内でPath指定しているものを「dist」内に集結させる。※exeのアイコンが変わっていることも確認できる。

このdistフォルダをユーザーに配ればユーザーはpython環境が無くてもアプリを実行できるようになる。※ただし今回のアプリではpyocrを使用する為、以下2-3の対応が別途必要です

参考までに完成イメージGIFは以下のようになります(クドイようですが今回のアプリは2-3の対応しないとOCR実行時にエラーになります)

2-3.onefile+noconsoleの組み合わせ時のエラー対応(ハンドルが無効)

現時点(2021/11/23)で上で述べたexeを作成して実行するとtesseractで以下のようなエラーが発生するはずである。調べた限りだと「onefile」「noconsole」のコマンドオプションを同時に使用し、かつ「subprocessライブラリ」を併用した場合にエラーが起きる模様(以下公式のWiki)

noconsoleなのでこのエラー内容を確認するには、try-except文にlogを残すような改造が必要です(本記事では省略)
※ハンドルというのは「何らかのリソースを識別・操作するための識別子」のことです

エラー内容
File "pyocr\tesseract.py", line 364, in image_to_string
File "pyocr\tesseract.py", line 293, in run_tesseract
File "subprocess.py", line 804, in __init__
File "subprocess.py", line 1142, in _get_handles
OSError: [WinError 6] ハンドルが無効です。

まずはエラーの原因になっているtesseract.pyを探す必要がある。
仮想環境に入ったままpythonを起動して以下のように確認すれば簡単。

(コマンドプロンプトでそのままpythonを起動)
>>> import pyocr
>>> pyocr.__file__
'C:\\Users\\****\\envs\\pyinstaller\\lib\\site-packages\\pyocr\\__init__.py'

指定された場所に行けば以下のように問題となっている「tesseract.py」が見つかるはずである。

pyocr.png

以下は修正前と後を記載する。

tesseract.py(修正前)
293    proc = subprocess.Popen(command, cwd=cwd,
294                            startupinfo=g_subprocess_startup_info,
295                            creationflags=g_creation_flags,
296                            stdout=subprocess.PIPE,
297                            stderr=subprocess.STDOUT)

これを以下のように修正して保存する。

tesseract.py(修正後)
293    proc = subprocess.Popen(command, cwd=cwd,
294                            startupinfo=g_subprocess_startup_info,
295                            creationflags=g_creation_flags,
296                            stdout=subprocess.PIPE,
297                            stderr=subprocess.STDOUT,
298                            stdin=subprocess.DEVNULL) #これを追加

この修正後にpyinstaller sample.py --onefile --noconsole --clean --icon OCR.icoとやり直せばエラーを出さずに実行可能である。

subprocess.pyのエラー箇所解説(読み飛ばしOK)

そもそも以下部分で「if stdin is None:」だとエラーになるようになっており、subprocess.pyにおけるデフォルト設定(init関数)がstdin=Noneなのでエラーになるようである。そこで上述のようにstdin=subprocess.DEVNULLを追加することにより回避させている。

subprocess.py
        def _get_handles(self, stdin, stdout, stderr):
            """Construct and return tuple with IO objects:
            p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite
            """
            if stdin is None and stdout is None and stderr is None:
                return (-1, -1, -1, -1, -1, -1)

            p2cread, p2cwrite = -1, -1
            c2pread, c2pwrite = -1, -1
            errread, errwrite = -1, -1

            if stdin is None: #★★ここでNoneだとエラーになるようになってる★★
                p2cread = _winapi.GetStdHandle(_winapi.STD_INPUT_HANDLE)
                if p2cread is None:
                    p2cread, _ = _winapi.CreatePipe(None, 0)
                    p2cread = Handle(p2cread)
                    _winapi.CloseHandle(_)
            elif stdin == PIPE:
                p2cread, p2cwrite = _winapi.CreatePipe(None, 0)
                p2cread, p2cwrite = Handle(p2cread), Handle(p2cwrite)
            elif stdin == DEVNULL: #★★ここに分岐できるように修正する★★
                p2cread = msvcrt.get_osfhandle(self._get_devnull())

3. さいごに

Pythonが触れる人間が社内で作ったアプリを紹介したら「それ使いたい!」と言われることも多いと思う。さらに使い勝手がいい場合、勝手に口コミで他部署にも伝番することも少なくない。
そんな時に手順書作成していちいち環境構築をやらせるわけにもいかないので、exe化を覚えておいて損はないと思う。
今回3回に渡って記事連載した流れなんて例えば「これからDX進めるぞ!!」なんて部署では割とありがちな流れを書いたつもりなのでぜひ参考にしていただければと思います。

それでは今回はここまで!

参考リンク集

https://github.com/pyinstaller/pyinstaller
https://pyinstaller.readthedocs.io/en/stable/usage.html
https://news.mynavi.jp/article/20180131-windows_icon/
https://stackoverflow.com/questions/69425010/pyinstaller-error-oserror-winerror-6-the-handle-is-invalid
https://github.com/pyinstaller/pyinstaller/wiki/Recipe-subprocess
https://github.com/pyinstaller/pyinstaller/issues/5601

42
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
44