目 录CONTENT

文章目录
ACM

Python 库 - Streamlit

解小风
2024-09-10 / 0 评论 / 3 点赞 / 148 阅读 / 39639 字

简介

Streamlit 是一个用于创建数据科学和机器学习应用程序的开源 Python 库,它的主要目标是使开发人员能够以简单的方式快速构建交互式的数据应用,而无需过多的前端开发经验,无需编写 HTML、CSS、JavaScript,即可快速、轻松地构建交互式 Web 应用程序。


安装与配置

安装 streamlit

# 安装
pip install --no-cache-dir -i https://mirrors.bfsu.edu.cn/pypi/web/simple streamlit
# 查看版本
import streamlit as st
print(st.__version__)

配置 streamlit

# 编辑配置文件(如果没有则自行创建:~/.streamlit/config.toml)
# 该步骤非必须,可以全使用默认配置,然后在运行时直接指定端口号即可
[server]
port = 9080
enableCORS = false
headless = true
enableCaching = true
maxUploadSize = 200

[browser]
serverAddress = "localhost"
gatherUsageStats = false

[runner]
magicEnabled = false


# 参数说明
# port: streamlit 应用端口号,默认{8501}
# enableCORS: 是否启用跨域资源共享,决定了是否允许从其他源访问 streamlit 应用,默认{false}
# headless: 是否在无头模式下运行,意味着不打开任何浏览器窗口,默认{false}。设置为 true 表示在没有GUI的情况下启动应用,在服务器或CI环境中运行 streamlit 应用非常有用
# enableCaching: 是否启用静态资源缓存,默认{true}。当启用缓存时,浏览器会存储应用的静态资源(如JavaScript、CSS文件),从而加快页面加载速度
# maxUploadSize: 设置上传文件的最大大小(单位:MB),默认{200}
# serverAddress: streamlit 服务器地址,默认{"localhost"}
# gatherUsageStats: 是否允许 streamlit 收集使用统计信息,默认{true},一般禁用即可
# magicEnabled: 是否启用 streamlit 的魔法命令功能,默认{true}。魔法命令功能即任何时候如果 streamlit 看到一个变量或常量值,则自动将其使用 st.write 显示,可能容易导致网页速度变慢、重复加载数据等

运行 streamlit

# 运行 streamlit(以 myapp.py 应用文件为例)
# 不指定端口号(使用默认的端口号{8501})
streamlit run myapp.py

# 指定端口号
streamlit run myapp.py --server.port 9080

Tips


基础知识

# 首先导入 streamlit 库
import streamlit as st

# 页面具有的属性方法,侧边栏基本上也都具备,如:
st.header("这是一个较小的标题")
st.sidebar.header("这是一个较小的标题")

st.markdown('''# 静夜思''')
st.sidebar.markdown('''# 静夜思''')

页面显示

网页全局配置

# st.set_page_config() 设置网页全局配置
# page_title 浏览器网页标签页标题
# layout 页面布局模式,可选["centered", "wide"],默认情况下 streamlit 应用程序是居中对齐的,而 "wide" 布局会扩展整个浏览器窗口宽度,适合展示图表或需要大量空间的内容
# initial_sidebar_state 侧边栏初始状态,可选["auto", "expanded", "collapsed"]

st.set_page_config(
    page_title='my streamlit app', 
    layout="wide", 
    initial_sidebar_state="expanded")

页面布局

# 上下布局:代码顺序
# streamlit 会逐行读取代码,并翻译为前端语言(HTML,CSS),然后进行页面渲染并显示
# 因此,代码顺序即为页面中的上下显示顺序
# 左右布局:st.columns()

# 左右两列布局
col1, col2 = st.columns(2)
col1.write('我是左侧')
col2.write('我是右侧')

# 更多列布局且控制列宽
col1, col2, col3 = st.columns([3,1,1])
with col1:
    st.write('我是第 1 列')
with col2:
    st.write('我是第 2 列')
with col3:
    st.write('我是第 3 列')

标题

# st.title() 创建页面主标题,通常是最大和最显眼的标题。它是在应用的顶部设置的,并且在整个页面中通常只使用一次,用于表示应用的主题或总体内容
# st.header() 创建相对较小的标题,比 st.title() 稍小一些,可以在应用中多次使用,用于将内容分成不同的部分或主题
# st.subheader() 创建相对较小的标题,比 st.header() 还小,可以在应用中多次使用,用于在小节内更细致地标识内容

st.title("这是一个标题")
st.header("这是一个较小的标题")
st.subheader("这是一个相对较小的标题")

文本

# st.caption() 显示描述性文本或注释,一般会置灰显示
# st.text() 只显示纯字符串内容
# st.markdown() 支持 markdown 格式的文本,使用 markdown 语法来添加样式、链接、列表等元素,提供更灵活的富文本内容呈现

st.caption('这是需要显示的注释文本,一般会置灰显示')

st.text('''
静夜思
床前明月光,疑是地上霜。
举头望明月,低头思故乡。
''')

st.markdown('''
# 静夜思
床前**明月**光,疑是地上霜。
举头望**明月**,低头思故乡。
''')

# 带链接
st.sidebar.markdown('''
<small>参考文档:[docs](https://docs.streamlit.io/), 版本:[Streamlit v1.25.0](https://www.streamlit.io/) </small>''', unsafe_allow_html=True)

代码块

# st.code() 用于显示代码块,主要入参:
# body 要显示的代码字符串
# language 代码语言,字符串,默认{"python"},如果省略则没有语法高亮效果

st.markdown('**以下为打印的代码:**')
 
st.code('''
def bubble_sort(arr):
    n = len(arr)
    # 遍历所有数组元素
    for i in range(n):
        # 最后 i 个元素已经排好序,不需要再比较
        for j in range(0, n-i-1):
            # 如果元素比下一个元素大,则交换它们
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
# 示例使用
if __name__ == "__main__":
    # 测试数据
    example_list = [64, 34, 25, 12, 22, 11, 90]
    print("原始数组:", example_list)
    # 调用冒泡排序函数
    bubble_sort(example_list)
    print("排序后的数组:", example_list)
''', language='python')

数学公式

# st.latex() 用于在应用程序中展示 latex 公式

st.latex(r''' e^{i\pi} + 1 = 0 ''')

JSON数据

