StreamlitのEXE化

Stremlitで作成したプログラムをPyinstallerでExe化を行った。
基本的には、以下のサイトを参考にExe化したが、いくつか変更した点があるので覚えとして記載する。
[PyInstallerでStreamlitアプリをexe/app化する](https://zenn.dev/aidemy/articles/caa3dae565c987)

#### 試した環境
・Windows 11 Pro
・miniconda
・Python 3.9
・Streamlit　1.20
・pyinstaller 5.0.1
（仮想環境はcondaで作成。最低限はCondaで入れて、それ以外はpipでモジュールを導入している）

```powershell
# 例えば、仮想環境の構築
conda create -n st_app python=3.9

# 仮想環境ができたら、仮想環境に移動
conda activate st_app

# 仮想環境に入り
pip install streamlit numpy pyinstaller scipy seaborn
# などpipでインストール

```


#### 作成方法と変更箇所
基本的には、以下のサイトを参考に作成。
[PyInstallerでStreamlitアプリをexe/app化する](https://zenn.dev/aidemy/articles/caa3dae565c987)

Specファイルの変更箇所を記載。
datasにある、
("altair/vegalite/v4/schema/vega-lite-schema.json"), "./altair/vegalite/v4/schema/",
Streamlitのversionによっては、v4ではなくv5の可能性があるので、その際にはv5に変更する必要がある。
C:\Users\Username\...\envs\仮想環境名\Lib\site-packages\altair\vegalite以下のホルダーを確認。

hiddenimportにモジュールを記載。
モジュール名とsit-packageにあるホルダ名が異なるとエラーがでる。
例えば
scikit-learn ---> sklearn
Stremlit-aggrid ---> st_aggrid

また、例えば以下のようなモジュールをインポートする場合、
下のモジュール名まで書かないとプログラムが実行されない場合がある。

参考：https://stackoverflow.com/questions/35060523/how-to-use-pyinstaller-with-hidden-imports-for-scipy-optimize-leastsq

"sklearn.metrics",
"sklearn.metrics.r2_score",
"sklearn.metrics.mean_absolute_error",
"scipy.optimize",
"scipy.optimize.curve_fit",





#### 自作モジュールをプログラムに含める場合

Mainのプログラムの他に自作のモジュールをmainプログラムでインポートする場合について

ホルダー構成
```
streamlit_app           # フォルダ名は何でもよい
├── .streamlit/
│   └── config.toml
├── hooks/  
│   └── hook-streamlit.py
├── main.py
├── my_module/          # 自作のモジュール
│   ├── __init__.py
│   └── my_module1.py
└── run_main.py
```

なお、自作モジュールで使うライブラリーもhiddenimportに含める必要がある。
config.toml、run_main.py、hook-streamlit.pyの記載は、参考サイトと同じ。

```python
# main.py

import os
import sys
import pandas as pd

import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from my_module import my_module1       # 自作モジュールのimportの記載方法


def main():
    st.header('スタンドアロンアプリのデモ',)
    val = my_module1.calc(5,10)
    val2 = my_module1.fit_xy()
    st.subheader('my_module calcの計算結果',str(val))
    st.subheader('my_module Fittingの結果',str(val2[0]))

    # グラフを作成する
    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()
```

```python
# my_module1.py
# 自作モジュール　Mainからの関数呼び出しやScipyを使った関数のの呼び出しの例として

import numpy as np
from scipy.optimize import curve_fit

def base_func(x,a,b,c):
    y = c + a*(x - b)**2
    return y

def fit_xy():
    x = np.arange(-30, 30, 1)
    para = [2.0,5.0,10.0]
    np.random.seed(seed=10)
    y = base_func(x,para[0],para[1],para[2])+np.random.normal(0, 60, len(x))

    popt, pcov = curve_fit(f=base_func, xdata=x, ydata=y, p0=para)
    
    return popt, pcov

def calc(x,y):
    return x+y
```
なお、Exe化する前にプログラムが動作するか確認した方がよい。

`streamlit run main.py`

Ctrl + C を押すと、Local hostが停止する。

```python
# 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=[],

    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")],

    hiddenimports=['seaborn','scipy.optimize.curve_fit'],
    
    hookspath=['./hooks'],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='run_main',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)

```
Specファイルを修正したら

```
pyinstaller run_main.spec --clean
```
distホルダにできたrun_main.exeをmain.pyと同じ階層に移動してから実行する。

#### 実行結果

タイトルの下に、
my_module1にある関数を用いて、
5と10の計算結果を表示
Fittingの結果の値を表示

![sfigs01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165987/3fc6868c-10cb-2f16-acd1-ca8dbcaa2d4d.png)


#### 追記
st_aggridを使ったプログラムをExe化する際に修正したSpecファイルの内容。

理由はよくわからないがとりあえず動作。

Specファイル

追加（1）

```python
import sys
sys.setrecursionlimit(5000)
```



追加（2）

st_aggrid initファイルで読み込んでいるモジュールを追加

hiddenimportsに以下のモジュールを追加

```python
"scipy",
"statsmodels",
"numpy",
"streamlit.components.v1",
```



datasに以下を追加

```python
(os.path.join(package_path, "st_aggrid/frontend/build"), "./st_aggrid/frontend/build"),
(os.path.join(package_path, "st_aggrid/frontend/build/static"), "./st_aggrid/frontend/build/static"),
```