Streamlitで複数ページのアプリを作る

今回はStreamlitで複数ページのアプリを作る方法を紹介します。この記事で紹介している方法を使うと、複数のファイルに分けてStreamlitのアプリを作れるのでコードの管理も簡単になります。

一応書いておくと、サイドバーに選択式のウィジェットを配置して、その選択によって表示する項目を切り分ける実装にするとこの記事に書いてる方法を使わなくても複数ページっぽく振る舞うアプリを作ることはできます。ただ、コードが長くなることや名前空間を共有してしまう不便さ等もあるので別ページのものは別ページで作った方が良いことが多いです。(逆に1ファイルにまとめた方が便利なこともありますが。)

複数ページのアプリを作る場合、最初にこちらのページからリンクされている各ページを参照してStreamlitの方針を把握しておくことをお勧めします。
参考: Multipage apps – Streamlit Docs

では具体的な方法を見ていきましょう。といっても、ほとんどのケースで一番最初のpagesディレクトリを使う方法で十分だと思います。

pagesディレクトリを使用する方法

これは Streamlitのメインページとなる.py ファイルと同じディレクトリに pages という名前でディレクトリを作り、その下に .py ファイルを作るというものです。

ドキュメントでは以下の構成が例示されていますね。

your_working_directory/
├── pages/
│   ├── a_page.py
│   └── another_page.py
└── your_homepage.py

この通りに作って動かして見ましょう。ファイルの中身は全部からっぽです。

% mkdir your_working_directory
% cd your_working_directory
% mkdir pages
% touch your_homepage.py
% touch pages/a_page.py
% touch pages/another_page.py
% streamlit run your_homepage.py

するとこのような画面になります。

実際に動かしていただくと確認できるのですが、それぞれURLが、
– http://localhost:8501/
– http://localhost:8501/a_page
– http://localhost:8501/another_page
となります。メインのファイルがルートパスになる感じですね。URL中には/pages/の文字は含まれません。

ファイル名と、上記の画面のサイドバーに表示されるページ名を見比べていただけると分かるのですが、 _ がスペースに変換されています。これは _ や – がセパレーターとして半角スペースに変換されるからです。また、拡張子 .py も取り除かれます。

また、ファイル名の先頭に正の数字をつけていた場合、それも取り除かれます。項目の並び順の指定には使えるのでうまく活用しましょう。

st.Pageとst.navigationを使う方法

もう一つの方法は、専用のメソッド、st.Pageとst.navigationを使って明示的に複数ページを定義することです。

この方法であれば、端末内の別の場所に置いてあるファイルを呼び出すこともできますし、分岐等を用いて、リンクを配置するページを出し分けることもできます。

具体的な方法は次のようになります。

# ファイル構成
your-repository/
├── page_1.py
├── page_2.py
└── streamlit_app.py

# streamlit_app.pyの中身
import streamlit as st

pg = st.navigation([st.Page("page_1.py"), st.Page("page_2.py")])
pg.run()

st.Page(“ファイル名”)でファイルを読み込んでページオブジェクトを作り、それをst.navigation に渡して、pg.run() しています。run()は忘れがちなので注意しましょう。

正直、pagesディレクトリを使う方がずっと単純なのですが、大規模になってくるとst.navigation を使う場面も出てくると思うので頭の片隅にでもおいておいてください。

Streamlitでメインカラムに複数列の要素を配置したりタブ切り替えを実装したりする

今回もStreamlitの記事です。ページレイアウトをスッキリさせるのに便利なテクニックを2つ紹介します。

これまでの記事でも書いてきた通り、Strreamlitは原則的に上から下へと要素が足され続けていきます。(サイドバーへの配置や、プレースホルダーを使って上に遡って要素を配置する等の例外はありましたが。)

ただ、メインカラムに配置したい要素が多くなってくるとページがどうしても縦長になってしまって不便なことがあります。このような時に、サイドバーではなくメインカラム内を複数列に切って要素を配置したり、タブで表示する要素を切り替えたりすると便利です。

