快速构建你的Web项目

简介

项目早期,不希望引入Vue、React等来增加复杂度,更不希望将大量时间花在CSS、JS实现页面布局和交互上,那如何快速构建一个Demo级用例呢?

你可以试试streamlit,经过我一段时间的使用,感觉是个不错的工具,嗯,开源的:https://github.com/streamlit/streamlit

而且,商业化感觉做的不错,社区比较活跃,可用组件比较丰富。

快速构建你的Web项目_第1张图片

本文先会介绍Streamlit的基础用法,然后会讨论如何实现登录、注册,并在最后提一下我眼中它的缺点。

run streamlit

streamlit自己的定位是给数据科学家使用的web原型开发工具,通过streamlit,你可以快速上线你的模型或各种数据分析报告。

这里,基于streamlit教程文档,了解streamlit的基础用法,其实这块,不是本文的重点,但为了文章完整性,还是补一下。

首先,你需要安装一下streamlit。

pip install streamlit

因为streamlit依赖比较多,建议你在虚拟环境中搞。

安装完后,可以先来个hello world,创建hello.py文件,代码如下:

import streamlit as st

st.title('Hello World!')

run起来:

streamlit run hello.py

效果如下:

快速构建你的Web项目_第2张图片

streamlit提供了很多组件,供你开发美观的页面,关于如何使用,streamlit的文档写的已经很好了,这里不费过多笔墨去讨论这个,大家看见文档就好了:https://docs.streamlit.io/library/get-started/create-an-app。

本文主要聊点文档中没有的。

streamlit如何启动的?

streamlit运行方式是使用streamlit run xxx.py的形式,阅读文档,发现,它也支持python -m streamlit run xxx.py 的形式。

感觉控制感不强,streamlit run命令是怎么运行的?

拉下streamlit源码,看其setup.py文件中的entry_points配置。

快速构建你的Web项目_第3张图片

由上图可知,streamlit命令的功能来自streamlit/web/cli.py的main()方法。

快速构建你的Web项目_第4张图片

嗯,使用click库来接收命令参数,提一嘴,我感觉click库接收命令行参数的方式比Python原生的argparse优雅多了。

简单阅读cli.py的代码,可以发现streamlit基于tornado开发,使用了tornado.httpserve作为Web后端。

快速构建你的Web项目_第5张图片

Tornado是Python中比较老的Web框架了,当时python的asyncio还不成熟,为了获得高性能的web框架,tornado基于Linux的epoll和BSD的kqueue实现了其高性能的异步io,其源码比较复杂难懂。

经过代码阅读,可以发现,streamlit会先载入index.html,然后再在index.html中嵌入你的页面逻辑(即使用streamlit提供组件创建的页面),具体的嵌入位置是id为root的div元素。

快速构建你的Web项目_第6张图片

这便是streamlit的基本流程。

消除streamlit标识

在我们实现hello.py并运行起来时,会发现多处都有streamlit的特征,如下图所示:

快速构建你的Web项目_第7张图片

从上图可知,streamlit提供了设置、重新加载、访问streamlit官网的操作,此外streamlit在title、footer上,都加上了自己的streamlit的标识。

当我们需要上线自己的web app时,当然不希望用户可以有那么多操作,也不希望竞争对手一眼看出我使用了streamlit。

解决方法便是隐藏掉这些内容,我们搜索【streamlit hiddle xxx】时,会搜到如下解决方案:https://discuss.streamlit.io/t/remove-made-with-streamlit-from-bottom-of-app/1370/2,相关代码如下:

hide_streamlit_style = """
            
            """
st.markdown(hide_streamlit_style, unsafe_allow_html=True)

原理很简单,通过st的markdown方法执行html代码,利用css来隐藏这些东西。

很遗憾,这效果并不好,streamlit在加载时,会优先加载自己的html和js,然后再载入你的逻辑,当网络比较差时,menu和footer会显示一段时间,再被你的css隐藏。

此外,streamlit在用户每次操作时,比如点击页面中的按钮,都会重新加载一次页面,依旧是老流程,优先加载自己的html、css、js,再加载你的,当比较卡时,用户每次操作,页面中都会出现menu和footer,这就很掩耳盗铃。

最好的方式,当然是直接修改streamlit源码,将不需要的部分,全部删除。

阅读streamlit源码,可知,streamlit的前端是React实现的,发布成python库时,React实现的代码都被webpack打包了,如果要修改源码,就需要修改React代码,然后自己搞一遍打包发布流程。

嗯,成本有点高,且自己改后,后面streamlit新功能,就很难兼容了,简单思索后,采用硬替换的方式来搞。

创建init_streamlit.py,写入如下代码:

import pathlib
import os
from bs4 import BeautifulSoup
from shutil import copyfile

from configs import ROOT_PATH


def modify_title_str(soup, title):
    """
    修改 index.html 的 title
    """
    soup.title.string = title
    return soup


def add_js_code(soup, js_code):
    """
    添加 js code 到 index.html 中
    """
    script_tag = soup.find(id='custom-js')
    if not script_tag:
        script_tag = soup.new_tag("script", id='custom-js')
    # custom-js script 中的 js code
    script_tag.string = js_code
    # 向 body 节点中添加内容
    soup.body.append(script_tag)
    return soup


def replace_favicon(streamlit_model_path):
    """替换streamlit的icon"""
    logo_path = os.path.join(streamlit_model_path, 'static', 'favicon.png')
    # 删除 logo
    pathlib.Path(logo_path).unlink()
    copyfile(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'favicon.png'), logo_path)