# st.json() 用于展示 JSON 格式的数据,会自动适应 JSON 数据的大小,如果数据较大会自动启用滚动条,并且可以处理包含嵌套结构的复杂 JSON 数据,以树状结构的形式展示。

st.json({
    'foo': 'bar',
    'baz': 'boz',
    'stuff': [
        'stuff 1',
        'stuff 2',
        'stuff 3',
        'stuff 5',
    ],
})

统计指标

# st.metric() 用于显示一个带有标签(label)和值(value)的度量指标,并且可以附加一个变化量(delta),用于展示该度量相对于前一次值的变化情况。这个函数非常适合用来展示关键性能指标(KPIs)、统计数据或者任何需要强调变化的重要数值

st.metric(label="温度", value="40.2 ℃", delta="+2.7 ℃")
st.metric(label="大气压", value="1 atm", delta="-0.02 atm")
st.metric(label="增长率", value="50%", delta="+3%")

通知消息

# st.toast 在页面右上角显示一条自定义文本的通知
# st.success 显示成功消息,通常用于向用户报告任务或操作成功完成
# st.info 显示一般信息消息,可以用于提供一般性的信息或指导
# st.warning 显示警告消息,通常用于向用户提供潜在的问题或需要注意的情况
# st.error 显示错误消息,通常用于向用户报告发生的错误或异常
# st.exception 用于显示异常消息,当发生异常时,可以使用此函数将异常信息呈现给用户

# 显示自定义文本通知
st.toast('这是一个自定义文本通知,将动态出现在页面右上角')

st.success('成功消息') 
st.info('提示消息') 
st.warning('警告消息')
st.error('错误消息') 

# 异常消息
try:
    # 抛出异常
    raise ValueError("出现异常")
except Exception as e:
    # 显示异常消息
    st.exception(e)

通用打印

# st.write() 用于在应用程序中展示文本和数据的通用函数,可接受多种类型入参,包括 markdown 格式的字符串、数字、DataFrame、图表等。

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

# 字符串
st.write("这是一段文本。")
 
# 数字
st.write(42)
 
# 列表
st.write([1, 2, 3])
 
# 字典
st.write({"key": "value"})
 
# 数据框(DataFrame)
df = pd.DataFrame({"Column 1": [1, 2, 3], "Column 2": ["A", "B", "C"]})
st.write(df)
 
# 多参数用法
st.write("这是一个字符串", 42, [1, 2, 3], {"key": "value"})
 
# 自定义渲染
fig, ax = plt.subplots()
x = np.linspace(0, 10, 100)
y = np.sin(x)
ax.plot(x, y)
st.write(fig)

表格

# st.dataframe() 以表格的形式呈现数据,支持 Pandas 的特有功能,如排序、过滤等,并且会自动适应数据框的大小,如果数据框太大会自动启用滚动条。入参如下:
# width:UI元素的期望宽度,单位为像素,类型为 Int 或 None,如果是 None 的话将基于页面宽度计算元素宽度
# height:UI元素的期望高度,单位为像素,类型为 Int 或 None

# st.table() 显示通用表格数据,不仅支持 Pandas,还可以处理列表、元组等可迭代数据结构,但仅用于显示数据,不提供排序和过滤等数据框专有功能

import pandas as pd
import numpy as np

random_data = np.random.rand(100, 10)
df = pd.DataFrame(random_data, columns=[f'Col{i}' for i in range(1, 11)])
st.dataframe(df)
st.table(df)
# 将每列最大值高亮显示
st.dataframe(df.style.highlight_max(axis=0))
# 使用 add_rows 在已经显示的 DataFrame 或 图表 上添加新的行数据

import pandas as pd

# DataFrame 数据
df1 = pd.DataFrame({
    'A': [1, 2, 3],
    'B': [4, 5, 6]
})
df2 = pd.DataFrame({
    'A': [4, 5],
    'B': [6, 7]
})

# 显示 df1 并获取元素引用
element_df = st.dataframe(df1)
# 创建 df1 线形图并获取元素引用
element_chart = st.line_chart(df1)

# 向已显示的 DataFrame 添加新行
element_df.add_rows(df2)
# 向已显示的线形图添加新行数据
element_chart.add_rows(df2)

图表

# st.line_chart() 折线图
# st.area_chart() 区域图
# st.bar_chart() 柱状图
# st.pyplot() matplotlib 图表
# matplotlib 支持几种不同的后端。如果在 streamlit 中使用 matplotlib 出现问题, 可以尝试将后端设置为 “TkAgg”
# echo "backend: TkAgg" >> ~/.matplotlib/matplotlibrc
# st.plotly_chart() plotly 图表

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px

# 创建数据集
df = pd.DataFrame(np.random.randn(10, 2), columns=('x', 'y'))
# 折线图
st.line_chart(df)
# 区域图
st.area_chart(df)
# 柱状图
st.bar_chart(df)
# 使用 matplotlib 创建一个图表并使用 st.pyplot() 显示
fig, ax = plt.subplots()
ax.scatter(df['x'], df['y'])
st.pyplot(fig)
# 使用 plotly 创建一个图表并使用 st.plotly_chart() 显示
fig = px.scatter(df, x='x', y='y')
st.plotly_chart(fig)

地图

# st.map() 显示地图及叠加的数据点,支持自动居中与自动缩放。入参:
# data 要显示的数据,必须包含字段 lat、lon、latitude 或 longitude,可以是 DataFrame、Styler、数组以及其他可迭代对象等类型
# zoom 缩放等级
# use_container_width {bool} 是否使用容器的整个宽度。如果设置为True,地图将占据整个容器的宽度

data = {
    'latitude': [37.7749, 34.0522, 40.7128],
    'longitude': [-122.4194, -118.2437, -74.0060],
    'name': ['San Francisco', 'Los Angeles', 'New York']
}
st.map(data, zoom=4, use_container_width=True)

图像

# st.image() 用于在应用程序中显示图像,接受多种输入格式,包括文件路径、URL、图像的字节数据等,默认参数为:
# st.image(image, caption=None, width=None, use_column_width=False, clamp=False, channels='RGB', format='JPEG')
# image 要显示的图像,可选[numpy.ndarray,[numpy.ndarray],BytesIO,str,[str]]。单色图像为 (w,h) 或 (w,h,1);彩色图像为 (w,h,3);RGBA图像为 (w,h,4);也可以指定一个图像 url 或 url列表
# caption {str} 图像标题。如果显示多幅图像,caption 应当是字符串列表
# width 图像宽度,None 表示使用图像自身宽度
# use_column_width {bool} 设置为 True 则使用列宽作为图像宽度
# clamp 是否将图像的像素值压缩到有效域(0~255),仅对字节数组图像有效
# channels 图像通道类型,可选['RGB','BGR'],默认值{'RGB'}
# format 图像格式,可选['JPEG','PNG'],默认值{'JPEG'}