複数カラムとタブ切り替えは全然違う機能なのですが縦長になるページをスッキリさせたいという同じ目的で使えるのと、コードで実装した時の書き方が似てるのでまとめてこの記事で紹介します。

Streamlitで複数カラムに要素を配置する方法

まず、画面を縦に分割して複数列に要素を配置する方法です。これは、st.columns() を使います。ドキュメントはこちら。
参考: st.columns – Streamlit Docs

st.columns(3) のように、分割したいカラム数(この例では3個)と指定したり、st.columns([1, 2, 3]) のように、配列で数値を渡すと、1:2:3の長さの比率になるように3分割できたりします。

そして、 st.colunms() が指定したカラムの数だけオブジェクトを返してくるので、そいつをwith文で使います。サンプルコードはこちらです。

import streamlit as st


st.title("複数列のサンプル")

col1, col2, col3 = st.columns(3)

with col1:
    st.write("1列目の中身")
    st.write("サンプルテキスト サンプルテキスト サンプルテキスト サンプルテキスト ")

with col2:
    st.write("2列目の中身")
    st.write("サンプルテキスト サンプルテキスト サンプルテキスト サンプルテキスト ")

with col3:
    st.write("3列目の中身")
    st.write("サンプルテキスト サンプルテキスト サンプルテキスト サンプルテキスト ")

st.write("withを抜けると元通り1カラムのレイアウトになる")

実行した見た目はこのような感じ。無事に複数列のレイアウトが実現できていますね。

タブ切り替えの利用

続いてタブ切り替えの利用です。こちらは、選択したタブに対応したコンテンツだけヒョじすることで表示するものを最小化することができます。

先に注意しておきたいのは、選択しなかったタブの中身のコンテンツはブラウザ上表示されないだけで、サーバー側ではコードは実行されているということです。(今日時点では選択したタブの中身のコードだけ実行するといった機能は用意されていません)

使い方はカラムととても似ていますが、一点違うのは、カラム数のようにタブ数を整数で渡すということはできず、タブのタイトルの配列を渡す必要があります。詳しくは、st.tabs()のドキュメントを参照してください。
参考: st.tabs – Streamlit Docs

サンプルコードはこちらです。

import streamlit as st


st.title("タブ切り替えのサンプル")

tab1, tab2, tab3 = st.tabs(["タブ1", "タブ2", "タブ3"])

with tab1:
    st.write("タブ1の中身")

with tab2:
    st.write("タブ2の中身")

with tab3:
    st.write("タブ3の中身")

これでタブ切り替えが実現できます。表示したい画像を切り替えるとか、オプションを隠しておくとか色々使い道がありそうですね。

データ分析においては表示するグラフを切り替える、といった使い道ももちろんあります。
ただ、先ほども書きましたが、これは選択しなかったタブの中身を表示しない、というだけの機能でコードは一通り実行されるものなので、もしデータの取得や計算などをタブの中に書いていたら表示しない分も無駄に実行されますので注意してください。

Streamlitでサイドバーを利用する

今回はStreamlitでサイドバーを使う方法を紹介します。

Streamlitは基本的には1カラムで、上から順に要素が追加されていくのですが、st.sidebar という属性を使うことでサイドバーを表示しその中に要素を追加していくことができます。

ドキュメントはこちら: st.sidebar – Streamlit Docs

使い方は簡単で、通常、 st.{要素名} とするところを、 st.sidebar.{要素名} とするか、
with st.sidebar: のブロック内で st.{要素名} と宣言することで要素の配置場所をサイドバーにできます。何か一つでもサイドバーに配置されたらそれまで表示されていなかったサイドバーを表示できます。

普通に要素を配置するだけれあれば、要素の配置場所が変わるだけという挙動になります。
例えばこのような例になるでしょうか。

import streamlit as st


st.sidebar.title("サイドバーの基本例")

# テキスト入力
name = st.sidebar.text_input("名前を入力してください")

