Streamlit 入门介绍
前言
Streamlit
是一个 Python Web 应用框架。但和常规 Web 框架,如 Flask/Django
的不同之处在于,它不需要你去编写任何客户端代码(HTML/CSS/JS),只需要编写普通的 Python
模块,就可以在很短的时间内创建美观并具备高度交互性的界面,从而快速生成数据分析或者机器学习的结果;另一方面,和那些只能通过拖拽生成的工具也不同的是,你仍然具有对代码的完整控制权。
也许最令人惊讶的是,我们可以完全抛弃复杂的回调/异步代码,以及服务器/客户端之间繁琐的通信细节,而创建的程序仍然是实时和动态的,这令人耳目一新。尽管 Streamlit
也有它自己的限制,但是在了解它以后,相信你会感叹“原来还有这种操作”(哪怕你并不关心数据分析或机器学习)。
通过本文,我们会了解到 Streamlit
的使用方法,包括各种构建块(Building Block)和编写方法,并初步了解它的运行机制,以及缓存、配置和部署等相关话题。
安装与启动
Streamlit
是一个 Python
软件包,它同时支持 Python
2/3。当然,在现在这个时间,相信绝大多数人已经没有理由再去用 Py2 了。在平台支持方面,Streamlit
支持 Windows/Mac/Linux
三大操作系系统,但它主要是在 Posix
系统下开发的,在 Windows
上可能会遇到一些兼容性方面的问题,因此最好是在从 Linux
或 Mac
来运行。Windows 10
的用户建议使用虚拟机或 WSL
。
安装 Streamlit
最简单的方法是通过 PIP
:
$ pip install streamlit
Streamlit
包自带了一个管理命令行。我们可以验证安装是否正常。在写作本文时,Streamlit 的版本是 0.56
:
$ streamlit --version
Streamlit, version 0.56.0
为了让你快速体验到运行的效果,Streamlit
内置了一个示例应用。我们可以通过以下命令运行它:
$ streamlit hello
然后会看到如下提示:
服务器启动完毕后,Streamlit
也会自动在默认浏览器中打开地址 http://localhost:8501
。
我们通过这个界面了解一下 Streamlit
程序的基本布局。Streamlit
界面有一个比较固定的规则:
- 左边是一个可折叠的区域,也就是 Sidebar(这是可选的);
- 中间是主要内容的输出区域;
- 右上角是
Streamlit
的系统菜单,包含一些常用的命令和选项。
我们可以从左边的下拉选择框选择不同的演示类型,并观察程序展示的效果。略有遗憾的是,部分示例使用了来自外部网站的数据源,因此在国内可能显示不太正常,如果想要看到完整效果的话,建议使用科学上网。
基本示例
在体验过 Streamlit
的界面效果后,我们来自己编写和运行程序。Streamlit
程序就是普通的 Python
程序,因此我们只需要创建一个 .py
文件。Streamlit
对文件和目录布局没有特殊要求,因此按你自己的喜好命令即可。
一个最基本的示例程序代码如下:
import streamlit as st
"Hello World"
这里有几点需要说明。首先,Streamlit
使用了一个可以自动重载的 Web 服务器,只要你输入代码并保存,Streamlit 会自动侦测到这一变化,并向你提示:
如果选择了 Always rerun
,只要代码发生变更,Streamlit
创建的网页会自动刷新浏览器,无需你执行额外的动作,也不需要安装任何浏览器插件。这个机制使得用 Straemlit
开发网页界面成为一个非常高效且愉悦的过程。如果你不想再要这个行为的话,也可以从页面右上角的菜单选择 Settings,然后关闭 Run on Save
选项。
也因为如此,开发 Streamlit
时最好是把编辑器和浏览器并排摆放,这样就能随时看到网页刷新后的结果了:
上述程序还展示了 Streamlit
的一个令人惊讶的特点:只要在单独的行上声明变量,而不是执行语句或调用函数的话,那么 Streamlit
会认为你要把它的内容显示在屏幕上。输出的内容不止可以是简单的文本,也可以是更复杂的内容或控件。
不过,我个人不太喜欢这种风格,因为这似乎背离了 Python “显式胜于隐式”的观念。从 Streamlit
的接口设计来看,它的开发者似乎认为简洁比明确更重要。如果你更喜欢明确表达自己意图的话,也可以使用略微正式一些的语法:
import streamlit as st
st.write("Hello World")
它和前面代码的效果是完全一样的。具体使用哪种方式完全是代码风格问题。
组件
为了让网页输出内容以及和用户交互,Streamlit
实现了大量的组件。我们把它们分成几组来学习,首先看最简单的:文本输出类组件。
文本组件
Streamlit
提供的文本组件比较简单,标题只有两级(Header
和 SubHeader
)。对于大段的内容,纯文本形式可使用 text
,复杂格式可使用 Markdown
:
import streamlit as st
st.header("My Website")
st.subheader("Data Analytics")
st.text("Simple text")
st.markdown("""
- chapter 01
- chapter 02
- chapter 03
Emojis: :+1: :sunglasses:
From: [Github](https://github.com)""")
说明:Streamlit
所使用的 Markdown
分析器使用 Github Flavored Markdown
方言。从图中可以看到,我们可以添加 Emoji 表情来创建一些比较有趣的效果。
Streamlit
也能够生成带风格样式的提示信息,例如:
import streamlit as st
import numpy as np
import pandas as pd
import altair as alt
import time
from datetime import datetime
st.success('Success!')
st.warning('Warning!')
st.error('Error!')
结果:
Streamlit
还能显示程序代码(不过目前只支持 Python
):
st.code("""
class MyClass:
def __init__(self):
super(MyClass, self).__init__()
""")
以及一个漂亮的 JSON
数据格式化显示:
st.json({
"pagination": {
"total": 100,
"per_page": 20,
"page": 1,
},
"items": [
{
"id": 1,
"name": "admin",
"email": "admin@test.com",
"is_superuser": True
}
]
})
有时候我们可能希望同时展示代码和对应的显示效果。为此,Streamlit
提供了一个便利的方法:echo()
。
with st.echo():
if st.button('Click Me'):
st.text('Button clicked!')
数据表格
Streamlit
支持两种表格形式:可重新排序的动态样式表格,以及静态的纯 HTML 表格。它们分别用 dataframe()
和 table()
方法创建。它们的数据源通常是 Pandas DataFrame
,也可以是 Python
或 Numpy
多维数组。
import streamlit as st
import numpy as np
import pandas as pd
df = pd.DataFrame(
np.random.randn(10, 20),
columns=('列 %d' % (i+1) for i in range(20)))
st.subheader('Data Frame')
st.dataframe(df)
st.subheader('Static Table')
st.table(df)
如果我们愿意的话,也可以在 DataFrame 上添加一些简单的样式规则,比如高亮显示突出的数据。但部分比较复杂的样式目前是不支持的。
st.dataframe(df.style.highlight_max(axis=0))
图表与地图
Streamlit
支持多种图表库和图表类型。具体支持哪些图表取决于底层所使用的图表库,Streamlit
只是对它们进行了轻量级的包装。如果不明确指定的话,默认接口使用 Matplotlib
:
import streamlit as st
import numpy as np
import pandas as pd
chart_data = pd.DataFrame(
np.random.randn(20, 3),
columns=['Serie 1', 'Seria 2', 'Seria 3'])
st.line_chart(chart_data)
显示效果:
我们也可以用其他图表库,比如 Altair (一个基于 Vega-Lite 的图表库)来显示散点图:
import streamlit as st
import numpy as np
import pandas as pd
import altair as alt
df = pd.DataFrame(np.random.randn(200, 3),
columns=['a', 'b', 'c'])
c = alt.Chart(df).mark_circle().encode(
x='a', y='b', size='c', color='c')
st.altair_chart(c)
显示结果:
也可以显示地图数据,需要的参数是经纬度坐标和一系列要显示的点:
df = pd.DataFrame(
np.random.randn(100, 2) / [50, 50] + [22.56, 114.03],
columns=['lat', 'lon'])
st.map(df)
从显示结果可以看到,Streamlit
使用的是 Mapbox OpenStreetMap
提供的地图数据。在生产环境下,最好申请一个 专用的 Token
。这又涉及到一个话题:配置。我们会在后面讨论 Streamlit
的配置。
更多关于图表的示例可以参考 官方文档。
交互组件
接下来我们看看能够与用户交互的组件,也是 Streamlit
体系中比较有趣和复杂的部分。
以下示例来自 Streamlit 官方文档,也是对于 Streamlit 威力的一个很好的说明:
import streamlit as st
x = st.slider('Select a value between 0-100')
st.write(x, 'squared is', x * x)
以上代码创建了一个 Slider 组件。初看起来似乎没什么,但神奇的是,当你在浏览器中拖动 Slider 滑动条的时候,你会看到下面的显示值也跟着同步变化了。而我们并未编写任何客户端代码,也不需要回调或通知函数。这个小例子很好地体现了 Streamlit
的威力。
再用一个例子说明 Streamlit
是如何可以大大简化复杂的状态界面的。常规的 Web 应用中,在界面上显示进度是一个非常麻烦而琐碎的工作,需要自己定义和管理通知机制,往往需要编写很多 JavaScript 代码才能办到。而这在 Streamlit
中是非常简单的:
import streamlit as st
import time
value = 0
progress = st.progress(value)
label = st.empty()
for i in range(0, 100):
value += 1
progress.progress(value)
label.text(f"Running progress: {value}%")
time.sleep(0.2)
label.text("Running completed.")
这里我们用 time.sleep()
来模拟长时间运行的操作。运行程序,你会看到进度指示不断更新直到任务执行完毕。如果你写过基于 Ajax
或者其他基于服务器通知的进度指示程序的话,那么你应该会惊讶于 Streamlit
的实现竟然可以如此简单。
上述代码同时也显示了一个一个有用的辅助方法:empty()
。该方法可以给需要以后修改的文本留出一个空白位置,在代码的其他任何位置可以根据程序逻辑动态更新此文本。
如果进度不容易估算的话,我们也可以回退到简单的提示风格:
import streamlit as st
label = st.empty()
label.warning('Running in progress...')
time.sleep(2)
label.success('Running finished.')
运行程序,我们会看到短暂的进度提示,然后变成完成信息。
Streamlit
也包括常用的输入组件。最简单的组件是按钮,但是它的接口可能有点不太直观:
import streamlit as st
if st.button('Click me'):
st.text('Button clicked!')
st.button() 生成了一个按钮实例,但它返回的不是某种 Button 对象,而是一个 bool 标志,表示该按钮有没有被点击。虽然用法很简单,但有点不太符合直觉,所以需要注意写法。
当页面首次显示的时候只有一个按钮,点击它,提示信息就会出来了(但页面并没有整体刷新):
检查框(Checkbox
)的用法和 Button 几乎是一样的,这里就不再重复了。
接下来是两个略微复杂、带有选项的组件,它们的接口也非常相似,只是外观差别较大,因此这里一并展示:
import streamlit as st
genre = st.radio(
"What's your favorite movie genre",
('Comedy', 'Drama', 'Documentary'))
st.write(f"You selected {genre}")
option = st.selectbox(
'How would you like to be contacted?',
('Email', 'Home phone', 'Mobile phone'))
st.write(f"You selected {option}")
类似的,它们返回的值也都是一个字符串,表示选中项。
边栏
复杂的界面通常需要某种形式的的边栏(Sidebar),以放置一些通用的类似导航和过滤功能的内容。以上介绍的组件基本上都可以在边栏中调用,只需要在前面加上 sidebar
的前缀:
import streamlit as st
import numpy as np
import pandas as pd
import altair as alt
chart_type = st.sidebar.selectbox(
'Which chart are you perfer?',
('Matplotlib', 'Altair')
)
chart_data = pd.DataFrame(
np.random.randn(20, 3),
columns=['a', 'b', 'c'])
if chart_type == 'Matplotlib':
st.line_chart(chart_data)
elif chart_type == 'Altair':
c = alt.Chart(chart_data).mark_circle().encode(
x='a', y='b', size='c', color='c')
st.altair_chart(c)
以上程序在边栏中创建了一个 selectbox
,并根据其选中项来动态切换主要区域所显示的内容。你可以自己运行这个程序来体验一下。上述代码同时也是在 Streamlit
中实现界面导航的一种常见的模式。
配置
Streamlit
有很多配置项可以用来控制它在运行时的行为,它们大都是有默认值的,因此我们在最初的开发阶段可以不用关心,不过在生产环境有些可能就要调整了。比如在运行程序的时候我们注意到了,默认的服务端口是 8501,这个端口是否可以变化?当然可以通过配置来修改。
为了搞清楚有哪些配置可用,我们可以从命令行运行:
streamlit config show
这样会显示大量配置:
所有配置项都带有注释,我们可以仔细阅读来理解它们的含义。而要修改配置的值则有多种不同的方法,我们来一一介绍。
方法一:通过全局配置
上述输出告诉我们,Streamlit 有一个全局的配置文件,位置是 ~/.streamlit/config.toml。不过这个文件默认情况下可能不存在,如果是这样,我们可以自己创建一个,然后添加自己想要修改的配置,比如:
[server]
port = 8000
说明:可能有些同学没有听说过 TOML
。 这是一种比较新、使用还不太广泛的配置格式,最初是由 Github 推广的。在简单的情况下,TOML
看起来比较类似于古老的 Windows INI 格式。你可以到 github.com/toml-lang/toml 了解详细的语法细节。
方法二:通过环境变量
修改全局配置文件的方法比较缺乏灵活性,如果多个项目需要不同的配置就不适用了,同时也不便于配置管理。另一种常见的配置手段是环境变量。我们可以把配置的名称大写,分段和名字中间用下划线分割,再加上 STREAMLIT 前缀,作为环境变量名称:
STREAMLIT_SERVER_PORT=8000 streamlit run app.py
方法三:通过命令行参数
第三种方法是通过命令行,其格式是在分段和名字中间用 .
分割,后面跟要赋的具体值:
streamlit run app.py --server.port 8000
相信大家最可能去修改的配置应该是 server.port
。另一个可能会用到的是 mapbox.token
。在前面我们曾说过,Streamlit
默认使用 Mapbox
提供的地图。为了让大家能够开箱即用,Streamlit
内置了一个开发者的 Token 供我们显示地图用,但正如注释所说,它的使用是有限制的。如果我们需要大量显示地图的话,最好是自己去申请一个 Token,并用上述三种方法的任意一种来配置它。
运行机制与缓存
以上我们介绍了 Streamlit 的使用。大家可能会好奇,它是如何做到在不编写客户代码、不用异步与回调的情况下,实现这样简洁方便的界面的?
这里是一个比较简化的回答:在幕后,Streamlit
会在服务端维护所有界面组件的状态。只要用户动作引起界面组件的值发生变化,服务器就会重新执行一遍代码,并且客户端组件的值会通过某种机制自动同步给服务端。
这个架构带来的显著好处是:程序员只需要专注编写 Python
代码,无需关心客户端代码,也不需要复杂的回调或异步设施,代码得以极大地简化,也减少了出错的可能。但相应的也付出了一定代价,即:重新执行的性能负担比较大,为每个用户维护状态也给服务器带来了沉重的负担。因此它并不适用于用户基数大、并发量高的互联网应用。但对于小范围内发布数据分析或机器学习结果来说,Streamlit
是相当好用的。此外,为了减少重新执行代码的成本,提高性能,Streamlit
也提供了一些提高性能的机制,比如缓存。
Streamlit
为提供缓存的机制是:将耗时但不需要频繁重新计算的操作封装为一个函数,并用 cache
装饰器来标识。我们看一个简单的示例:
import streamlit as st
from datetime import datetime
@st.cache
def calc(a, b):
time.sleep(2)
return a * b
start = datetime.now()
a, b = 10, 20
result = calc(a, b)
elapsed = (datetime.now() - start).total_seconds()
st.write(f"{a}*{b}={result}, Elapsed: {elapsed}")
刷新几次页面,我们会看到如下结果:
这说明该方法只有第一次会执行,以后就会直接从缓存中获取,而不会再重新计算。
在幕后,Streamlit
做了很多工作。它会考虑以下因素:
- 传入的参数
- 函数引用到的其他变量
- 函数方法体
- 函数所引用的其他函数
等等。假如所有内容都没有改变,则直接返回上次的结果,否则函数将重新执行。
尽管缓存可以有效提高性能,但在某些场景下可能会出现出乎意料的结果。建议将耗时的方法设计为纯函数(也就是说,对相同的参数总是返回相同的结果),以最大程度避免 Bug 的出现。
部署
一旦我们用 Streamlit
设计了一个漂亮的页面,当然希望把它发布在网上让更多人看到。尽管 Streamlit
提供了 Web 服务器,但它在性能和安全性方面的等级并不高,出于安全考虑,并不适合直接发布到公网上,而是应该用 Apache
或 Nginx
作为反向代理放在 Streamlit
的前面。
我们已经知道,Streamlit
的默认端口是 8051。你也可以用 --server-port 或者对应的环境变量来修改该端口。
当你在服务器上把 Streamlit
启动起来,接下来要做的就是修改 Apache
或 Nginx
的配置,让它去代理你的程序。一个典型的 Nginx
配置如下所示:
server {
listen 80;
listen [::]:80;
server_name mysite.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name mysite.com;
ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
location ~ /\.ht {
deny all;
}
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_pass http://127.0.0.1:8051;
}
}
当然,以上配置假定你已经用 Let's Encrypt
创建了免费的 HTTPS 证书。
缺点
尽管本文是向大家介绍 Streamlit
,但是并不需要为尊者讳:Streamlit
的设计方式决定了它是以运行效率来换取开发效率(以及开发体验),对网络带宽和稳定性的要求较高,所以并不太适合大规模、多用户的网站,而更加适用于个人或小型组织快速发布数据/内容,此外,如果默认组件不能满足要求的话,要扩展起来也比较困难。
可以想象的是,很多数据分析或机器学习的用户并不是经验丰富的前端开发者,因此要他们用复杂的现代 JavaScript 框架实现精美的客户端界面是比较困难的。Streamlit
正是瞄准了这一利基市场,很好的解决了他们的痛点。对于时间紧迫、效率胜于灵活性的场合,以及快速原型开发来说,Streamlit
也是一个不错的选择。
示例与资源
以下是学习 Streamlit
可能用到的资源和信息。
- 官网
- 文档
- Github 仓库
- 示例
官方提供的
Streamlit
演示案例。需要说明的是,由于该案例使用了外部数据资源,因此国内用户运行可能会遇到问题,请自行解决。 -
Awesome Streamlit Github 的 Awesome 资源信息集合
-
Awesome Streamlit 这是一个集合
Streamlit
资源的网站,同时它本身就是用Streamlit
编写的,因此是个不错的演示范例。感兴趣的同学也可以Fork
下来自己研究它的实现。