from PIL import Image

image = Image.open('test.jpg')
st.image(image, 
         caption='标题',
         width = 500)

音频

# st.audio() 用于在应用程序中播放音频,支持多种音频来源,包括本地文件、URL和字节数据,入参如下:
# format {str or None} 视频格式,可选['audio/wav','audio/mp3','audio/aac',],默认{'audio/wav'}。如果设置为 None 将尝试根据文件扩展名自动识别视频格式
# start_time {int} 音频开始播放的时间点(秒),默认从头开始播放

audio_file = open('test.wav', 'rb')
audio_bytes = audio_file.read()
# 本地音频
st.audio(audio_bytes , format='audio/wav', start_time=2)
# 网络音频
st.video("http://www.w3school.com.cn/i/test.wav")

视频

# st.video() 用于在应用程序中显示视频,支持多种视频来源,包括本地文件、URL和字节数据,入参如下:
# format {str or None} 视频格式,可选['mp4','webm']。如果设置为 None 将尝试根据文件扩展名自动识别视频格式
# start_time {int} 视频开始播放的时间点(秒),默认从头开始播放

video_file = open('test.mp4', 'rb')
video_bytes = video_file.read()
# 本地视频
st.video(video_bytes, format="mp4", start_time=2)
# 网络视频
st.video("http://www.w3school.com.cn/i/movie.mp4")

交互控件

按钮

# st.button() 创建按钮,返回 bool 值,表示在上个应用周期,按钮是否被点击

if st.button('点我'):
    st.write('今天是个好日子!')

标签页

# st.tabs() 创建标签页,让用户在不同内容区域间切换浏览

# 创建标签页
tab1, tab2, tab3 = st.tabs(["标签 1", "标签 2", "标签 3"])
# 在每个标签页中添加内容
with tab1:
    st.write("这是标签页 1 的内容")
    st.radio('请选择:', [1, 2])
with tab2:
    st.write("这是标签页 2 的内容")
with tab3:
    st.write("这是标签页 3 的内容")

单选框

# st.radio() 用于创建单选按钮组,使用户能够从一组选项中选择一个。入参如下:
# label {str} 必填,单选框文本,将显示在按钮组上方,介绍单选框用途
# options {list,tuple,dict,None} 必填,提供可供选择选项的列表、元组或字典。对于列表或元组,选项将按照它们在列表中的顺序显示。对于字典,将显示字典的键,并将字典的值用作用户选择的实际值。
# index {int},可选,单选按钮组的初始选择索引,默认{0},即默认选中第一个选项
# format_func {function,None},可选,用于格式化选项的函数,以便在显示时进行自定义格式
# help {str,None},可选,为单选按钮组提供帮助文本,将在用户悬停在组件上时显示

sex = st.radio(
    label = '请输入您的性别',
    options = ('男', '女', '保密'),
    index = 2,
    format_func = str,
    help = '如果您不想透露,可以选择保密')
if sex == '男':
    st.write('男士您好!')
elif sex == '女':
    st.write('女士您好!')
else:
    st.write('您好!')

复选框

# st.checkbox() 用于创建复选框,入参:
# value 默认{False},若设置为 True 则复选框初始状态即为选中状态,返回 bool 值,表示在上个应用周期,复选框是选中

cb = st.checkbox('确认', value=False)
if cb:
    st.write('确认成功')
else:
    st.write('没有确认')

下拉单选框

# st.selectbox() 创建一个下拉选择框,使用户能够从一组选项中选择一个。入参如下:
# label {str} 必填,单选框文本,将显示在按钮组上方,介绍单选框用途
# options {list,tuple,dict,None} 必填,提供可供选择选项的列表、元组或字典。对于列表或元组,选项将按照它们在列表中的顺序显示。对于字典,将显示字典的键,并将字典的值用作用户选择的实际值。
# index {int},可选,单选按钮组的初始选择索引,默认{0},即默认选中第一个选项
# format_func {function,None},可选,用于格式化选项的函数,以便在显示时进行自定义格式
# help {str,None},可选,为单选按钮组提供帮助文本,将在用户悬停在组件上时显示

sex = st.selectbox(
    label = '请输入您的性别',
    options = ('男', '女', '保密'),
    index = 2,
    format_func = str,
    help = '如果您不想透露,可以选择保密')
if sex == '男':
    st.write('男士您好!')
elif sex == '女':
    st.write('女士您好!')
else:
    st.write('您好!')

下拉多选框

# st.multiselect() 创建一个多选框,允许用户从一组选项中选择多个。入参如下:
# label {str} 必填,多选框的标签,将显示在多选框上方,用于标识多选框的用途。
# options {list,tuple,dict,None} 必填,提供可供选择选项的列表、元组或字典。对于列表或元组,选项将按照它们在列表中的顺序显示。对于字典,将显示字典的键,并将字典的值用作用户选择的实际值
# default {list,tuple,dict,None} 可选,多选框的初始选择,默认{None}。如果提供了默认值,则多选框将在初始时显示这些选项
# format_func {function,None},可选,用于格式化选项的函数,以便在显示时进行自定义格式
# help {str,None},可选,为多选按钮组提供帮助文本,将在用户悬停在组件上时显示

options = st.multiselect(
    label = '请问您喜欢吃什么水果',
    options = ('橘子', '苹果', '香蕉', '草莓', '葡萄'),
    default = None,
    format_func = str,
    help = '选择您喜欢吃的水果')
st.write('您喜欢吃的是', options)

连续滑块