# スライダー
age = st.sidebar.slider("年齢を選択してください", 0, 100, 25)

# セレクトボックス
favorite_color = st.sidebar.selectbox("好きな色を選んでください", ["赤", "青", "緑"])

# メイン画面に表示
st.write(f"こんにちは、{name}さん!")
st.write(f"年齢: {age}歳")
st.write(f"選んだ色: {favorite_color}")

sidebar を使わなかったらスライダーやドロップバーが一番上にあってその後結果が表示されるという子度ですが、sidebarを使うと操作部分はサイドバー、結果表示画カラムと分けて表示できます。

サイドバーのデフォルトの幅等を指定できないのは難点ですが、マウスでスライドさせて調整したり隠したりできるのもいいですね。

もう少し実践的な例として、サイドバーを使ってページ切り替え(のように見える) UIを実装して見ましょう。

これはサイドバーにラジオボタンなどの択一の選択肢を提示しておいて、その結果によってメンカラムに表示する項目を丸ごと切り替えてしまうというものです。

これを使うと、複数ページを持ったアプリを作ることができます。

例えばこのような感じです。

import streamlit as st


# サイドバーの操作要素
st.sidebar.title("操作要素")
page = st.sidebar.radio("ページを選択してください", ["ホーム", "データ可視化", "設定"])

# メインカラムの内容を動的に変更
if page == "ホーム":
    st.title("ホームページ")
    st.write("ここはホームページです。最新情報やお知らせを表示します。")
elif page == "データ可視化":
    st.title("データ可視化ページ")
    st.write("ここではデータの可視化を行います。")

    # サンプルデータのプロット
    import numpy as np
    import pandas as pd

    data = np.random.randn(100, 3)
    df = pd.DataFrame(data, columns=["A", "B", "C"])

    st.line_chart(df)
elif page == "設定":
    st.title("設定ページ")
    st.write("ここではアプリケーションの設定を行います。")

    # 設定の入力フォーム
    st.sidebar.header("設定オプション")
    option1 = st.sidebar.checkbox("オプション1")
    option2 = st.sidebar.checkbox("オプション2")
    st.write(f"オプション1: {option1}")
    st.write(f"オプション2: {option2}")

この機能を使うと、かなり本格的なアプリっぽいものが作れるようになるので、ぜひ試して見てください。

Streamlitのコンテナを使って動的にページを表示する

今回もStreamlitの記事です。まだ続く予定です。

今回はStreamlitのコンテナやプレースホルダーの紹介です。Streamlitでは基本的にコードで実行された順番に上から下へと要素が順次描写されていきます。
しかし、場合によっては描写済みのところに遡って要素を追加したり、一度描写した要素を再度書き直したりしたいという場面もあります。

一応、何かしらウィジェットを操作すれば画面全体が全部再描写されるので、前回の記事で紹介したst.session_stateをフル活用すれば大抵のことはできてしまうのですが、この記事で紹介するコンテナを有効に使えばより手軽で柔軟に実装できます。

空要素をプレースホルダーとして挿入するst.empty()

最初の方法は、単一の要素を格納できるプレースホルダーを挿入するst.empty()です。
ドキュメントはこちら

これは場所を一つ予約しておいて、後からその中身を書き換えることができます。これの次に複数要素を格納できるコンテナってのを紹介しますが、それと違って単一要素しか入れられないので何か書き込むと値が上書きされて書き変わっていきます。

通常、Streamlitで何か書き換えたかったらページ全体が再描写されるので、単一要素を書き換えられるのは良いですね。使い方としてはwith文で使う方法と、変数に格納しておく方法があります。ドキュメントで、時間によるカウントアップの例が取り上げられているのでやって見ましょう。普通にfor文でst.write()したら毎行追記されてしまいますが、プレースホルダーを用意しているのでその中身が順次書き換えられていく動きが実装できます。

with文で使う場合はこうなります。

import streamlit as st
import time


with st.empty():
    for seconds in range(60):
        st.write(f"{seconds}秒経過 ")
        time.sleep(1)
    st.write("一分経過")

