🐥

ブラウザーだけで動くStreamlit(Stlite)を使って嬉しかった話

2024/12/20に公開

はじめに

今回は、今年個人プロジェクトで開発したWebアプリで、Streamlit系の新ライブラリ「Stlite」を活用した際の体験談をお話ししたいと思います。

最近世の中的に利用率が増えているだけあって、Streamlitという名前を勉強会や技術記事で目にする機会が増えました。周りにデータ解析やMLopsの用途で利用している人も多く、私自身もその目的で使うことが増えています。

そもそもStreamlitとは?

簡単に言うと、PythonだけでインタラクティブなWebアプリを簡単に作れるライブラリーです。これまではデータ解析や可視化のためにちょっとしたツールを作ろうとすると、FlaskでWebサーバーを構築し、GETやPOSTメソッドを設定したり、HTMLやJavaScriptでフロントエンドを作り込む必要がありました。一方、Streamlitではこうした手間を省き、Pythonの数行でボタンやテキストフィールドなどのUIを作成できます。

Tkinterを使ったことがある人には、少し馴染みはあるかもしれません。しかしStreamlitはイベントループの処理を自動化したり、ウィジェットの配置やスタイルの設定などをより抽象化し、とても簡潔にWebアプリを書けるようにしてくれます。また、st.dataframeやst.pyplotなど、pandasやMatplotlibのオブジェクトをそのままUIに表示できるための機能を用意しています。さらに、ユーザー操作やイベントごとにコードを再実行するライブリロード機能も備わっており、特に機械学習や画像処理の用途で便利です。

自分は日常的に、地図上でさくっとデータを可視化したり、ダッシュボード作成などに活用しています。

Streamlitでデータ可視化用にWebアプリを書くイメージ:

import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt

# Sample Data
df = pd.DataFrame({
    "Category": ["A", "B", "C"],
    "Values": [10, 20, 15]
})

# Display DataFrame
st.dataframe(df)

# Plot a Bar Chart
fig, ax = plt.subplots()
ax.bar(df["Category"], df["Values"])
st.pyplot(fig)

今回作ったアプリの概要

今回作ったのは、完全に趣味で開発したWebアプリです。「Streamlitと生成AIを使って何か作ってみよう」と思い立ち、世の中に特に出すつもりのない、興味本位のものを作ってみました。

アプリの概要を簡単に説明すると、健康診断データなどのメディカルレポートをWeb UIからアップロードし、それを生成AIに解釈してもらうというものです。例えば、「HbA1c」のような、文字から意味が分かりづらい項目が何を意味するのか、この数値だったらこういう健康状態やリスクレベルだとか、その関連情報を説明してもらいます。また、複数の項目を組み合わせたリスクの推測も行えるようにしました。解釈と説明の仕事は全て生成AIに任せ、こちらはプロンプトを工夫し、あとはデータを入力して結果を出力するための簡単なGUIだけを用意するというイメージです。

そして、上記を作る上での方針は、なるべくサーバーサイドやバックエンドを持たないことでした。

理由は以下の通りです:

  • 趣味で作るアプリだからというのもあって、サーバーコストを極限に抑えたく
  • 医学的データをサーバーやDBに保存せず、その場で解析結果を表示して終わらせたい(セキュリティの観点から)
  • そもそも、サーバーサイドは生成AIのAPIにリクエストを投げるしか役割がない

そこで、「サーバーが本当に必要なのか?」「サーバーを持たずに済む方法があるのでは?」と考え始めました。

Stliteの導入

そんな時に辿り着いたのが「Stlite」というライブラリでした。ブラウザーだけで動くStreamlitがあるという噂を聞き、今回のプロジェクトに使えるんじゃね?と思い、調べてみたら使えそうでした。

そして、Stliteについて調べてみると、Pyodideを利用してPythonをWASM形式でブラウザ上で動作させる仕組みを使い、Streamlitのコードをブラウザーの中で動作させています。それで、Streamlitの機能をほぼそのまま利用できます。

実際に使ってみると、HTMLファイルの中でPythonを書いているような感覚です。生成されたHTMLファイルをブラウザーで開くだけで、ブラウザ内でStLiteのアプリが動作します。つまりStatic webホスティングのサービス(Github Pagesなど)でもデプロイできるはずです。