# st.slider() 创建一个连续滑块,允许用户在一个范围内选择一个连续的数值,可以是整数或浮点数。入参如下:
# label {str} 必填,滑块的标签,将显示在滑块上方,用于标识滑块的用途。
# min_value {int,float,datetime,None} 可选,滑块最小值,默认{0}
# max_value {int,float,datetime,None} 可选,滑块最大值,默认{100}
# value {int,float,datetime,tuple} 可选,滑块初始值,可以是单个数值或表示范围的元组。如果提供了元组,用户将能够选择一个范围而不是单个值
# step {int,float,None} 可选,滑块步进值,用户可以通过拖动滑块选择,默认{1}
# format 可选,滑块的显示格式。可以是包含 "{:.2f}" 之类的格式字符串,用于控制显示的小数位数
# key 可选,为滑块分配的唯一键,用于识别和跟踪滑块的状态变化,通常用于确保组件的稳定性
# help {str,None},可选,为滑块提供帮助文本,将在用户悬停在组件上时显示

age = st.slider(label='请输入您的年龄', 
                min_value=0, 
                max_value=100, 
                value=0, 
                step=1, 
                help="请输入您的年龄")
st.write('您的年龄是', age)

选择滑块

# st.select_slider() 创建一个选择滑块,允许用户从预定义的一组离散选项中选择一个或多个值,使得用户可以在有限的选项集合中进行选择。入参如下:
# label {str} 必填,滑块的标签,将显示在滑块上方,用于标识滑块的用途。
# options {list} 可选,包含滑块可选择的所有选项
# value {int,float,datetime,tuple} 可选,滑块初始值,可以是单个数值或表示范围的元组。如果提供了元组,用户将能够选择一个范围而不是单个值
# format 可选,滑块的显示格式。可以是包含 "{:.2f}" 之类的格式字符串,用于控制显示的小数位数
# key 可选,为滑块分配的唯一键,用于识别和跟踪滑块的状态变化,通常用于确保组件的稳定性
# help {str,None},可选,为滑块提供帮助文本,将在用户悬停在组件上时显示

options = [1, '南京', 3, '北京', 5]

# 单选模式
selected_value = st.select_slider(
    '滑动滑块来单选',
    options=options)
# 输出选定值
st.write(f'已选择: {selected_value}')

# 多选模式
selected_values = st.select_slider(
    '滑动滑块来多选',
    options=options,
    value=(1, 3))
# 输出选定值
st.write(f'已选择: {selected_values}')

单行文本输入框

# streamlit.text_input()  用于创建文本输入框,允许用户输入文本信息。入参如下:
# label {str} 必填,文本输入框的标签,将显示在文本输入框上方,用于标识文本输入框的用途
# value {str} 可选,文本输入框的初始值,默认{""}
# max_chars {int,None} 可选,文本输入框允许的最大字符数。如果未提供,则不设置字符数限制
# type {str} 可选['default','password'(密码输入框),'number'(数字输入框)],输入框的类型,默认{'default'}
# help {str,None},可选,为文本输入框提供帮助文本,将在用户悬停在组件上时显示

name = st.text_input('请输入用户名',  max_chars=100, help='最大长度为100字符')
# 根据用户输入进行操作
st.write('您的用户名是', name)

数字输入框

# st.number_input() 创建数字输入框,允许用户输入数字,入参如下:
# label {str} 必填,数字输入框的标签,将显示在数字输入框上方,用于标识数字输入框的用途
# min_value {int,float,None} 可选,数字输入框最小值,如果未提供,则不设置最小值
# max_value {int,float,None} 可选,数字输入框最大值。如果未提供,则不设置最大值
# value {int,float,None} 可选,数字输入框初始值,可以是单个数值或表示范围的元组。如果提供了元组,用户将能够选择一个范围而不是单个值
# step {int,float,None} 可选,数字输入框步进值,用户可以通过点击按钮调整,默认{1}
# format 可选,数字输入框的显示格式。可以是包含 "{:.2f}" 之类的格式字符串,用于控制显示的小数位数
# help {str,None},可选,为数字输入框提供帮助文本,将在用户悬停在组件上时显示

age = st.number_input(label = '请输入您的年龄', 
                     min_value=0, 
                     max_value=100, 
                     value=0, 
                     step=1, 
                     help='请输入您的年龄')
st.write('您的年龄是', age)

多行文本输入框

# st.text_area() 创建文本区域,允许用户输入多行文本信息,入参如下:
# label {str} 必填,多行文本输入框的标签,将显示在多行文本输入框上方,用于标识多行文本输入框的用途
# value {str} 可选,文本区域的初始值,默认{""}
# height {int,None} 可选,文本区域的高度,表示显示的文本行数。如果未提供,则根据内容自动确定高度
# max_chars {int,None} 可选,文本区域允许的最大字符数。如果未提供,则不设置字符数限制
# help {str,None},可选,为多行文本输入框提供帮助文本,将在用户悬停在组件上时显示

text = st.text_area(label = '请输入文本', 
                    value='请输入...', 
                    height=5, 
                    max_chars=200, 
                    help='最大长度限制为200')
st.write('您的输入是', text)

日期输入框

# st.date_input() 创建日期输入框,允许用户选择日期,入参如下:
# label {str} 必填,日期输入框的标签,将显示在日期输入框上方,用于标识日期输入框的用途
# value {datetime.date,None} 可选,日期输入框的初始值,默认{None}
# min_value {datetime.date,None} 可选,日期输入框最小值,用户不能选择早于该日期的日期。如果未提供,则不设置最小值限制
# max_value {datetime.date,None} 可选,日期输入框最大值,用户不能选择晚于该日期的日期。如果未提供,则不设置最大值限制
# help {str,None},可选,为日期输入框提供帮助文本,将在用户悬停在组件上时显示

import datetime
birthday = st.date_input(label = '请输入您的出生年月', 
                     value=None, 
                     min_value=None, 
                     max_value=datetime.date.today(), 
                     help='请输入您的出生年月')
st.write('您的生日是:', birthday)

时间输入框

# st.time_input() 创建时间输入框,允许用户选择时间,入参如下:
# label {str} 必填,时间输入框的标签,将显示在时间输入框上方,用于标识时间输入框的用途
# value {datetime.time,None} 可选,时间输入框的初始值,默认{None}
# step {int,timedelta} 可选,时间输入框的时间间隔
# help {str,None},可选,为时间输入框提供帮助文本,将在用户悬停在组件上时显示

from datetime import time 
t = st.time_input(label = '请输入一个时间', 
                  value=None, 
                  help='请输入一个时间')
st.write('您输入的时间是:', t)

表单

# st.form() 创建一个表单容器,用来分组多个输入控件和其他小部件,能够在用户提交表单之前捕获所有输入,并在表单提交时进行批量处理
# key {str,None} 表单的唯一标识符