変数に格納する場合はこうです。

import streamlit as st
import time


placeholder = st.empty()
for seconds in range(60):
    placeholder.write(f"{seconds}秒経過 ")
    time.sleep(1)
placeholder.write("1分経過")

withだと宣言した直後にそこを書き換えるような使い方しかできませんが、変数を用意せずにすみコードがシンプルになります。

一方で変数に格納しておくと、上記のサンプルコードでは直後に書き換えているのであまりメリット感じませんが実際は、何か別の処理を色々やってから改めて確保していた場所を書き換えるといったことができます。

複数の要素を格納できるコンテナを作るst.container()

次に紹介するのが、st.container()です。ドキュメントはこちら

これもempty()同様にほぼプレースホルダーなのですが、違うのは複数の要素を格納できることです。その代わり、上書き修正ができません。

後からこのコンテナの中に要素を追加していくことができます。コンテナの中では追加されたものが上から順番に表示されます。

超シンプルな例ですが、以下のようなのをやって見ましょう。

import streamlit as st


st.write("1行目")
container = st.container()
st.write("2行目")
container.write("3行目")
container.write("4行目")

全部st.writeで実装したら、1行目から4行目までが順番に表示されるところですが、コンテナを1行目と2行目の間に置いているので、これを実行すると3行目と4行目は1行目と2行目の間に出力されます。

Streamlitの変数がリセットされないようにsession単位で保存する

今回もStreamlitのお話です。前回、各種ウィジェットの使い方を紹介しましたが、ウィジェットを何かしら操作するとコードが一通り再実行されます。
ウィジェットの値自体は保存されているのですが、それ以外のPythonコード中の変数は全てリセットされてしまいます。

例えば、次のように、スライダーで値を選択してボタンを押したらそれが合計されていく、みたいなことをしようとした(しかしできてない)ソースを書いて見ます。

import streamlit as st


sum_ = 0
value = st.slider(label="スライダー", min_value=0, max_value=10)
if st.button("合計"):
    sum_ += value
    st.write(sum_)

これ、スライダーで値を選んで、「合計」ボタンを押したらどんどん値が足されてその結果が表示されそうなコードに見えませんか?

しかし実際はそのような挙動にならず、スライダーの値を変更したり、ボタンをクリックしたりすると一番最初のsum_=0の部分も再実行されるので値は積み重なっていかず、逐一リセットされます。

また、スライダー動かしただけ、の場合、st.write() の部分で書き出した数字まで消えます。(ボタン押したときだけ数字が表示される。)

このような場合に使うのが、st.session_stateです。
参考: Session State – Streamlit Docs

これはほとんど辞書のように使えます。
一番最初はst.session_stateが空っぽなのでif文で判定を入れて初期化し、それ以降は辞書の値を読み書きするのと同じイメージで実装していきます。先ほどのコードは次のようになります。

import streamlit as st


# session_state に値がない場合初期化する。
if "sum_" not in st.session_state:
    st.session_state.sum_ = 0

value = st.slider(label="スライダー", min_value=0, max_value=10)
if st.button("合計"):
    # session_stateのkeyに対応した値を更新する
    st.session_state.sum_ += value

# 表示
st.write(st.session_state.sum_)

スライダーを動かした時に消えないようにst.writeはif文の外に出しました。これで、合計ボタンを押すたびにスライダーで指定している値分だけ数字が足され、合計が表示されます。

これは単純に変数の値を保存しておくだけでなく、DBやWebなどの外部リソースからのデータ取得を伴う処理の効率化、要するにキャッシュにも使えます。

df = {DBからデータを取ってくる処理}
みたいなのをそのまま実装してしまうと、ウィジェットを何か動かすたびにSQLを発行してDBに負担をかけてしまいます。ここで、一度データを取得したらst.session_state.df などに値を保存しておき、値があったら再取得しないような実装にすると、DBなどの外部リソースへの負荷を減らし、レスポンス速度も改善できます。

