📊

PyInstallerでStreamlitアプリをexe/app化する

2023/02/08に公開2

はじめに

こんにちは。株式会社アイデミーデータサイエンティストの中沢(@shnakazawa_ja)です。

みなさんはStreamlitをご存知でしょうか?
StreamlitとはPythonでWebアプリを作成するためのフレームワークで、HTMLの知識無しにWebアプリを簡単に作成できるという特長があります (例: 公式Gallery)。特に適当に書いてもUIをいい感じに整えてくれる点は素晴らしく、分析結果のインタラクティブなプロットや機械学習処理を手軽にアプリ化できるため弊社内でも大流行、仕事から趣味まで大活躍しています。

Streamlitで作ったアプリを人に使ってもらうときには基本的にはサーバーにアップロードしオンラインでアクセスしてもらう必要があるのですが、一方でアプリをオフラインで共有したいというニーズもニッチながら存在します。
そこで本稿では PyInstallerを使ってStreamlitアプリをexe/app化する手法をご紹介します。

注意点

  • Windowsで作ったアプリはWindowsで、Macで作ったアプリはMacでしか開けません。
  • Mac間でもM1 MacとIntel Macの間では互換性が無いようです。
  • 本記事では以下のバージョンのPython/ライブラリを使用しています。
Python 3.10.8
streamlit 1.17.0    # 1.12.0以前だと本記事の内容は再現できません
pyinstaller 5.7.0

seaborn 0.12.2      # 本稿デモ用。アプリ化本筋とは無関係

アプリの作成

フォルダ構成

まずは以下のようにフォルダ/空ファイルを作ります。中身は後から書いていきます。

streamlit_standalone        # 親フォルダ名は何でもよいです
├── .streamlit/
│   └── config.toml
├── hooks/  
│   └── hook-streamlit.py
├── main.py
└── run_main.py

アプリの中身を作る

main.py の中にWebアプリとして表示したい内容を記載していきます。今回はデモデータを用いて簡単なグラフを表示してみましょう。

main.py
import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

def main():
    st.header('スタンドアロンアプリのデモ')

    # グラフを作成する
    fig = plt.figure(figsize=(6,3))            
    sns.set_theme(style="darkgrid")
    fmri = sns.load_dataset("fmri")
    sns.lineplot(x="timepoint", y="signal",
                hue="region", style="event",
                data=fmri)
    # グラフを表示する
    st.pyplot(fig)

if __name__ == '__main__':
    main()

ターミナルに以下のコマンドを入力しStreamlitを実行するとブラウザが立ち上がり、作成したアプリが表示されます。

Terminal
streamlit run main.py

本稿では簡単のためにグラフの表示のみに留めますが、目的に応じて追加の.pyファイルを作ったり、外部データを読み込んだりと、通常のStreamlitアプリと同じようにアプリの作成を進めていただければと思います。

一点、データの読み込みには注意が必要です。PyInstallerでは絶対パスでファイルの在り処を指定する必要があるので、データのパスは例えば

import os
import sys
import pandas as pd
p = os.path.dirname(sys.executable)
df = pd.read_csv(p+'/data/original_data.csv')

のように指示される必要があります。

Webアプリの中身が完成したら、exe/app化へと進みます。

exe/app化

起動用ファイルの作成

まず、先程作成した空っぽのrun_main.pyの中に以下を記載します。

run_main.py
import streamlit.web.cli as stcli
import os
import sys

def streamlit_run():
    # pyinstallerでは絶対パスでの指定が必要
    src = os.path.dirname(sys.executable) + '/main.py'
    sys.argv=['streamlit', 'run', src, '--global.developmentMode=false']
    sys.exit(stcli.main())

if __name__ == "__main__":
    streamlit_run()

設定ファイルの作成

次に、.streamlit/config.tomlの中に以下を記載します。

config.toml
[global]
developmentMode = false

[server]
port = 8501

さらに、hooks/hook-streamlit.pyの中に以下を記載します。

hook-streamlit.py
from PyInstaller.utils.hooks import copy_metadata
datas = copy_metadata('streamlit')

PyInstallerの実行 (1回目)

ここまで完了したら、一度ターミナルから PyInstaller を実行します。

Terminal
pyinstaller --onefile --additional-hooks-dir=./hooks run_main.py --clean

しばし待ちます。

プロセスが完了すると、いくつかの新しいフォルダ/ファイルが作られます。しかし、このままではまだ動きません。

.specファイルの編集

PyInstallerの実行が終わるとrun_main.specというファイルが作られます。その中身を以下のように書き換えていきます。

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

import site
import os

block_cipher = None

assert len(site.getsitepackages()) > 0

package_path = site.getsitepackages()[0]
for p in site.getsitepackages():
    if "site-package" in p:
        package_path = p
        break