# 创建一个简单的登录表单,key='form_login' 是表单的唯一标识符
with st.form(key='form_login'):
    # 表单内的输入字段
    username = st.text_input('用户名')
    # type='password' 使输入框隐藏实际输入的字符,适合用于密码输入
    password = st.text_input('密码', type='password')

    # 创建一个提交按钮,当点击时,可以触发表单内的逻辑处理
    submitted = st.form_submit_button('登录')

    if submitted:
        # 处理表单提交的动作
        st.write(f"用户名: {username}")
        st.write(f"密码: {password}")
        # 在这里可以添加验证逻辑等
        # 如果处理逻辑完成且不需要进一步执行,可在适当位置用 st.stop() 来停止

表格编辑器

# st.data_editor() 允许用户直接在表格中编辑数据,适用于如数据清理、数据分析或用户输入数据的场景,入参如下:
# df Pandas DataFrame 对象,用于显示和编辑
# use_container_width {bool} 默认{False},如果为 True 则数据编辑器的宽度将自动扩展以适应其父容器的宽度
# num_rows {str} 可选["fixed","dynamic"],默认{"fixed"}。"fixed" 不允许用户添加或删除行;"dynamic" 允许用户添加或删除行
# column_config {dict} 用于配置列的显示和编辑方式。键是 DataFrame 列名,值是 st.column_config.* 类型的对象
# column_order {list} 用于指定列的顺序。如果不提供,则按照 DataFrame 的原始列顺序显示
# hide_index {bool} 默认{False},如果为 True 则不显示索引列
# disabled {list} 包含不允许用户编辑的列名
# key {str,None} 该数据编辑器的唯一标志

import pandas as pd

# DataFrame 数据源
data = {
    'date': ['2024-09-15', '2024-09-16', '2024-09-20'],
    'time': ["上午", "上午", "下午"],
    'master': ["张三", "李四", "王五"],
    'money': [200.5, 306, 900]
}
df = pd.DataFrame(data)

# 编辑 DataFrame
edited_df = st.data_editor(
    df, # 数据源
    use_container_width=True, # 使表格宽度适应容器
    num_rows="dynamic", # 允许用户添加或删除行
    # 指定每列的显示和编辑方式
    column_config={
        "date": st.column_config.DateColumn("日期"), # DateColumn 日期类型
        "time": st.column_config.SelectboxColumn("时段", options=["上午", "下午"]), # SelectboxColumn 单选下拉框类型
        "master": st.column_config.TextColumn("业主"), # TextColumn 文本类型
        "money": st.column_config.NumberColumn("预计金额", step=5, help="请输入大于0的数") # NumberColumn 数值类型,可提供帮助文本
    },
    column_order=["date", "time", "master", "money"], # 指定列的显示顺序
    hide_index=True, # 隐藏索引列
    disabled=["date"] # 禁止编辑的列
)

# 输出编辑后的 DataFrame
st.write("编辑后的数据:")
st.write(edited_df)

进度状态

# st.progress 显示一个进度条,可以设置最小值、最大值和当前值,通常用于表示长时间运行的非阻塞任务的进度
# st.spinner 显示一个旋转的加载器,表示任务正在执行,通常用于表示短时间运行的任务的执行状态。当任务完成时,加载器将自动消失

import time

# 显示并更新一个进度条
progress_bar = st.empty()
for i in range(5):
    time.sleep(1)
    progress_bar.progress((i+1)/5, '进度')

# 显示一个进度条,设置当前值,并更新
# 当前进度为 20%
bar = st.progress(20)
time.sleep(2)
# 进度为 50%
bar.progress(50)
time.sleep(3)
# 进度为 100%
bar.progress(100)

# 显示一个旋转加载器
with st.spinner(text='加载中...'):
    time.sleep(5)
    st.success('已完成!')

文件上传

# st.file_uploader() 允许用户选择并上传文件,入参如下:
# uploaded_file = st.file_uploader(label="标签文本", type=None, accept_multiple_files=False, key=None, help=None, on_change=None, args=None, kwargs=None, disabled=False)
# label {str} 必填,文件上传控件的标签,将显示在文件上传框上方,用于标识文件上传框的用途
# type {list,tuple,None} 指定允许上传的文件类型,如 ['csv', 'txt'] 表示只允许 .csv 和 .txt 文件被上传。省略此参数则所有类型的文件都可以上
# accept_multiple_files {bool} 是否一次上传多个文件,默认{False},表示一次只能上传一个文件
# on_change {callable,None} 当文件选择发生变化时调用的回调函数
# args {tuple,None} 传递给 on_change 回调函数的位置参数
# kwargs {dict,None} 传递给 on_change 回调函数的关键字参数
# disabled {bool} 是否禁用文件上传,默认{False},表示不禁用
# help {str,None},可选,为文件上传控件提供帮助文本,将在用户悬停在组件上时显示
# key {str,None} 该文件上传控件的唯一标志
# 返回值 {UploadedFile,list(当 accept_multiple_files=True 时)} 可以通过 UploadedFile 访问上传文件的数据,如读取文件内容等

import pandas as pd
from PIL import Image
import numpy as np

# 创建文件上传器
uploaded_file = st.file_uploader(
    '选择一个文件', 
    type=['txt', 'csv', 'xlsm', 'png', 'jpg', 'wav', 'mp3', 'mp4', 'pt']
    )
if uploaded_file is not None:
    file_type = uploaded_file.name.split('.')[-1]
    # 根据文件类型读取内容
    if file_type == 'txt':
        # 读取文本文件
        content = uploaded_file.read().decode()
        st.text_area('文件内容', content, height=200)
    elif file_type in ['csv', 'xlsm']:
        # 读取 CSV 或 Excel 文件
        if file_type == 'csv':
            df = pd.read_csv(uploaded_file)
        else:
            df = pd.read_excel(uploaded_file)
        st.write(df)
    else:
        # 读取二进制文件内容
        binary_data = uploaded_file.read()
        if file_type in ['png', 'jpg']:
            # 读取图像文件
            image = Image.open(uploaded_file)
            st.image(image, use_column_width=True)
        elif file_type in ['wav', 'mp3']:
            # 读取音频文件
            st.audio(binary_data, format=f'audio/{file_type}')
        elif file_type == 'mp4':
            # 读取视频文件
            st.video(binary_data)
        elif file_type == 'pt':
            # 方式一:读取 .pt 文件为二进制数据流
            st.write(f'文件大小: {len(binary_data)} 字节')
            st.download_button('下载文件', binary_data, file_name=uploaded_file.name)
            # 方式二:加载 PyTorch 模型或张量
            try:
                import torch
                model_or_tensor = torch.load(uploaded_file, map_location=torch.device('cpu'))
                st.write('PyTorch 模型或张量已加载')
                if isinstance(model_or_tensor, torch.Tensor):
                    st.write(model_or_tensor)
                else:
                    st.write('模型结构复杂,无法直接展示')
            except Exception as e:
                st.error(f"加载 PyTorch 模型或张量失败: {e}")