ちなみに、値を消したかったら、対象のkeyに対してdelをやれば良いです。
del st.session_state.{key}

また、GUI上でも、右上の3点リーダーにClear cache というメニューがあるのでこれで消せます。

Streamlitの入力ウィジェット

今回もStreamlitの話です。

Streamlitではいろんな操作をインタラクティブに行えるようにさまざまなウィジェットが用意されています。ドキュメントのウィジェットのページはこちら。
参考: Input widgets – Streamlit Docs

テキストエリアやチェックボックス、ラジオボタンにドロップダウン、日時のセレクターなど主要なものは一通り揃っていると思います。

一個一個説明するよりも、ざっと使用例をお見せしたほうがいいと思うのでサンプルコードを最初に紹介します。基本的に、 st.{ウィジェット名}(ラベルなどの引数) で使って、戻り値を変数で受け取ってのちの処理を実装します。今回のサンプルコードでは全部書き出すようにしました。

import streamlit as st

# ボタン。ラベルを指定する。
if st.button("ボタン"):
    st.write("ボタンが押されました!")
else:
    st.write("ボタンがまだ押されていません。")

# チェックボックス。ラベルを指定する。
check = st.checkbox("チェックボックス")
if check:
    st.write("チェックされました")
else:
    st.write("チェックされていません")

# トグル。ラベルを指定する。
toggle = st.toggle("トグル")
if toggle:
    st.write("ONです")
else:
    st.write("OFFです")

# ラジオボタン。ラベルと選択肢を渡す。
color = st.radio(
    "色を選んでください",
    ("赤", "青", "緑")
)
st.write(f"選択された色: {color}")

# 1個の値を選択するドロップリスト。
fruits = st.selectbox(
    "フルーツを選んでください",
    ("りんご", "オレンジ", "バナナ")
)

st.write(f"選択されたフルーツ: {fruits}")
# 複数選択可能なドロップリスト。ラベルと選択肢を指定する。
colors = st.multiselect(
    "色を選んでください",
    ["赤", "青", "緑", "黄", "黒", "白"]
)
# 選択した値は配列で返される。
st.write(f"選択された色: {colors}")

# スライダー。ラベルと最小値, 最大値, 初期値, ステップ を指定する。(省略可)
# 下の例は0才から100才まで5才刻みで選択でき、初期値が20才
age = st.slider('年齢を選択してください', 0, 100, 20, 5)
st.write(f"選択された年齢: {age}")

# 日付入力。
date = st.date_input("日付")
st.write(f"選択された日付: {date}")

# 時間入力。
time = st.time_input("時刻")
st.write(f"選択された時刻: {time}")

# テキストボックス(1行)
text_single = st.text_input("1行のテキストを入力できます。")
st.write(f"入力されたテキスト: {text_single}")

# テキストボックス(複数行)
text_multi= st.text_area("複数行のテキストを入力できます。")
# 出力はマークダウンなので、改行したい場合は行末に半角スペース2個必要です。
st.write(f"入力されたテキスト: {text_multi}")

これを実行すると次のような画面になります。

コードからイメージした通りのものになっているのではないでしょうか。

画面内のウィジェットのうちどれか一つを操作すると、画面全体が再描写されます。他のウィジェットの現時点の値はリセットされないので安心です。(ただ、それ以外の変数の値などはリセットされます。)

先週のグラフの可視化の記事と組み合わせると、ウィジェットで描写するデータを絞り込んで対象のデータ分のグラフを書く、といった使い方ができますね。

Streamlitの標準機能によるグラフ描写

Streamlitの記事2本目です。今回はデータの可視化として、Streamlit標準のグラフ描写機能と少し他のデータ表示方法を紹介します。

前回の記事でも描きましたが、Streamlitにはmatplotlibのグラフを表示する機能があります。また、これ以外にもgraphvizとかplotlyとかのグラフを表示する機能もあって、それらを使えば良いからというのもあってか標準のグラフ作成機能は作成できるグラフの種類がかなり限られます。

