MSBOOKS

プログラミングとか深層学習とか日常の出来事とか

【python, pyinstaller】画像や音楽などの外部ファイルも一括でexe化して配布する

はじめに

pyinstallerを使うことでpythonのプログラムをexe化できるのは知っていたのですが,外部ファイルをexeと一緒のフォルダに入れないといけないのはちょっと不便だなって思ってました.私はpygameで自作したゲームを配布したりしたいなって思っていたのですが,画像とかを見える状態にしてるのは不格好ですし,消したり名前を変更したりできちゃって保守性もないし,世の中で配布されているゲームは画像ファイルとか見えないしなーって思ってました.そこでなんとかして組み込めないかなーって思っていたところ,こんな記事を見つけました.
teratail.com
今回はこの記事を参考にしながら,実際にpygameのコードをexe化してみました.

環境

環境は以下のようになっています.

  • Python 3.6.5
  • PyInstaller 3.5
  • pygame 1.9.6
  • OS : Windows10 (64bit)

画像ファイルの参照方法の変更

まず,exeに入っているファイルは実行時にuserprofileの"_MEIPASS"等といったフォルダに展開されるようです.なので,その展開されたファイルを読みにいけるようにresource_path()を使用します.

def resource_path(relative_path):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.abspath("."), relative_path)

例えば私はpygameで画像とかを色々読み込んでいるので,以下のような感じですね.

star = pygame.image.load(resource_path("star.png")).convert_alpha() 

specファイルを作成、追記

以下のコマンドを実行するとdistの他,specファイルが作成されます.

pyinstaller game.py

このspecファイルのままでは,外部ファイルを読み込まないので,a.datas += [('star.png', '.\\star.png', 'DATA')]などを加えます.ここで注意したいのは,この3つは(ファイル名, パス, タイプコード)の順に記述し,( )で囲う必要があることです(私はここで色々なファイルを追加しまくってエラーで躓きました…).つまり複数書く場合は,('img1.jpg', '.\\img1.jpg', 'DATA'),('img2.jpg','.\\img2.jpg', 'DATA'),・・・といった具合です.またEXE()の引数のexclude_binaries=False,を削除してa.binaries,とa.datas,を追加します。console=TrueをFalseに変更しておくと,コンソールが開かなくなり,直接ゲーム画面に移行できます.私が作ったゲームの場合は色々なファイルがあるので以下のように長々となりました.

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['game.py'],
             pathex=['C:\\game'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
a.datas += [('1menu.jpg', '.\\1menu.jpg', 'DATA'),
            ('2main.jpg','.\\2main.jpg', 'DATA'),
            ('3setting.jpg','.\\3setting.jpg', 'DATA'),
            ('4ranking.jpg','.\\4ranking.jpg', 'DATA'),
            ('5bye.jpg','.\\5bye.jpg', 'DATA'),
            ('button1.wav','.\\button1.wav', 'DATA'),
            ('button2.wav','.\\button2.wav', 'DATA'),
            ('chara0.png','.\\chara0.png', 'DATA'),
            ('chara1.png','.\\chara1.png', 'DATA'),
            ('chara2.png','.\\chara2.png', 'DATA'),
            ('chara3.png','.\\chara3.png', 'DATA'),
            ('hit.wav','.\\hit.wav', 'DATA'),
            ('kirakira.wav','.\\kirakira.wav', 'DATA'),
            ('music1.mp3','.\\music1.mp3', 'DATA'),
            ('music2.mp3','.\\music2.mp3', 'DATA'),
            ('star.png','.\\star.png', 'DATA')]
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.datas,
          name='game',
          debug=False,
          strip=None,
          upx=True,
          console=False )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               name='game')

specファイルを使ってexeを作成

pyinstaller game.spec --onefile

を実行してexeを作成します.そうすると,distフォルダにgame.exeができていました.これをデスクトップにもっていっても無事exeだけで動かすことができました!
ここで気づいたことは私のゲームではゲームの記録を保存するsave機能のようなものがあるのですが,このexeはあくまで"_MEIPASS"等といったフォルダに展開して読み込むだけで,ファイルへ再書き込みはできないようです.なので,よくフリーゲームとかを見るとSaveフォルダが外に出ているっていうのはこういうことだったんですね.なので私のゲームもSaveというフォルダを外に作って,ここに書き出すようにプログラムを書き換えました.以下のように無事exeとsaveフォルダだけでゲームを動かすことに成功しました!
f:id:msteacher:20200627170139p:plain