文件下载

# st.file_uploader() 允许用户下载文件,入参如下:
# downloaded = st.download_button(label="标签文本", data="要下载的数据", file_name="文件名", mime="MIME类型", key=None, help=None, on_click=None, args=None, kwargs=None)
# label {str} 必填,文件下载控件的标签,将显示在文件下载框上方,用于标识文件下载框的用途
# data {str,bytes,file-like} 要下载的数据,可以是字符串、字节串或类似文件的对象
# file_name {str} 下载文件的名称,如果未指定将使用默认名称 data.txt
# mime {str} MIME 类型,用来指定文件的格式,可选[text/plain (文本文件),application/pdf (PDF 文件),image/png (PNG 图像),image/jpeg (JPEG 图像),application/octet-stream (任意二进制数据)],如果未指定将尝试根据文件扩展名自动推断。
# on_click {callable} 当下载按钮被点击时调用的回调函数
# args {tuple,None} 传递给 on_click 回调函数的位置参数
# kwargs {dict,None} 传递给 on_click 回调函数的关键字参数
# help {str,None},可选,为文件下载控件提供帮助文本,将在用户悬停在组件上时显示
# key {str,None} 该文件下载控件的唯一标志
# 返回值 {bool} 下载按钮是否被点击

# 文本数据
text_data = "Hello, world!"
# 二进制数据
binary_data = b"\x00\x01\x02"
# PNG 图像的 base64 编码
image_data = "data:image/png;base64,iVBORw0KG..."

# 文本文件下载按钮
if st.download_button(
    label="下载文本文件",
    data=text_data,
    file_name="example.txt",
    mime="text/plain"):
    st.success("文本文件已下载")

# 二进制文件下载按钮
if st.download_button(
    label="下载二进制文件",
    data=binary_data,
    file_name="example.bin",
    mime="application/octet-stream"):
    st.success("二进制文件已下载")

# 图像文件下载按钮
if st.download_button(
    label="下载图像文件",
    data=image_data,
    file_name="example.png",
    mime="image/png"):
    st.success("图像文件已下载")

相机输入

# st.camera_input() 添加一个摄像头输入组件,允许用户直接使用设备上的摄像头拍摄照片,并将其上传到应用中进行进一步处理,入参如下:
# image = st.camera_input(label="标签文本", key=None, help=None, on_change=None, args=None, kwargs=None, disabled=False)
# label {str} 必填,摄像头控件的标签,将显示在摄像头上方,用于标识摄像头的用途
# on_change {callable,None} 当摄像头输入发生变化时调用的回调函数
# args {tuple,None} 传递给 on_change 回调函数的位置参数
# kwargs {dict,None} 传递给 on_change 回调函数的关键字参数
# disabled {bool} 是否禁用摄像头输入,默认{False},表示不禁用
# help {str,None},可选,为摄像头输入控件提供帮助文本,将在用户悬停在组件上时显示
# key {str,None} 该摄像头输入控件的唯一标志
# 返回值 {PIL.Image} 用户通过摄像头拍摄的照片,如果没有照片被拍摄,则返回 None

# 扫描二维码并显示二维码信息
# pyzbar 依赖于 libzbar0,因此先使用下述代码安装 libzbar0
# sudo apt-get update
# sudo apt-get install libzbar0
from pyzbar.pyzbar import decode
from PIL import Image

# 创建摄像头输入组件
image = st.camera_input("二维码扫一扫")
# 检查是否有照片被拍摄
if image is not None:
    # 使用 PIL 打开图像
    img = Image.open(image)
    # 使用 pyzbar 解码二维码
    decoded_objects = decode(img)
    if decoded_objects:
        # 获取第一个解码的二维码信息
        decoded_data = decoded_objects[0].data.decode("utf-8")
        st.write(f"扫描到的二维码信息: {decoded_data}")
        # 显示解码后的二维码数据
        st.text_area("二维码内容", decoded_data, height=200)
    else:
        st.write("未找到二维码,请重新扫描")
    # 显示拍摄的照片
    st.image(img, caption='拍摄的照片', use_column_width=True)

颜色选择器

# st.color_picker() 允许用户使用颜色选择器选择颜色,并获取所选颜色的十六进制(hex)表示形式,入参如下:
# color = st.color_picker(label="颜色选择器", value="#00f900", key=None, help=None, on_change=None, args=None, kwargs=None, disabled=False)
# label {str} 必填,颜色选择器的标签,将显示在颜色选择器上方,用于标识颜色选择器的用途
# value {str} 初始颜色值,以十六进制形式表示。默认值{#00f900(一种绿色)}
# on_change {callable,None} 当颜色选择器发生变化时调用的回调函数
# args {tuple,None} 传递给 on_change 回调函数的位置参数
# kwargs {dict,None} 传递给 on_change 回调函数的关键字参数
# disabled {bool} 是否禁用颜色选择器,默认{False},表示不禁用
# help {str,None},可选,为颜色选择器提供帮助文本,将在用户悬停在组件上时显示
# key {str,None} 该颜色选择器的唯一标志
# 返回值 {str} 用户选择的颜色的十六进制表示形式,如果颜色选择器被禁用或没有选择颜色,则返回初始值 value

# 创建颜色选择器
selected_color = st.color_picker("选择颜色", value="#ff0000")
# 显示所选颜色
st.write(f"所选颜色: {selected_color}")
# 可以使用所选颜色做一些事情,例如绘制图形
st.markdown(f"<div style='background-color:{selected_color}; padding: 10px;'>这是所选颜色的背景</div>", unsafe_allow_html=True)

聊天界面