a = Analysis(
    ['run_main.py'],
    pathex=[],
    binaries=[],

    # ポイント1
    datas=[(os.path.join(package_path, "altair/vegalite/v4/schema/vega-lite-schema.json"), "./altair/vegalite/v4/schema/"),
        (os.path.join(package_path, "streamlit/static"), "./streamlit/static"),
        (os.path.join(package_path, "streamlit/runtime"), "./streamlit/runtime")],
    
    # ポイント2
    hiddenimports=['seaborn'],
    
    hookspath=['./hooks'],
    hooksconfig={},
    runtime_hooks=[],
    (以下略)

ポイントが2つあります。

1つ目がdatasです。Streamlitアプリのテンプレートとなるhtml等の存在する場所を指定する必要があります。(参考記事)

2つ目がhiddenimportsです。アプリの実行に必要なライブラリ(streamlit標準搭載ライブラリ除く)をリスト形式で指定します。アプリを起動したときに No Module ~~ エラーが出た場合は、ここにライブラリ名を追記して、この下の操作からやり直して下さい。(参考記事)

PyInstallerの実行 (2回目)

Specファイルが更新できたら、ターミナルからPyInstallerを、specファイルを指定して実行します。

Terminal
pyinstaller run_main.spec --clean

これでexe/app化が完了です!

アプリの起動

上記が完了すると以下のようなフォルダ構成になっているはずです。

streamlit_standalone
├── .streamlit/
│   └── config.toml
├── build/
│   └── いろいろ
├── dist/
│   └── run_main           # これを1階層上に移動!
├── hooks/  
│   └── hook-streamlit.py
├── main.py
├── run_main.py
└── run_main.spec

アプリを実行するためには

  • .streamlit/フォルダ
  • run_main
  • main.py

を同じ階層に置く必要があります。

実際には付随する.pyファイルや外部データもあってmain.pyの階層を変えたくないケースが多いでしょう。そのため、run_mainを1階層上、main.pyと同じところに移動して使うのが簡単でよいかなと思います。(もっとスマートな方法がありそうですね。)

run_mainのディレクトリを変更後、run_mainをダブルクリックするとブラウザがlocalhostで立ち上がりアプリが起動します。(起動まで結構な時間がかかりますが正常です。)

アプリの更新

main.pyを更新すれば、変更はすぐにアプリに反映されます。
更新時にライブラリを追加した場合、アプリを起動するとNo Module ~~エラーが出ることがあります。その場合はspecファイルにそのライブラリ名を追記して、

pyinstaller run_main.spec --clean

を再実行、dst/内に再作成されたrun_mainを移動・実行してください。

おわりに

以上の手順を踏めばオフラインでStreamlitアプリを共有することが可能となります。

想定使用状況としては、例えばインターネットに繋がっていないマシンにStreamlitアプリを導入したいというケースが考えられます。
特に研究室の実験機器に繋がっているマシンはスタンドアロンのことも多く、機器から得られたデータをその場で可視化・処理したいというようなニーズに対しては本稿の手法も選択肢として挙がり得るかと思います。

一方で、アプリの起動に時間がかかるので使い勝手は良くはないです。また、ソースコードがフルオープンになるので共有しづらい場面があることも想像されます。
アプリを使いたいマシンがインターネットに繋がっているのであれば大人しくクラウドサーバーを借りたほうが楽であることは間違いありません。(Streamlit Cloudを使えばデプロイも比較的簡単ですし、1つだけならプライベートなアプリも無料で作成できます。)

使い勝手の面は改善できるポイントもあり、例えば、

  • 起動時にターミナルが立ち上がらないようにする
  • アプリのアイコンを変える
  • ファイルサイズを小さくする

などは簡単に可能です。この辺りの詳細はPyInstallerのドキュメントや他解説記事をご参照いただければと思います。

「オフラインのアプリにはそれ用のフレームワークを」という話ではあるのですが、UIについて考えることが少なく分析に集中できる慣れ親しんだ手法で行えるという点で、オフラインでもStreamlit使う選択肢を取れるようにしておくことは有意義かもしれません。

本稿がお役に立てば幸いです。よいStreamlitライフを!

参考にさせていただいた記事

Aidemy Tech Blog

Discussion

kiryu-3kiryu-3

始めまして!kiryu-3と申します。

こちらの記事を参考にし、Streamlitをexe化することができました!ありがとうございます!
しかし、起動時にターミナルが立ち上がらないようにしたかったため、
以下のように--noconsoleオプションを付けたところ、エラーが発生しました。
pyinstaller --onefile --additional-hooks-dir=./hooks --noconsole run_main.py --clean

どうすればエラーをなくすことができるでしょうか?

Shingo NakazawaShingo Nakazawa

コメントありがとうございます。記事の内容がお役に立ったようで何よりです!

さて、エラーにつきまして、エラーメッセージを検索してのお返事でしかありませんが、こちらこちらと同様の原因ではないかと思います。 「ターミナルを起動していないと実行できない処理」がアプリの中に書かれているのではないでしょうか。
上記リンクで紹介されている方法等でkiryu-3さんの問題が解決すればと願っています!