從 0 開始寫 AI 評測平臺 -- streamlit 中的路由

孙高飞發表於2024-12-18

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 的內容會在實戰這部分內容的時候介紹。 順便再推銷一下自己的星球。

如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章