Streamlit框架的定制化

Streamlit框架的定制化


最近做了一个关于streamlit框架的项目,颇有感触,所以在这里记录一下。

什么是streamlit?

Streamlit 是一个python的WEB UI库,它做了高度的封装以便于不懂后前端开发的人员也能轻松构建画面。你可以从官网进行详细的了解:https://docs.streamlit.io/library/api-reference

我的第一感受是,画面美观,很方便可以集成调用python的模块,不用像以前通过 ajax+web服务器的方式,省了很多麻烦。但是同时也会有一些问题,比如,定制化完成画面的布局等等,就会非常需要考验人的想象力了。

看完本文你能够了解到什么?

我将从实际遇到的问题入手,去探究如何去使用Streamlit 完成一些定制化的需求。

  • Streamlit 的执行原理和流程
  • Streamlit 如何自定义CSS
  • Streamlit 如何嵌入执行js
  • Streamlit 静态资源
  • Streamlit 布局问题
  • Streamlit 录音组件
  • Streamlit 事件如何处理
Streamlit 的执行原理和流程

用我不太专业的说法来讲就是,不论画面做了任何动作,streamlit都会从头到尾执行一次。
是的,这就代表当画面交互变得复杂,你需要很强的逻辑能力才能够驾驭,不然你可能会遇到各种问题。

Streamlit 如何自定义CSS

这部分其实官方有提到,参考以下代码即可实现:

global_css = open("bfs/global.css", encoding="utf-8").read()
st.markdown(f"""<style>{global_css}</style>""", unsafe_allow_html=True)

建议紧跟在 st.set_page_config 语句之后,因为代码总是自上而下执行,可以确保后续布局渲染时可以立即应用上css, 不然可能会出现 先出现布局后,位置再发生变化的情况。

另外,顺带分享一个两个选择器——索引选择器(官方可能不这么说)和属性选择器。

Section[data-testid='stSidebar'] div[data-testid='stVerticalBlock'] > div:nth-of-type(1){
	position: absolute;
	top: -80px;
	width: 130px;
	height: 35px
}

Section[data-testid='stSidebar'] 用于选中session元素属性 data-testid=‘stSidebar’ 的元素,div:nth-of-type(n) 用于选中 第 n 个div元素,注意这里的 n 从 1 开始。

这两个选择器基本上能够满足 streamlit 的布局需求。

如何嵌入执行js

官方也提到过,请参考以下代码:

global_js = open("bfs/global.js", encoding="utf-8").read()
st.components.v1.html(f"""<script >{global_js}</script>""", height=0, width=0)

这部分需要特别注意,使用这种方式实际上会在画面中 追加一个iframe 去执行我们的js代码。
但是 iframe 是完全独立的环境,这会导致你js获取dom元素会找不到,所以需要使用以下的代码跳出 iframe

let _document = window.parent.document;

在文件开头加入这句话,然后后续就可以使用 _document 愉快的使用js操作画面啦。

Streamlit 静态资源

这部分可以在自动 streamlit app时加入 --server.enableStaticServing=true 参数。

streamlit run frontend/app.py --browser.gatherUsageStats=False --server.enableStaticServing=true

它会将相对位置的 static 目录挂在为静态资源目录。这里的目录会指定为 frontend/static
请确保该文件夹存在。

项目启动成功以后,你就可以通过 http://xxx.xxx.xxx.xxx:xxxx/app/static/ 去访问该静态资源了。

这为我们布局提供了一些便利性,为什么这么说?
当我们需要向画面中添加一些图片的icon时,我个人在streamlit 有种解决方案:

1、通过 streamlit 的图片组件将图片放到画面上,然后通过 css 去调整布局。

# 全局css
global_css = open("bfs/global.css", encoding="utf-8").read()
st.markdown(f"""<style>{global_css}</style>""", unsafe_allow_html=True)

# 添加图片
st.image(Image.open("static/img/record.png"))

这种方式经实践是可行的,但是存在一些问题,画面加载时会有先展示原布局,然后才会被应用上css,另外st.image 生成的布局嵌套比较深,对于css定位不说麻烦,但是相对比较繁琐。最后偶尔会出现css应用不上去的情况,原因不明。