具体的には、エリアグラフ/棒グラフ/折れ線グラフ/地図/散布図の5種類です。
ドキュメントはこちら

今回の記事ではこの5種類のグラフと、あと指標の数値をそのまま表示する機能、そして画像データ(行列データ)を表示するサンプルコードをそのまま紹介します。

サクッと動かせるのでコピペして試してみてください。

import streamlit as st
import pandas as pd
import numpy as np

# データの準備
data = pd.DataFrame(
    np.random.randn(20, 3),
    columns=['a', 'b', 'c']
)

# エリアグラフ
st.area_chart(data)
# 棒グラフ
st.bar_chart(data)
# 折れ線グラフ
st.line_chart(data)
# 散布図(x軸、y軸に利用したい列を指定して使う)
st.scatter_chart(data, x="a", y="b")

# 地図表記用の緯度経度データの作成
data_map = pd.DataFrame(
    {
        'lat': [37.76, 37.76],
        'lon': [-122.4, -122.41]
    }
)
# 地図
st.map(data_map)

# メトリックを表示。valueが値で、deltaで変化幅を表示可能
st.metric(label="為替", value="165円", delta="-2円")

# imageで画像データを表示可能。identityはサンプルとして用意した斜め線の図(単位行列)
st.image(1-np.identity(100), caption="斜線")

念のためですが、実行方法は $streamlit run {ファイル名} です。

Streamlit入門

以前から気になっていたのですが、Streamlitというライブラリを最近本格的に使い始めました。これは、簡単にWebアプリケーションを作成できるPythonライブラリなのですが、データの可視化や分析を行うアプリケーションの作成に使うことを念頭において開発されており、僕らの業務と大変相性の良いライブラリです。

インストールとサンプルの実行

インストールはPyPIからpipで行えます。
参考: streamlit · PyPI

インストールしたあと、PyPIのサイトに掲載されているコマンドでサンプルを起動できます。

$ pip install streamlit
$ streamlit hello

結構面白いサンプルなので、これからStreamlitを使っていこうというモチベーションを上げる意味でも一度試すことをお勧めします。

ここから超基礎的な使い方の説明に入ります。(上記のサンプルよりしょぼくてすいません。今後の記事でもう少し色々解説します。)

最もシンプルなアプリの実装

最初に、起動の確認としてただテキストを表示するだけのアプリを作ってみましょう。

app.py というファイルに以下のコードを書いて保存します。

import streamlit as st

st.title('初めてのStreamlitアプリケーション')
st.write('こんにちは、Streamlit!')

そして、次のコマンドで起動します。

$ streamlit run app.py

これで、タイトルとテキストを表示するだけのアプリが起動します。(localhostの8501番ポートです)

インタラクティブなウィジェットの利用

少しだけ動きを出してみます。ドキュメントのウィジェットのページ に色々紹介されているのですが、ここではスライダーを試します。選択した値を表示してみましょう。

import streamlit as st

st.title('スライダーの例')

value = st.slider('数値を選んでください', 0, 100, 50)
st.write('選択した数値:', value)

これを同じように実行すると、スライダーが表示され、0から100の整数(初期値が50)を一つ選べ、スライダーを動かすとその下のテキストボックスに選んだ数値が表示されます。

データフレームの表示

次は、データフレームの表示方法を紹介します。StreamlitはPandas (と他にもPyArrow, Snowpark,PySpark) のDataFrameを表示する専用のメソッドを持っているのです。

参考: st.dataframe – Streamlit Docs

import streamlit as st
import pandas as pd

st.title('データフレームの表示')

data = {
    '名前': ['Alice', 'Bob', 'Charlie'],
    '年齢': [24, 27, 22],
    '得点': [88, 92, 85]
}
df = pd.DataFrame(data)

st.write('データフレーム:')
st.dataframe(df)

これでデータフレームが表示されます。

matplotlibのグラフの表示

最後にmatplotlibのグラフを表示する方法を紹介します。一応、データフレームとグラフが表示できたら超最低限のダッシュボードは作れます。