def init_streamlit(streamlit_model_path, title, footer):
    index_path = pathlib.Path(streamlit_model_path) / "static" / "index.html"

    soup = BeautifulSoup(index_path.read_text(encoding='utf-8'), features="lxml")

    soup = modify_title_str(soup, title)
    js_code = f'''
    document.querySelector("#MainMenu").style.visibility = 'hidden'
    document.querySelector('footer').innerHTML = '{footer}'
    '''
    soup = add_js_code(soup, js_code)
    index_path.write_text(str(soup), encoding='utf-8')


streamlit_model_path = os.path.join(ROOT_PATH, 'venv\\lib\\site-packages\\streamlit')
init_streamlit(streamlit_model_path=streamlit_model_path, title='懒编程', footer='Copyright © 2022, ayuliao Inc.')

上述代码主要就是替换streamlit中index.html的相关元素,比如title、footer之类的,通过直接修改index.html的方式,达到隐藏streamlit相关信息的效果,这样就不会因为streamlit先加载自身html、js而出现无法很好隐藏这些元素的问题了。

此外,单纯的修改index.html的title没有效果,原因是,index.html中的title后续也会被streamlit自身的js方法修改,要解决这个问题,可以修改一下hello.py文件,代码如下:

import init_streamlit
import streamlit as st

st.set_page_config(page_title='懒编程',
                   page_icon='logo.jpg')

st.title('hello world')

运行hello.py,效果如下图所示:

快速构建你的Web项目_第8张图片

实现登录、注册

如何实现登录、注册,也是文档里看不到的内容。

streamlit本身没有提供登录注册等功能,这可能跟streamlit自身定位有关,要实现登录与注册,我们需要自己写,通过streamlit插件的形式来实现。

streamlit有个插件页面,里面给出了比较优秀的streamlit插件,多数streamlit插件的前端都是自己利用React去实现,只是React中使用了streamlit提供的方法,从而达到实现streamlit插件的目的。

当然,一些简单的插件并不一定需要通过React开发页面交互,登录、注册类的插件便是如此。

经过查找,发现了Streamlit-Authenticator插件(https://github.com/mkhorasani/Streamlit-Authenticator),通过pip便可以安装使用:

pip install streamlit-authenticator

因为streamlit-authenticator提供的功能过于简单,它没有通过数据库来记录用户信息,在多数情况下,都不能满足我们,所以我们需要对它进行魔改。

要正常运行起streamlit-authenticator的源码,需要安装相关的依赖,但streamlit-authenticator并没有提供requirements.txt文件,其setup.py中却给出了依赖关系,你可以基于setup.py中的信息自己安装或者跟我一样使用偷懒方法,先安装streamlit-authenticator,然后再单独删除它,这样相关的依赖就安装好了。

我将streamlit-authenticator相关的代码放在libs文件夹中。

streamlit-authenticator原本是通过yaml配置文件来实现登录、注册的,我将其改成使用sqllite的形式,当然,你可以将其改成MySQL等等。此外,我添加了邀请码的逻辑,这里我写死了一些邀请码,只有拥有这些邀请码的用户才能注册,而注册后的用户,才能登陆。

为了配合修改后的streamlit-authenticator使用,我创建了models目录,在其中写相关的sql逻辑。

嗯,这块不复杂,但改动的逻辑比较多,就不通过文字描述了,翻到文末,看项目代码则可。

因streamlit的刷新机制(每操作页面中的一个按钮便会刷新页面),如何合理的组织登陆、注册和登陆后的页面也有坑。

如果你看streamlit官方文档中多页面app的内容,会发现布局很刻板,比较丑,经过简单实验与研究,我使用了tabs组件来实现最终布局,相关代码如下:

import os
import yaml
import init_streamlit
import streamlit as st
import libs.streamlit_authenticator as stauth

st.set_page_config(page_title='懒编程',
                   page_icon='logo.jpg')

st.title('hello world')


def init_authenticator():
    filepath = os.path.abspath(os.path.dirname(__file__))
    with open(os.path.join(filepath, 'auth.yaml')) as file:
        config = yaml.load(file, Loader=stauth.SafeLoader)

    authenticator = stauth.Authenticate(
        config['credentials'],
        config['cookie']['name'],
        config['cookie']['key'],
        config['cookie']['expiry_days'],
    )
    return authenticator


def register_user(authenticator):
    try:
        if authenticator.register_user('Register user', preauthorization=False):
            st.success('User registered successfully')
    except Exception as e:
        st.error(e)


def my_logics():
    st.markdown('login success')


def start_web():
    authenticator = init_authenticator()
    # check cookie not login again
    authenticator._check_cookie()
    if st.session_state["authentication_status"]:
        authenticator.logout('Logout', 'sidebar')
        my_logics()
    else:

        tab1, tab2 = st.tabs(["Login", "Register"])
        with tab1:
            name, authentication_status, username = authenticator.login(
                'Login', 'main')
            if st.session_state["authentication_status"] == False:
                st.error('Username/password is incorrect')
            elif st.session_state["authentication_status"] == None:
                st.warning('Please enter your username and password')
        with tab2:
            register_user(authenticator)

start_web()

登录页:

快速构建你的Web项目_第9张图片

注册页:

快速构建你的Web项目_第10张图片

登录成功后的主页:

快速构建你的Web项目_第11张图片

结尾

使用streamlit我们可以快速构建出可以拿出去给别人看的web demo,但streamlit在我眼中也有个比较大的缺陷,那便是没有区分请求的功能,比如Flask、Fastapi等框架,你可以区分出不同的请求,而streamlit不行,在多人使用时,就会出现,他人在操作页面时,你当前的页面也可能会被影响的情况。

嗯,这便是streamlit相关的实践了,本文相关代码github:https://github.com/ayuLiao/learn-streamlit

我是二两,下篇文章见。

你可能感兴趣的:(python,java,vue,javascript,js)