2、开启静态资源服务,由js完成图片icon的添加(推荐)
相对于第一种方式,我们需要开启静态资源服务,也就是本章节开始的部分。
然后后你可以定位到你需要的元素,去追加 img 元素,同时 可以指定 class,用于定位。
例:

let is_assistant = st_msg.querySelector("div[data-testid='stChatMessageContent']");
if (is_assistant) {
  if (is_assistant.getAttribute("aria-label").indexOf("from assistant") !== -1) {
   let img_ele = _document.createElement("img");
   img_ele.src = src;
   img_ele.width = 35;
   img_ele.height = 35;
   img_ele.classList.add("copy");
   img_ele.addEventListener("click", function() {
     img_ele.classList.add("hidden");
     const textarea = _document.createElement('textarea');
     _document.body.appendChild(textarea);
     textarea.textContent = img_ele.parentElement.innerText;
     textarea.select();
     if (_document.execCommand('copy')) {
       _document.execCommand('copy');
       // alert("copy success!")
       _document.body.removeChild(textarea);
       display_copied(img_ele);
     }
   });
   st_msg.append(img_ele);

上述代码是定位到 div[data-testid='stChatMessageContent'] 元素并追加图片的示例,同时设定了class并且添加了点击事件。

这种方式经实践比较稳定,已投入使用,未出现方式一中提到的问题。敬请参考。

Streamlit 布局问题

如何用Streamlit 实现我们想要的布局是一个问题,官方有一些组件可以使用。
https://docs.streamlit.io/library/api-reference/layout

其中 st.sidebar、st.columns、st.container 这三个基本上就可以完成我们需要的布局。

需要注意的点是,默认使用 st.* 是会加到整体的画面上的,我们以使用 with 关键字,这种情况下,会被加到 with 所在的容器当中。
例:

    with st.sidebar:
        # 2023/11/20 張 Add Start
        st.image(Image.open(f"{static_dir}/img/logo.jpg"))

上述代码中的 logo.jpg 就会被加到 侧边栏当中。

或者 在使用 container 等容器时,你可以先实例化一个 container,然后添加到容器。
例:

msg_container = st.container()
msg_container.image(Image.open(f"{static_dir}/img/logo.jpg"))

这样 logo.jpg 就会被加到 msg_container 中。
建议将需要的组件都分类装到各自的container中,这样会省不少事儿。

Streamlit 录音组件

这个部分,也有一些坑。这里就推荐一下吧~

1、 audio-recorder-streamlit
https://pypi.org/project/audio-recorder-streamlit/

示例代码:

import streamlit as st

from audio_recorder_streamlit import audio_recorder

audio_bytes = audio_recorder()
if audio_bytes:
    st.audio(audio_bytes, format="audio/wav")

效果:
在这里插入图片描述

2、streamlit-audiorecorder
https://pypi.org/project/streamlit-audiorecorder/

示例代码:

import streamlit as st
from audiorecorder import audiorecorder

st.title("Audio Recorder")
audio = audiorecorder("Click to record", "Click to stop recording")

if len(audio) > 0:
    # To play audio in frontend:
    st.audio(audio.export().read())  

效果:
在这里插入图片描述

3、streamlit-audiorec
https://pypi.org/project/streamlit-audiorec/

示例代码:

import streamlit as st
from st_audiorec import st_audiorec
wav_audio_data = st_audiorec()

if wav_audio_data is not None:
    st.audio(wav_audio_data, format='audio/wav')

效果:
在这里插入图片描述
这种方式虽然很炫酷~ 但是经测试,整合到项目之后非常慢,不知道原因。
目前笔者使用的是 第二种方式。

Streamlit 事件如何处理

这么说吧~ 我觉得Streamlit 没有所谓的事件处理,它所有的动作都会触发整个apps重新加载。
由于这一机制,你只能保存状态,通过flag去做处理。

例:

import streamlit as st
from audio_recorder_streamlit import audio_recorder


audio_bytes = audio_recorder()
if audio_bytes:
    text = "识别后的文字"
    st.write(text)

st.button("TEST")

上述是一段代码,模拟用户语音输入后识别文字,然后显示在文本框中。
在这里插入图片描述
然而,当我清空文本框之后,它又会出现。

细心地小伙伴会发现是 if audio_bytes: 这个判断条件的问题,清空文本框后 录音内容并未被清空,所以当它被重新加载时又会出现。

清空不就好了?很遗憾,目前我无法做到,当然有经验的小伙伴可以共有一下。

这里采用迂回使用flag配合js的方式来实现。

在这里插入图片描述
如上图所示,追加了一个录音按钮和录音图标,由 录音图标 去控制开始结束录音,完成录音时同时点击录音按钮 完成flag的设定。
当然录音图标的 追加与点击逻辑 由js实现,这部分我就不细说了。

最后判断 需要的识别条件就可以设置成该flag。
示例代码:

import streamlit as st
from audio_recorder_streamlit import audio_recorder

if "record_flag" not in st.session_state:
    st.session_state["record_flag"] = False

if "text" not in st.session_state:
    st.session_state["text"] = ""

audio_bytes = audio_recorder()
if st.session_state["record_flag"]:
    st.session_state["text"] = "识别后的文字"
    st.session_state["record_flag"] = False


st.text_area(label="", key="text")

def record():
    st.session_state["record_flag"] = True

st.button("Record", on_click=record)

用这种思路可以实现相对于比较复杂的逻辑。

另外,由于 streamlit 的刷新机制,某些情况下是部分刷新,这部分,我没太明白,导致某些情况下达不到我们预期的效果;比如笔者遇到的问题,需要给 chatgpt 回答的内容添加点击事件,但是用户发消息时并未触发 js的运行,导致事件没绑定上。

这种情况,可以使用js的 观察者对象,监听Dom元素的变化。

示例代码:

const msg_observer = new MutationObserver((mutationsList, observer) => {
  add_copy_icon();
});
// 配置观察选项
const msg_config = { attributes: false, childList: true, subtree: true };
let msg_container = _document.querySelector('section.main .block-container');
msg_observer.observe(msg_container, msg_config);

这样做了之后,一旦 msg_container 元素发生变化就会执行 add_copy_icon 方法,以刷新监听事件。

以上就是本次关于 Streamlit 的使用心得,基本上能够应对 Streamlit 各种复杂的需求 ,当然你可以选择使用 Streamlit 的自定义组件去解决该问题,不在本次的讨论范畴。

欢迎大家留言探讨!

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
爬虫(Web Crawler)是一种自动程序,用于从互联网上收集信息。其主要功能是访问网页、提取数据并存储,以便后续分析或展示。爬虫通常由搜索引擎、数据挖掘工具、监测系统等应用于网络数据抓取的场景。 爬虫的工作流程包括以下几个关键步骤: URL收集: 爬虫从一个或多个初始URL开始,递归或迭代地发现新的URL,构建一个URL队列。这些URL可以通过链接分析、站点地图、搜索引擎等方式获取。 请求网页: 爬虫使用HTTP或其他协议向目标URL发起请求,获取网页的HTML内容。这通常通过HTTP请求库实现,如Python中的Requests库。 解析内容: 爬虫对获取的HTML进行解析,提取有用的信息。常用的解析工具有正则表达式、XPath、Beautiful Soup等。这些工具帮助爬虫定位和提取目标数据,如文本、图片、链接等。 数据存储: 爬虫将提取的数据存储到数据库、文件或其他存储介质中,以备后续分析或展示。常用的存储形式包括关系型数据库、NoSQL数据库、JSON文件等。 遵守规则: 为避免对网站造成过大负担或触发反爬虫机制,爬虫需要遵守网站的robots.txt协议,限制访问频率和深度,并模拟人类访问行为,如设置User-Agent。 反爬虫应对: 由于爬虫的存在,一些网站采取了反爬虫措施,如验证码、IP封锁等。爬虫工程师需要设计相应的策略来应对这些挑战。 爬虫在各个领域都有广泛的应用,包括搜索引擎索引、数据挖掘、价格监测、新闻聚合等。然而,使用爬虫需要遵守法律和伦理规范,尊重网站的使用政策,并确保对被访问网站的服务器负责。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Anesthesia丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值