# st.chat_message() 插入一个聊天消息容器,用来模拟对话中的消息。可以包含各种元素,如文本、图表等
# st.chat_input() 创建一个聊天输入框,用于接收用户的输入,用户可以在其中输入消息

import numpy as np

# 创建用户的消息容器
with st.chat_message("user"):
    st.write("Hello 👋")
    st.line_chart(np.random.randn(30, 3))
# 显示一条系统消息
with st.chat_message("assistant"):
    st.write("您好 ~ 我是您的 AI 助手 ~ 请问有什么可以帮您?")
# 显示聊天输入框
user_input = st.chat_input("请输入...")
# 当用户按下 Enter 键时触发事件
if user_input:
    with st.chat_message("user"):
        st.write(f"你: {user_input}")
    with st.chat_message("assistant"):
        st.write("好的,我知道了!")

其他函数

特效动画

# 显示气球动画
st.balloons()

# 显示雪花动画
st.snow()

空白占位符

# element = st.empty() 创建一个空白的占位符元素,这个元素可以用来动态地替换其内容

import pandas as pd

# 创建 DataFrame
data = {
    'Day': ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
    'Sales': [150, 200, 180, 250, 300]
}
df = pd.DataFrame(data)

# 创建一个空白的占位符元素
element = st.empty()

# 一个按钮来控制替换
if st.button('切换文本/图表'):
    element.text_input('请输入...')
else:
    element.line_chart(df)

容器元素插入

# elements = st.container() 创建一个容器,并支持后续插入元素,即使这些元素在代码中出现的位置不是按照它们最终显示的顺序

import pandas as pd

# 创建 DataFrame
data = {
    'Day': ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
    'Sales': [150, 200, 180, 250, 300]
}
df = pd.DataFrame(data)

# 创建一个容器
elements = st.container()
# 在容器内添加一个线形图
elements.line_chart(df)
# 在容器外写入一段文本
st.write("Hello")
# 在容器内添加一个文本输入框
elements.text_input('请输入...')
# 在容器外再添加一些内容
st.write("容器外的文本")

# 运行后的显示顺序:
# 容器内的线形图
# 容器内的文本输入框 "请输入..."
# 容器外的文本 "Hello"
# 容器外的文本 "容器外的文本"
# 尽管文本输入框在代码中位于 st.write("Hello") 之后,但由于它们都在同一个容器内,因此文本输入框会显示在 "Hello" 文本之前
# 这种安排使得你可以灵活地组织元素,即使它们在代码中的顺序不同

代码显示

# st.echo() 是一个上下文管理器,用于在代码块执行的同时显示代码本身,即展示正在运行的代码

with st.echo():
    st.write('正在执行的代码将被打印显示')
    # 上述代码等同于如下操作:
    # 1. 执行 `st.write('正在执行的代码将被打印显示')`
    # 2. 打印出 `st.write('正在执行的代码将被打印显示')` 这行代码
  
    # 定义函数
    def example_function(a, b):
        return a + b
    # 调用函数并输出结果
    result = example_function(1, 2)
    st.write(f'结果是:{result}')

停止运行

# st.stop() 立即停止执行当前的 streamlit 脚本代码,如在处理表单提交后,一旦某个操作完成,就不需要继续执行后续的代码逻辑,可以避免不必要的计算或输出

禁用部件

# 设置 disabled=True 来禁用小部件(widgets)

# 检查是否应该禁用滑动条
disable_slider = st.checkbox('是否禁用滑动条')
# 根据条件禁用或启用滑动条
slider_val = st.slider('Pick a number', 0, 100, 50, disabled=disable_slider)
# 显示滑动条的值
st.write('已选值:', slider_val)

状态保存与跟踪

# st.session_state 是一个字典,用于存储在用户会话期间的数据状态,使得这些数据能够在页面刷新或在不同页面之间保持不变

# 示例:用户选择某选项,并且在每次选择后更新显示的内容
# 初始化 session_state
if 'selected_option' not in st.session_state:
    st.session_state.selected_option = '选项A'
    st.session_state.reload_required = False

# 创建选择框
option = st.selectbox('请选择:', ['选项A', '选项B', '选项C'])

# 当选择发生变化时,更新 session_state
if option != st.session_state.selected_option:
    st.session_state.selected_option = option
    st.session_state.reload_required = True

# 检查是否需要重新加载页面
if st.session_state.reload_required:
    # 重置 flag
    st.session_state.reload_required = False
    # 重新加载页面
    # 由于 Streamlit 自动重新加载页面,此处不需要额外操作
    pass

# 显示用户的选择
st.write(f"你的选择: {st.session_state.selected_option}")

数据缓存装饰器

# @st.cache_data 装饰器用于记忆函数的历史执行
# 当使用 @st.cache 装饰一个函数时,streamlit 会将该函数的结果存储在一个缓存中,以便在后续调用中直接返回缓存的结果,而不是重新计算。这可以提高应用程序的性能,特别是在处理大量数据或计算密集型任务时
# 入参如下:
# func 要缓存的函数。如果提供了函数,则会对该函数的计算结果进行缓存。如果为 None 则返回一个可以接受函数作为参数的装饰器
# ttl {int,None} 必填,缓存的生存时间,以秒为单位。在缓存的生存时间内,对函数的调用将返回缓存的结果而不是重新计算。如果设置为 None 则缓存将永不过期
# max_entries {int,None} 可选,缓存的最大条目数。当达到指定的最大条目数时,新的计算结果将替换最早的计算结果
# show_spinner {bool} 可选,当进行缓存计算时,是否显示加载指示器,默认{True}
# persist {bool} 可选,是否将缓存数据持久化到磁盘。如果为 True 则数据将在应用程序重新启动时仍然存在。默认{False}
# experimental_allow_widgets {bool} 可选,是否允许在被缓存的函数中使用 streamlit 小部件,默认{False}
# hash_funcs {dict,None} 可选,用于指定自定义哈希函数的字典。键是参数名称,值是哈希函数。如果为 None 则将使用默认哈希函数

import time

# 定义一个计算密集型函数,并使用 @st.cache_data 装饰器来缓存结果
@st.cache_data
def load_data(data_ref):
    # 模拟一个耗时的任务,例如从数据库加载数据或执行复杂的计算
    print("正在计算...")
    time.sleep(5)
    return {"data": 666}

# 第一次调用 load_data 函数,传入参数 'ref1'
# 函数执行,加载数据,并返回结果
data1 = load_data('ref1')
st.write(data1)

