SHUHARI 的博客

流光飞舞

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 上可能会遇到一些兼容性方面的问题,因此最好是在从 LinuxMac 来运行。Windows 10 的用户建议使用虚拟机或 WSL

安装 Streamlit 最简单的方法是通过 PIP:

$ pip install streamlit

Streamlit 包自带了一个管理命令行。我们可以验证安装是否正常。在写作本文时,Streamlit 的版本是 0.56

$ streamlit --version
Streamlit, version 0.56.0

为了让你快速体验到运行的效果,Streamlit 内置了一个示例应用。我们可以通过以下命令运行它:

$ streamlit hello

然后会看到如下提示:

Streamlit Hello

服务器启动完毕后,Streamlit 也会自动在默认浏览器中打开地址 http://localhost:8501

我们通过这个界面了解一下 Streamlit 程序的基本布局。Streamlit 界面有一个比较固定的规则:

  • 左边是一个可折叠的区域,也就是 Sidebar(这是可选的);
  • 中间是主要内容的输出区域;
  • 右上角是 Streamlit 的系统菜单,包含一些常用的命令和选项。

我们可以从左边的下拉选择框选择不同的演示类型,并观察程序展示的效果。略有遗憾的是,部分示例使用了来自外部网站的数据源,因此在国内可能显示不太正常,如果想要看到完整效果的话,建议使用科学上网。

Streamlit Hello UI

基本示例

在体验过 Streamlit 的界面效果后,我们来自己编写和运行程序。Streamlit 程序就是普通的 Python 程序,因此我们只需要创建一个 .py 文件。Streamlit 对文件和目录布局没有特殊要求,因此按你自己的喜好命令即可。

一个最基本的示例程序代码如下:

import streamlit as st

"Hello World"

这里有几点需要说明。首先,Streamlit 使用了一个可以自动重载的 Web 服务器,只要你输入代码并保存,Streamlit 会自动侦测到这一变化,并向你提示:

Streamlit Query re-run

如果选择了 Always rerun,只要代码发生变更,Streamlit 创建的网页会自动刷新浏览器,无需你执行额外的动作,也不需要安装任何浏览器插件。这个机制使得用 Straemlit 开发网页界面成为一个非常高效且愉悦的过程。如果你不想再要这个行为的话,也可以从页面右上角的菜单选择 Settings,然后关闭 Run on Save 选项。

也因为如此,开发 Streamlit 时最好是把编辑器和浏览器并排摆放,这样就能随时看到网页刷新后的结果了:

Streamlit Side by Side

上述程序还展示了 Streamlit 的一个令人惊讶的特点:只要在单独的行上声明变量,而不是执行语句或调用函数的话,那么 Streamlit 会认为你要把它的内容显示在屏幕上。输出的内容不止可以是简单的文本,也可以是更复杂的内容或控件。

不过,我个人不太喜欢这种风格,因为这似乎背离了 Python “显式胜于隐式”的观念。从 Streamlit 的接口设计来看,它的开发者似乎认为简洁比明确更重要。如果你更喜欢明确表达自己意图的话,也可以使用略微正式一些的语法:

import streamlit as st

st.write("Hello World")

它和前面代码的效果是完全一样的。具体使用哪种方式完全是代码风格问题。

组件

为了让网页输出内容以及和用户交互,Streamlit 实现了大量的组件。我们把它们分成几组来学习,首先看最简单的:文本输出类组件。

文本组件

Streamlit 提供的文本组件比较简单,标题只有两级(HeaderSubHeader)。对于大段的内容,纯文本形式可使用 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 Text Widgets

说明: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 Alert

Streamlit 还能显示程序代码(不过目前只支持 Python):

st.code("""
class MyClass:
    def __init__(self):
        super(MyClass, self).__init__()
""")

Streamlit Code

以及一个漂亮的 JSON 数据格式化显示:

