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

然后我们每个页面的代码如下:

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()

效果如下图:

下面是主页的整体思路:

结尾

好了,今天先讲到这里, 单独介绍 streamlit 的基础部分的内容就到这里了, 下一期直接讲多模态大模型的测试平台的构建。 更多 streamlit 的内容会在实战这部分内容的时候介绍。 顺便再推销一下自己的星球。


↙↙↙阅读原文可查看相关链接,并与作者交流