ちなみに、上記のStreamlitアプリのサンプルコードをStlite化すると、コードがこんな感じになります:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <title>stlite app</title>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@stlite/mountable@0.73.1/build/stlite.css"
    />
  </head>
  <body>
    <div id="root"></div>
    <script src="https://cdn.jsdelivr.net/npm/@stlite/mountable@0.73.1/build/stlite.js"></script>
    <script>
stlite.mount(
  {
    requirements: ["matplotlib"],
    entrypoint: "streamlit_app.py",
    files: {
"streamlit_app.py": `import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt

# Sample Data
df = pd.DataFrame({
    "Category": ["A", "B", "C"],
    "Values": [10, 20, 15]
})

# Display DataFrame
st.dataframe(df)

# Plot a Bar Chart
fig, ax = plt.subplots()
ax.bar(df["Category"], df["Values"])
st.pyplot(fig)`,
"data/logo.png": Ut("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"),
"pages/🎈_subpage.py": `import streamlit as st

st.title("Sub page")

st.markdown("""
_Stlite_ supports **Multi Page Apps (MPA)**, of course!

Create \`pages/*.py\` like this file to add new pages.

If you are new to MPA, read the official tutorial about it [🔗 here](https://docs.streamlit.io/library/get-started/multipage-apps)
""")
`,

},
  },
  document.getElementById("root")
)

function Ut(e){const t=window.atob(e),n=t.length,r=new Uint8Array(n);for(let a=0;a<n;a++)r[a]=t.charCodeAt(a);return r}
    </script>
  </body>
  <!-- Generated from stlite sharing (https://edit.share.stlite.net/), and the source version is 9605754c6cd12a2f17da6aee306239bb690dc81a -->
</html>

Stliteを使ってみた感想

良かった点

1. Stlite Sharing が便利すぎる

最初はStreamlitのコードを手動でStliteのアプリに書き換えようとしたのですが、ライブラリのインポートが上手くいかない、ファイルの構成がよく分からないなどのトラブルで一回詰まりました。困っていたところ、Stlite SharingというWebツール(https://edit.share.stlite.net/) の存在を知り、一気に問題解決できました。

Stlite Sharingでは、Streamlitのコードを貼り付けると、ブラウザ内で動作するStliteアプリ用のHTMLファイルを生成してくれます(このHTMLにはPythonコードがPyodideとして埋め込まれており、そのまま実行可能です)。更に、Stliteで使えるComponentの一覧、様々なアプリのテンプレートやサンプルコードも豊富です。

サンプルコード集と生成されたコードを参考しながら、作り込みをスムーズに進めることができました。

Stlite Sharingにコードを貼り付けて、アプリの操作を見るイメージ:
Stlite Sharingにコードを貼り付けて、アプリの操作を見るイメージ

Share Appを押すと生成されたコードをDLできます:
Share Appを押すと生成されたコードをDLできます

2. デプロイが超簡単

出力は1枚のHTMLファイルなので、それを開くだけでアプリが動きます。サーバーコストは削減でき、クラウドインフラを考える必要もなく、計算処理はすべて利用者のPCで行われます。

そして、更に簡単に共有したい場合、Stlite SharingのWebツールにコードを貼り付けるとStlite Sharingで自動的にHostingされますので、そのURLを共有すれば良いだけです。

ハマった点

1. 一部のライブラリが動かない

特に、内部でHTTPXを使っているライブラリーは動作しないようです(ここはStliteが悪いという訳ではなく、Pyodideが対応していないasync機能やHTTPライブラリーがあるから、だそうです)。自分のアプリでは、openaiの公式ライブラリーを使ってchatGPTのAPIにデータを投げつける仕組みだったのですが、そのままでは使えず、Pythonのrequestsライブラリーを使うように書き換える必要がありました。HTTPリクエスト周りは、少し手間がかかりました。

2. コード変換に慣れが必要

手動でStreamlitコードをStliteコードに変換するのは、最初は少しラーニングカーブがありました。ファイル構成あたりやライブラリーのインポートなどについて一回調べる必要はあったが、Stlite SharingのGUIツールを使えばこの類の問題は大体解決できるようです。

まとめ

ここまで、Stliteを使ってみた話を伝えてきました。セキュリティやコスト削減のためにサーバーレスにするといったニーズを満たすために使い、スムーズに開発を進められました。Stlite Sharingの便利さや導入コストの低さには特に感動し、今後も何かしらのプロジェクトに活用したいと思いました。

「サーバーを持ちたくない」「static ホスティングで済ませたい」「クライアントサイドで高速に動くアプリを作りたい」などのニーズがある人はぜひ一度試してみてください。

Discussion