• 快速构建你的Web项目


    简介

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

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

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

    8c08e241ebdcf2051a53a344bbc497e0.png

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

    run streamlit

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

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

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

    pip install streamlit

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

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

    1. import streamlit as st
    2. st.title('Hello World!')

    run起来:

    streamlit run hello.py

    效果如下:

    09786d0bc5f710729a4114046acffabd.png

    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配置。

    538f11d1e83a3b1ac9f698b10419b3d7.png

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

    71b5f0470bb79e2b6b6e98a27d975201.png

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

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

    2cd45ca9372bdfff85f166150c80a45e.png

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

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

    6999cf8130fe089957f62ca5bd331dfb.png

    这便是streamlit的基本流程。

    消除streamlit标识

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

    b60866e0a851aeee16a08607b8aa077e.png

    从上图可知,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,相关代码如下:

    1. hide_streamlit_style = """
    2.             
    3.             """
    4. 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,写入如下代码:

    1. import pathlib
    2. import os
    3. from bs4 import BeautifulSoup
    4. from shutil import copyfile
    5. from configs import ROOT_PATH
    6. def modify_title_str(soup, title):
    7.     """
    8.     修改 index.html 的 title
    9.     """
    10.     soup.title.string = title
    11.     return soup
    12. def add_js_code(soup, js_code):
    13.     """
    14.     添加 js code 到 index.html 中
    15.     """
    16.     script_tag = soup.find(id='custom-js')
    17.     if not script_tag:
    18.         script_tag = soup.new_tag("script", id='custom-js')
    19.     # custom-js script 中的 js code
    20.     script_tag.string = js_code
    21.     # 向 body 节点中添加内容
    22.     soup.body.append(script_tag)
    23.     return soup
    24. def replace_favicon(streamlit_model_path):
    25.     """替换streamlit的icon"""
    26.     logo_path = os.path.join(streamlit_model_path, 'static''favicon.png')
    27.     # 删除 logo
    28.     pathlib.Path(logo_path).unlink()
    29.     copyfile(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'favicon.png'), logo_path)
    30. def init_streamlit(streamlit_model_path, title, footer):
    31.     index_path = pathlib.Path(streamlit_model_path) / "static" / "index.html"
    32.     soup = BeautifulSoup(index_path.read_text(encoding='utf-8'), features="lxml")
    33.     soup = modify_title_str(soup, title)
    34.     js_code = f'''
    35.     document.querySelector("#MainMenu").style.visibility = 'hidden'
    36.     document.querySelector('footer').innerHTML = '{footer}'
    37.     '''
    38.     soup = add_js_code(soup, js_code)
    39.     index_path.write_text(str(soup), encoding='utf-8')
    40. streamlit_model_path = os.path.join(ROOT_PATH, 'venv\\lib\\site-packages\\streamlit')
    41. 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文件,代码如下:

    1. import init_streamlit
    2. import streamlit as st
    3. st.set_page_config(page_title='懒编程',
    4.                    page_icon='logo.jpg')
    5. st.title('hello world')

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

    72ac2957d5ad087a61145384a5d28f10.png

    实现登录、注册

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

    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组件来实现最终布局,相关代码如下:

    1. import os
    2. import yaml
    3. import init_streamlit
    4. import streamlit as st
    5. import libs.streamlit_authenticator as stauth
    6. st.set_page_config(page_title='懒编程',
    7.                    page_icon='logo.jpg')
    8. st.title('hello world')
    9. def init_authenticator():
    10.     filepath = os.path.abspath(os.path.dirname(__file__))
    11.     with open(os.path.join(filepath, 'auth.yaml')) as file:
    12.         config = yaml.load(file, Loader=stauth.SafeLoader)
    13.     authenticator = stauth.Authenticate(
    14.         config['credentials'],
    15.         config['cookie']['name'],
    16.         config['cookie']['key'],
    17.         config['cookie']['expiry_days'],
    18.     )
    19.     return authenticator
    20. def register_user(authenticator):
    21.     try:
    22.         if authenticator.register_user('Register user', preauthorization=False):
    23.             st.success('User registered successfully')
    24.     except Exception as e:
    25.         st.error(e)
    26. def my_logics():
    27.     st.markdown('login success')
    28. def start_web():
    29.     authenticator = init_authenticator()
    30.     # check cookie not login again
    31.     authenticator._check_cookie()
    32.     if st.session_state["authentication_status"]:
    33.         authenticator.logout('Logout''sidebar')
    34.         my_logics()
    35.     else:
    36.         tab1, tab2 = st.tabs(["Login""Register"])
    37.         with tab1:
    38.             name, authentication_status, username = authenticator.login(
    39.                 'Login''main')
    40.             if st.session_state["authentication_status"] == False:
    41.                 st.error('Username/password is incorrect')
    42.             elif st.session_state["authentication_status"] == None:
    43.                 st.warning('Please enter your username and password')
    44.         with tab2:
    45.             register_user(authenticator)
    46. start_web()

    登录页:

    6b767f9a3b0f78cb1f1138677b2d11fd.png

    注册页:

    0d6120f9951bb9e8f3f3e684922977b9.png

    登录成功后的主页:

    13cc8c9a2c3ee49c250062db6c3113a9.png

    结尾

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

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

    我是二两,下篇文章见。

  • 相关阅读:
    常用的软件配置管理工具
    TikTok运营干货——养号篇
    Vue-引入ElementUI&&use底层原理
    基于Java+微信小程序实现《模拟考试平台》
    学长教你学C-day7-C语言指针
    Ubuntu 20.04 安装 flameshot
    路线中桩测量计算程序浅析
    【节能学院】剩余电流动作继电器在浴室中的应用
    Nuxt3第三篇【布局与组件】
    SpringBoot整合SpringSecurity
  • 原文地址:https://blog.csdn.net/weixin_30230009/article/details/126684850