streamlit 中預設如何設計頁面與頁面之間的聯動
上一次的文章中也說過 streamlit 是沒有路由的概念的, 它所有的東西其實都是在一個頁面展示的。 那它如何控制什麼時候應該展示什麼樣的內容呢? 還是透過上次介紹的 session_state 這個快取來控制的。 我們可以設定一個 button,點選這個 button 後就往 session_state 中新增一個資料,然後在頁面展示的時候判斷這個資料是否存在於 session_state 中,如果存在就展示特定的內容。 如下:
import streamlit as st
# 設定 3 個按鈕
button_1 = st.button("新增資料 A")
button_2 = st.button("新增資料 B")
button_3 = st.button("新增資料 C")
# 根據按鈕點選情況往 session_state 中新增資料
if button_1:
st.session_state["data"] = "A"
elif button_2:
st.session_state["data"] = "B"
elif button_3:
st.session_state["data"] = "C"
if 'data' in st.session_state:
if st.session_state['data'] == "A":
st.write('當前展示的是dataA')
elif st.session_state['data'] == "B":
st.write('當前展示的是dataB')
elif st.session_state['data'] == "C":
st.write('當前展示的是dataC')
效果如下:
所以其實在 streamlit 我們是可以把所有的邏輯都寫在一個 py 檔案中, 或者也可以寫在多個 py 檔案中,並用一個 main.py 來決定應該顯示哪個 py 檔案中的哪個函式的內容。 但這樣不符合我們的習慣,並且有諸多缺點, 比如最大的缺點是無法透過 url 定位到特定的頁面功能上。 比如我們寫了測試平臺, 然後有個測試報告想給其他人看。 你沒辦法給對方一個 url 來快速訪問到這個測試報告的內容。
streamlit 自帶的多頁面應用
# main.py
import streamlit as st
from pages import home, about, contact
PAGES = {
"Home": home,
"About": about,
"Contact": contact
}
def main():
st.sidebar.title("Navigation")
page = st.sidebar.radio("Go to", list(PAGES.keys()))
PAGES[page]()
if __name__ == "__main__":
main()
# pages/home.py
import streamlit as st
def main():
st.title("Home Page")
# Home page content
main()
# pages/about.py
import streamlit as st
def main():
st.title("About Page")
# About page content
main()
# pages/contact.py
import streamlit as st
def main():
st.title("Contact Page")
# Contact page content
main()
上面的程式碼片段中我們定了 4 個 py 檔案, 一個主檔案和 3 個子頁面檔案, 我們用 st.sidebar.radio 這個元件來切換應該顯示哪個 python 檔案。 但這個方式仍然無法解決我們上面解決的問題。
利用 st.query_params 來封裝路由功能
在 streamlit 中使用者可以使用 st.query_params 來對頁面當前的 url 引數進行處理。 可以從 url 中獲取引數的值, 也可以設定 url 引數的值。
我們可以從 url 中獲取對應的引數,來決定渲染哪個頁面:
page = st.query_params["page"]
if page == 'home_page':
home_page.write()
if page == 'nav_page':
nav_page.write()
首先,程式透過 query_params 獲取 url 中的 page 引數的值, 然後根據不同的值來判斷應該渲染哪個頁面。
而當我們需要在程式碼中跳轉頁面的時候, 可以像下面一樣:
st.query_params["page"] = 'home_page'
st.query_params.task = 'your_task_id'
st.rerun()
st.rerun 用於重新渲染頁面,但是 url 中的引數不變, 這就可以讓頁面透過上面的程式碼根據 page 的值來判斷應該渲染哪一個頁面了。
一個頁面架構推薦的形態
首先定義一個 Page 類:
class Page:
def __init__(self, route):
self.route_path = route
def refresh_route(self):
st.query_params["page"] = self.route_path
def route(self):
st.query_params["page"] = self.route_path
time.sleep(0.1) # 需要等待路由更新
st.rerun()
def get_route(self):
return self.route_path
def write(self):
pass
- 在我們的設計中, url 中一定要帶一個名字叫 page 的引數,用來指定當前應該渲染哪個頁面,所以 init 方法中定義了要傳一個引數來定義
- Page 類的主要作用就是定義一些公共的能力。 其中就包括了 route 方法。其中 st.rerun 用來重新渲染頁面。這通常用於在程式碼中進行頁面的跳轉。
- wirte 方法用於讓子類重寫, 所有的頁面渲染邏輯就在這裡編寫。 之所以取名 write,也是為了跟 streamlit 的風格保持一致。
然後我們每個頁面的程式碼如下:
class DocParse(Page):
def write(self):
# 頁面渲染程式碼
doc_parse = DocParse('doc_parse')
然後我們還需要一個整個平臺的主頁:
import streamlit as st
from page.page import Page
from page.mllm_task import mllm_test
from page.mllm_task_detail import mllm_test_detail
from page.mllm_test_compare import mllm_test_compare
from page.doc_parse import doc_parse
from page.doc_parse_compare import doc_parse_compare
from page.doc_parse_data_detail import doc_parse_data_detail
class MultiApp:
""" 整個平臺的主頁面,所有子頁面需要按照它的標準進行初始化. 每個子頁面物件都要整合page.page中的Page父類
Usage:
app = MultiApp()
app.add_xiaoguo_app("專案管理", project)
app.run()
"""
def __init__(self):
self.apps = []
self.extra_apps = []
self.buttons_status = []
def add_xiaoguo_app(self, title, page):
""" 這裡定義的頁面會顯示在側邊導航欄
Parameters
----------
page:
頁面物件,該物件需要繼承page.page中的Page父類
title:
顯示在側邊導航欄的名字
"""
self.apps.append({
"title": title,
"page": page,
})
def add_extra_app(self, page):
""" 這裡定義的頁面不會顯示在側邊導航欄
Parameters
----------
page:
頁面物件, 該頁面不會顯示在側邊導航欄
"""
self.extra_apps.append({
"page": page,
})
def run(self):
"""
負責主頁面顯示的函式,主要透過以下步驟:
1. 定義側邊導航欄架構。
2. 獲取當前url中是否已經設定了page引數,如果有page引數則並遍歷已註冊的所有頁面物件並導航到對應的頁面那種, 如果沒有則直接顯示預設首頁
"""
# ratio的回撥函式, 負責設定url並導航到對應子頁面
def change_route():
app = st.session_state['app_key'] # 獲取當前被選中的頁面
app['page'].refresh_route() # 要重置一下url中的引數
# 獲取當前URL中是否已經帶了page引數, page引數決定了應該顯示哪個子頁面.
route = st.query_params.get('page')
default_page = 0
if route:
for a in self.apps:
pa: Page = a['page']
print(pa.get_route())
if route == pa.get_route():
default_page = self.apps.index(a)
# step 1: 定義側邊導航欄架構
st.sidebar.title("任務導航")
with st.sidebar.expander("效果測試管理", expanded=True):
app = st.radio(
'',
self.apps,
format_func=lambda app: app['title'],
on_change=change_route,
key="app_key",
index=default_page,
)
url = 'http://localhost:8501/'
st.markdown(f'<a href="{url}" target="_self">{"返回首頁"}</a>', unsafe_allow_html=True)
if route:
for a in self.apps:
pa: Page = a['page']
if route == pa.get_route():
pa.write()
for a in self.extra_apps:
pa: Page = a['page']
if route == pa.get_route():
pa.write()
else:
app['page'].refresh_route()
app['page'].write()
st.set_page_config(layout='wide')
app = MultiApp()
# 開始註冊效果測試側邊欄
app.add_xiaoguo_app("多模態", mllm_test)
app.add_xiaoguo_app("文件解析", doc_parse)
# app.add_extra_app(doc_split_detail)
app.add_extra_app(mllm_test_detail)
app.add_extra_app(mllm_test_compare)
app.add_extra_app(doc_parse_compare)
app.add_extra_app(doc_parse_data_detail)
app.run()
效果如下圖:
下面是主頁的整體思路:
- 需要一個側邊欄元件:st.sidebar.expander,透過側邊欄來協調各個子模組的展示。
- 各個子模組都有獨立的 page 頁面來負責, 這些頁面都是要繼承上面說的 Page 類的,在主頁的類 MultiApp 中有一個 add_xiaoguo_app 方法,用這個方法把子模組的頁面類加入到一個 dict 中。而不需要在側邊欄展示的頁面, 則使用 add_extra_app 加入到另一個 dict 中
- 透過 st.radio 來展示側邊欄中每個子模組的條目。 使用者選擇哪個,就去呼叫對應的子模組頁面的 write() 方法來渲染
- 渲染頁面的時候需要透過 st.query_params 獲取當前 url 中 page 引數,來判斷應該渲染哪個頁面。
結尾
好了,今天先講到這裡, 單獨介紹 streamlit 的基礎部分的內容就到這裡了, 下一期直接講多模態大模型的測試平臺的構建。 更多 streamlit 的內容會在實戰這部分內容的時候介紹。 順便再推銷一下自己的星球。