matplotlibのグラフは、st.pyplotメソッドで表示します。とりあえずsinのグラフでも表示しておきましょう。

import streamlit as st
import matplotlib.pyplot as plt
import numpy as np

# タイトルを設定
st.title('matplotlibのグラフを表示する例')

# データを作成
x = np.linspace(0, 10, 100)
y = np.sin(x)

# matplotlibでグラフを作成
fig, ax = plt.subplots()
ax.plot(x, y, label='sin(x)')
ax.set_xlabel('X軸')
ax.set_ylabel('Y軸')
ax.set_title('Sine Wave')
ax.legend()

# Streamlitでグラフを表示
st.pyplot(fig)

matplotlibの使い方に慣れている人(ぼくもそうです)はとりあえずいつものノリでグラフを書いて渡すだけで表示できるので便利です。

これ以外にも、Streamlit自体の機能でのグラフ描写等も行えるので今後の記事で紹介していきたいと思います。

http.serverでCGIを動かす

今回までhttp.serverの話です。

前回の記事でカスタムハンドラーを作成する方法を紹介しましたのでPythonを使って動的にサイトを作ることもできるようになりましたが、これ以外にもCGIを使って動的なサイトを作ることもできます。
参考: Pythonのhttp.serverモジュールでカスタムハンドラーを実装する方法

要するに普通にPythonファイルをドキュメントルートに設置しておいて、それを動かせるわけですね。

一番シンプルな方法は、 http.serverをコマンドで起動する際に –cgi オプションをつけることです。デフォルトでは、ドキュメントルート直下の、 /cgi-bin と /htbin の 二つのディレクトリに配置されたファイルはCGIとして処理されるようになります。

この二つのディレクトリに固定されるのは、class http.server.CGIHTTPRequestHandler の、cgi_directories ってプロパティにそう指定されているからです。逆にこれ以外のディレクトリに.pyファイルを配置していてもhtmlファイルと同じようにただそのファイルの中身が返されます。

やってみましょう。

cgi-binというディレクトリを作成して、その直下に sample-cgi.py というファイル名で以下のスクリプトを書いておきます。

#!/usr/bin/env python

print("Content-Type: text/html; charset=utf-8\n")
print("<html><body>")
print("<h1>CGIスクリプト実行!</h1>")
print("</body></html>")

そして、このファイルにchmod 744 で実行権限をつけておきます。これ重要です。

そして、http.serverを起動します。

 % python -m http.server --cgi

こうすると localhost:8000/cgi-bin/sample-cgi.py にブラウザでアクセスすると、
CGIスクリプト実行! の文字が表示されます。

CGIで作成するとファイル名とURLがそのままシンプルに対応していくのでいくつも作る場合はシンプルで良いですね。

実は、class http.server.CGIHTTPRequestHandler というのを使うと、CGIディレクトリの指定とかがもっと柔軟に行えるのですが、http.server使ってそこまで凝ったことをすることもないんじゃないかなぁと思うので簡潔ですが今回の記事はここまでとします。

Pythonのhttp.serverモジュールでカスタムハンドラーを実装する方法

前回の記事に続いて、http.serverの話です。せっかくPythonを使ってWebサーバーを立てるわけですからファイルの内容を表示する静的サイトだけでなく、クエリパラメーターやフォームからPOSTされたデータを処理して表示する動的サイトの作り方を軽く紹介しておきます。

ただ、前回の記事でも書きました通り、http.server自体がプロダクション環境に適さない簡易的なものなのであくまでもちょっとした手元のツール等での利用に止めることを推奨します。

カスタムハンドラーの作成

http.serverのBaseHTTPRequestHandlerを継承してカスタムハンドラーを作成することで、特定の処理に対して独自の処理を行えます。

例えば、以下の内容で、sample1.py というファイルを作ってみましょう。