# 再次调用 load_data 函数,传入相同的参数 'ref1'
# 此时不会重新执行函数,而是从缓存中获取结果
# 因此 data1 和 data2 应该包含相同的数据
data2 = load_data('ref1')
st.write(data2)

# 使用不同的参数 'ref2' 调用 load_data 函数
# 函数会重新执行,因为缓存中没有这个参数对应的结果
data3 = load_data('ref2')
st.write(data3)

# 清除所有针对函数 load_data 的缓存
load_data.clear()

# 再次调用 load_data 函数,即使使用相同的参数 'ref1'
# 由于缓存已被清除,所以函数会重新执行
data4 = load_data('ref1')
st.write(data4)

# 清除所有被 @st.cache_data 装饰过的函数缓存
st.cache_data.clear()

# 再次调用 load_data 函数
# 即使使用相同的参数 'ref1',由于全局缓存已清除,函数会重新执行
data5 = load_data('ref1')
st.write(data5)

对象缓存装饰器

# @st.cache_resource 装饰器用于缓存非数据对象(如 TensorFlow 会话、数据库连接等),适用于那些创建成本较高但不需要频繁更新的对象

import time

# 定义一个创建会话或连接的函数,并使用 @st.cache_resource 装饰器来缓存结果
@st.cache_resource
def create_session(session_ref):
    # 创建并返回一个非数据对象,例如一个TensorFlow会话或数据库连接
    print("正在计算...")
    time.sleep(5)
    return {"data": 666}

# 第一次调用 create_session 函数,传入参数 'ref1'
# 函数执行,创建会话/连接,并返回结果
session1 = create_session('ref1')
st.write(session1)

# 再次调用 create_session 函数,传入相同的参数 'ref1'
# 此时不会重新执行函数,而是从缓存中获取结果
# 因此 session1 和 session2 引用的是同一个对象
session2 = create_session('ref1')
st.write(session2)

# 使用不同的参数 'ref2' 调用 create_session 函数
# 函数会重新执行,因为缓存中没有这个参数对应的结果
session3 = create_session('ref2')
st.write(session3)

# 清除所有针对函数 create_session 的缓存
create_session.clear()

# 再次调用 create_session 函数,即使使用相同的参数 'ref1'
# 由于缓存已被清除,所以函数会重新执行
session4 = create_session('ref1')
st.write(session4)

# 清除所有被 @st.cache_resource 装饰过的函数缓存
st.cache_resource.clear()

# 再次调用 create_session 函数
# 即使使用相同的参数 'ref1',由于全局缓存已清除,函数会重新执行
session5 = create_session('ref1')
st.write(session5)

进阶使用

多页面应用

  • 【目标】

    • 实现多应用展示:构建具有多个页面的Web应用,每个页面专注于不同的任务。

    • 子页面独立开发:确保每个子页面应用的代码逻辑清晰,独立于其他应用,便于开发和后续维护。

    • 算法与框架分离:将算法实现和框架结构分开存储,以促进代码的复用和扩展性。


  • 【方案】

    • 主页面

      • 提供页面路由:负责供用户选择要查看的子页面应用

      • 使用字典映射:键为子应用名称,值为子应用模块的引用

      • 保持文件简洁:仅负责页面路由和布局设置,具体功能实现在子应用中

    • 子页面

      • 每个子页面都是一个独立的子应用

      • 每个子应用使用 app() 函数封装子应用的逻辑流程和界面元素

      • 每个子应用创建独立的 py 文件,专注于某个特定的任务,与界面UI逻辑解耦


项目文件结构

my_project
├── main_app.py # 主页面
└── subpages # 子页面
    ├── __init__.py # 子页面配置文件
    ├── page_1.py # 子页面 1
    └── page_2.py # 子页面 2

主页面

方式一:Tab 标签页
# my_project/main_app.py

import streamlit as st
# 导入子页面
from subpages import page_1, page_2

# 使用字典来映射页面标题到页面函数
SUBPAGES = {
    "页面 1": page_1,
    "页面 2": page_2,
}

# 全局页面配置
st.set_page_config(
    page_title='主页面平台',
    layout="wide",
    initial_sidebar_state="collapsed")

# 设置页面标题
st.title("主页面平台")

def main():
    # 主页 Tab 页
    tabs = st.tabs(list(SUBPAGES.keys()))
    # 遍历 Tab 页并根据选择显示内容
    for i, tab in enumerate(tabs):
        with tab:
            # 获取当前 Tab 页的实际标题(基于原始列表)
            title = list(SUBPAGES.keys())[i]
            # 根据标题获取对应的页面函数,并调用
            page = SUBPAGES[title]
            page.app()

if __name__ == '__main__':
    main()

方式二:侧边栏
# my_project/main_app.py

import streamlit as st
# 导入子页面
from subpages import page_1, page_2

# 使用字典来映射页面标题到页面函数
SUBPAGES = {
    "页面 1": page_1,
    "页面 2": page_2,
}

# 全局页面配置
st.set_page_config(
    page_title='主页面平台',
    layout="wide",
    initial_sidebar_state="expanded")

# 设置页面标题
st.title("主页面平台")

def main():
    # 侧边栏导航
    st.sidebar.title('功能导航') 
    # 下拉框
    # selected_page = st.sidebar.selectbox("选择功能", list(SUBPAGES.keys()))
    # 单选框
    selected_page = st.sidebar.radio("", list(SUBPAGES.keys()))
    # 根据用户选择的页面调用相应的函数
    page = SUBPAGES[selected_page]  
    page.app()

if __name__ == '__main__':
    main()

子页面配置文件

# my_project/subpages/__init__.py

__author__ = "Faramita"
__version__ = "1.0"

子页面1

# my_project/subpages/page_1.py

import streamlit as st

def app():
    st.title('子页面 1')
    # 子页面 1 的具体内容
    st.header('这是 子页面 1 的内容')
    st.text('这是 子页面 1 的示例文本。')
    st.checkbox('这是一个复选框')

if __name__ == "__main__":
    app()

子页面2

# my_project/subpages/page_2.py

import streamlit as st

def app():
    st.title('子页面 2')
    # 子页面 2 的具体内容
    st.header('这是 子页面 2 的内容')
    st.text('这是一个示例文本,用于演示 子页面 2 的功能。')
    st.button('点击我!')   

if __name__ == "__main__":
    app()

3
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区