#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から作成する。皆さんはもっとかっこいいアイコンを設定してみてください。
※今回のアイコンは以下(パワポで適当に作成しただけ・・・)
>>> 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を指定するような箇所がある場合は修正が必要になる。
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を起動して**以下のように確認すれば簡単。
>>> import pyocr
>>> pyocr.__file__
'C:\\Users\\****\\envs\\pyinstaller\\lib\\site-packages\\pyocr\\__init__.py'
指定された場所に行けば以下のように問題となっている「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)
これを以下のように修正して保存する。
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
を追加することにより回避させている。
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