st.json({
    "pagination": {
        "total": 100,
        "per_page": 20,
        "page": 1,
    },
    "items": [
        {
            "id": 1,
            "name": "admin",
            "email": "admin@test.com",
            "is_superuser": True
        }
    ]
})

Streamlit Json

有时候我们可能希望同时展示代码和对应的显示效果。为此,Streamlit 提供了一个便利的方法:echo()

with st.echo():
    if st.button('Click Me'):
        st.text('Button clicked!')

Streamlit Echo

数据表格

Streamlit 支持两种表格形式:可重新排序的动态样式表格,以及静态的纯 HTML 表格。它们分别用 dataframe()table() 方法创建。它们的数据源通常是 Pandas DataFrame,也可以是 PythonNumpy 多维数组。

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 Data Frame and Static Table

图表与地图

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)

显示效果:

Streamlit Matplotlib line chart

我们也可以用其他图表库,比如 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)

显示结果:

Streamlit Altair scatter chart

也可以显示地图数据,需要的参数是经纬度坐标和一系列要显示的点:

df = pd.DataFrame(
    np.random.randn(100, 2) / [50, 50] + [22.56, 114.03],
    columns=['lat', 'lon'])
st.map(df)

Streamlit Map

从显示结果可以看到,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 Slider UI

再用一个例子说明 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 标志,表示该按钮有没有被点击。虽然用法很简单,但有点不太符合直觉,所以需要注意写法。

当页面首次显示的时候只有一个按钮,点击它,提示信息就会出来了(但页面并没有整体刷新):

Streamlit Button

检查框(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}")

Streamlit Radio Select Box

类似的,它们返回的值也都是一个字符串,表示选中项。

边栏

复杂的界面通常需要某种形式的的边栏(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)

Streamlit Sidebar

以上程序在边栏中创建了一个 selectbox,并根据其选中项来动态切换主要区域所显示的内容。你可以自己运行这个程序来体验一下。上述代码同时也是在 Streamlit 中实现界面导航的一种常见的模式。

配置

Streamlit 有很多配置项可以用来控制它在运行时的行为,它们大都是有默认值的,因此我们在最初的开发阶段可以不用关心,不过在生产环境有些可能就要调整了。比如在运行程序的时候我们注意到了,默认的服务端口是 8501,这个端口是否可以变化?当然可以通过配置来修改。

为了搞清楚有哪些配置可用,我们可以从命令行运行:

streamlit config show

这样会显示大量配置:

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 Mapbox 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 Cache

这说明该方法只有第一次会执行,以后就会直接从缓存中获取,而不会再重新计算。

在幕后,Streamlit 做了很多工作。它会考虑以下因素:

  • 传入的参数
  • 函数引用到的其他变量
  • 函数方法体
  • 函数所引用的其他函数

等等。假如所有内容都没有改变,则直接返回上次的结果,否则函数将重新执行。

尽管缓存可以有效提高性能,但在某些场景下可能会出现出乎意料的结果。建议将耗时的方法设计为纯函数(也就是说,对相同的参数总是返回相同的结果),以最大程度避免 Bug 的出现。

部署

一旦我们用 Streamlit 设计了一个漂亮的页面,当然希望把它发布在网上让更多人看到。尽管 Streamlit 提供了 Web 服务器,但它在性能和安全性方面的等级并不高,出于安全考虑,并不适合直接发布到公网上,而是应该用 ApacheNginx 作为反向代理放在 Streamlit 的前面。

我们已经知道,Streamlit 的默认端口是 8051。你也可以用 --server-port 或者对应的环境变量来修改该端口。

当你在服务器上把 Streamlit 启动起来,接下来要做的就是修改 ApacheNginx 的配置,让它去代理你的程序。一个典型的 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 下来自己研究它的实现。

评论
评论须知:
  1. 本博客只接受纯文本形式的评论;
  2. 评论须经管理员审核通过后才会显示;
  3. 若评论内容涉及灌水、广告、不文明用语及违反法律规定的内容,管理员有权直接删除,而无需通知您;
  4. 如果您对评论处理有异议,请联系站长。