from http.server import BaseHTTPRequestHandler, HTTPServer
import urllib.parse

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/hello":
            self.send_response(200)
            self.send_header("Content-type", "text/html; charset=utf-8")
            self.end_headers()
            self.wfile.write("こんにちは!".encode("utf-8"))
        else:
            super().do_GET()

PORT = 8000
with HTTPServer(("", PORT), MyHandler) as httpd:
    print(f"Serving on port {PORT}")
    httpd.serve_forever()

そして、このファイルを実行します。

% python sample1.py

そうすると、`http://localhost:8000/hello` にアクセすると、こんにちは! のメッセージが表示されます。あとはプログラムで出力したい文字列を作成すれば任意のhtmlを返せますし、テンプレートファイルを読み込んでそれを表示すると言ったこともできます。

クエリパラメータの処理

次は、クエリパラメーターを受け取ってそれに応じた表示をするようにしてみましょう。

ファイル名は sample2.py等で作ります。実行方法は同じようにpythonコマンドにファイル名を渡すだけです。

from http.server import BaseHTTPRequestHandler, HTTPServer
import urllib.parse

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed_path = urllib.parse.urlparse(self.path)
        query_params = urllib.parse.parse_qs(parsed_path.query)
        self.send_response(200)
        self.send_header("Content-type", "text/html; charset=utf-8")
        self.end_headers()
        response = f"Path: {parsed_path.path}, Query parameters: {query_params}"
        self.wfile.write(response.encode("utf-8"))


PORT = 8000
with HTTPServer(("", PORT), MyHandler) as httpd:
    print(f"Serving on port {PORT}")
    httpd.serve_forever()

pathとパラメーターを受け取ってそれを表示するようにしてみました。先ほどの例と同様に、
% python sample2.py で起動して、 http://localhost:8000/hello?name=%E3%82%86%E3%81%86%E3%81%9F%E3%82%8D%E3%81%86 にアクセスすると、
Path: /hello, Query parameters: {‘name’: [‘ゆうたろう’]}
という表示が得られます。

上記のスクリプトでパスとパラメーターが取得できているのであとはそれを自由に活用するコードを書くだけです。

POSTリクエストの処理(フォームデータの処理)

最後にポストされたデータの処理方法を書いておきます。

これはポストするフォームも必要なのでそちらから用意します。
index.html という名前で次のファイルを作っておいてください。

<!DOCTYPE html>
<html>
<head>
    <title>Form Submission</title>
</head>
<body>
    <form action="/submit" method="post">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name"><br>
        <label for="age">Age:</label>
        <input type="text" id="age" name="age"><br>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

そして作成するpythonファイル、 sample3.pyを用意します。

from http.server import BaseHTTPRequestHandler, HTTPServer
import urllib.parse

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/":
            self.send_response(200)
            self.send_header("Content-type", "text/html; charset=utf-8")
            self.end_headers()
            with open("index.html", "rb") as file:
                self.wfile.write(file.read())
        else:
            self.send_response(404)
            self.end_headers()

    def do_POST(self):
        if self.path == "/submit":
            content_length = int(self.headers["Content-Length"])
            post_data = self.rfile.read(content_length)
            data = urllib.parse.parse_qs(post_data.decode("utf-8"))
            self.send_response(200)
            self.send_header("Content-type", "text/html; charset=utf-8")
            self.end_headers()
            response = f"Received: {data}"
            self.wfile.write(response.encode("utf-8"))
        else:
            self.send_response(404)
            self.end_headers()

PORT = 8000
with HTTPServer(("", PORT), MyHandler) as httpd:
    print(f"Serving on port {PORT}")
    httpd.serve_forever()

これで、 localhost:8000 にアクセスすると、 do_GETメソッドが実行されてindex.htmlのファイルの中身(フォーム)が表示され、フォームにデータをPOSTすると、do_POSTメソッドが実行されてフォームで送信した内容が表示されます。

簡単ではありますが、以上がhttp.serverモジュールを利用したカスタムハンドラーの作成やクエリパラメーターの処理、POSTリクエストの処理方法でした。