- 数据分析
- 数据可视化
- 后端
- 前端
【免费下载链接】dtale
Visualizer for pandas data structures
导读
本文讲解如何在 Streamlit 应用中嵌入 D-Tale(乃至任意 Flask 应用)的完整方案。你将掌握一条由“覆写 Streamlit Tornado 服务器的start_listening函数 +WSGIContainer/FallbackHandler桥接”构成的成熟技术路线,并学会使用 D-Tale 官方提供的dtale-streamlit命令直接运行脚本、通过startup()/get_instance()在 Streamlit 与 D-Tale 之间传递 DataFrame,以及用<iframe>把 D-Tale 网格、图表和代码导出面板渲染进 Streamlit 页面。文中所有原理均可对照本仓库的 dtale/cli/streamlit_script.py、setup.py 与 tests/dtale/test_cli.py 逐一验证。
一、背景:Streamlit 的 Web 服务器为什么需要“黑客手段”
Streamlit 是一个用 Python 快速搭建网页应用的工具,它底层使用的是 Tornado 的tornado.httpserver.HTTPServer。而 D-Tale 本身是一个基于 Flask 的应用(核心构建逻辑见 dtale/app.py 中的build_app())。如果两个应用各自监听一个端口,就需要用户在两个页面之间来回切换,体验割裂;理想的方式是让 D-Tale 的 Flask 路由“寄生”在 Streamlit 同一个 Tornado 服务器上,共用端口与域名。
在 docs/EMBEDDED_DJANGO.md 中我们已经完成了 D-Tale 嵌入 Django 的实践,这套经验完全可以迁移到 Streamlit:关键在于找到 Streamlit 服务器构建路由的地方,再把 Flask 应用挂进去。
1.1 突破口:streamlit.server.Server._create_app()
通过阅读 Streamlit 源码可以发现,streamlit.server.Server中存在一个_create_app()函数,它负责为每个 Streamlit 服务器构建不同的路由;这些路由最终被组装进tornado.web.Application。既然 Streamlit 的底层就是 Tornado 的Application,那么问题就转化为:
如何把一个 Flask 应用并入 Tornado 的
tornado.web.Application路由表中?
1.2 桥接方案:WSGIContainer+FallbackHandler
Tornado 官方提供了一条桥接路径:用tornado.wsgi.WSGIContainer把任意 WSGI 应用(Flask 应用天然满足 WSGI 规范)包装成 Tornado 可以调用的对象,再用tornado.web.FallbackHandler作为“兜底处理器”把请求转交给它。一个经典的示例骨架如下:
from tornado.wsgi import WSGIContainer from tornado.ioloop import IOLoop from tornado.web import FallbackHandler, RequestHandler, Application from flasky import app class MainHandler(RequestHandler): def get(self): self.write("This message comes from Tornado ^_^") tr = WSGIContainer(app) application = Application([ (r"/tornado", MainHandler), (r".*", FallbackHandler, dict(fallback=tr)), ]) if __name__ == "__main__": application.listen(8000) IOLoop.instance().start()这段代码展示了核心思想:Tornado 优先匹配自定义的MainHandler路由,其余所有请求(r".*")都落入FallbackHandler,转交给 WSGIContainer 包装的 Flask 应用。
1.3 找到可覆写的钩子:start_listening
既然 Streamlit 的_create_app()也用tornado.web.Application,剩下的问题就是如何在 Streamlit 启动时把我们的FallbackHandler追加进它的路由表。观察streamlit.server.server.Server.start中这两行:
app = self._create_app() start_listening(app)start_listening是一个公开函数——公开意味着它可以在 Python 中被“偷梁换柱”(monkey-patch)。于是思路清晰了:覆写start_listening,在它被调用时先把 Flask 应用挂进 Tornado 路由,再调用原始的start_listening完成 Streamlit 自身的监听。
二、核心原理:覆写start_listening的三步曲
最初的探索代码(即 docs/EMBEDDED_STREAMLIT.md 中记录的原型)如下:
import streamlit.server.server as streamlit_server from streamlit.cli import main from tornado.wsgi import WSGIContainer from tornado.web import FallbackHandler from dtale.app import build_app orig_start_listening = streamlit_server.start_listening def _override_start_listening(app): dtale_app_obj = build_app(reaper_on=False) tr = WSGIContainer(dtale_app_obj) app.add_handlers(r".*", [(".*dtale.*", FallbackHandler, dict(fallback=tr))]) orig_start_listening(app) streamlit_server.start_listening = _override_start_listening对这段代码逐行拆解:
- 保存原始函数:
orig_start_listening = streamlit_server.start_listening,为稍后“先挂路由、再正常监听”做准备; - 构建 Flask 应用:调用
dtale.app.build_app(reaper_on=False)。注意reaper_on=False——D-Tale 默认会启动一个“自动收割(reaper)”子进程,在一小时无活动后关闭应用(见 dtale/app.py 中REAPER_TIMEOUT与build_reaper的实现);在嵌入 Streamlit 的场景下,进程生命周期由 Streamlit 托管,必须关闭这个自带的重启/关闭机制; - 注册路由:
app.add_handlers(r".*", [(".*dtale.*", FallbackHandler, dict(fallback=tr))])将 D-Tale 应用以FallbackHandler形式挂进 Tornado 的Application。
2.1 路由匹配器的两个关键约束
原文特别强调了两点容易踩坑的约束,它们至今仍是正确嵌入的准绳:
- 匹配器不能过松:如果匹配模式写成
".*",会吞掉 Streamlit 自身的核心路由,导致整个 Streamlit 应用不可用; - 匹配器要覆盖所有 Flask 路由:
".*dtale.*"只匹配包含dtale的路径,如果 D-Tale 应用中存在不包含该关键字的其他路由(例如"/"、"/favicon.ico"、"/health"等,参见 dtale/app.py 中build_app()注册的根路由),这些路由将不会被转发到 Flask 应用。若你的 Flask 应用路由命名五花八门,需要相应调整正则,让所有业务路径都能命中 FallbackHandler。
三、仓库内的生产级实现:dtale/cli/streamlit_script.py
上文原型最终被固化进 D-Tale 仓库,成为官方入口模块 dtale/cli/streamlit_script.py。与早期原型相比,它针对 Streamlit 的版本演进做了兼容处理:
try: import streamlit.server.server as streamlit_server except BaseException: # ImportError, ModuleNotFoundError import streamlit.web.server.server as streamlit_server import sys try: from streamlit.cli import main except BaseException: from streamlit.web.cli import main from tornado.wsgi import WSGIContainer from tornado.web import FallbackHandler from dtale.app import build_app from dtale.cli.clickutils import run orig_start_listening = streamlit_server.start_listening def _override_start_listening(app): dtale_app_obj = build_app(reaper_on=False) tr = WSGIContainer(dtale_app_obj) app.add_handlers(r".*", [(".*dtale.*", FallbackHandler, dict(fallback=tr))]) orig_start_listening(app) streamlit_server.start_listening = _override_start_listening # we need to stop XSRF protection since dash won't work otherwise additional_args = [] if "--server.enableCORS" not in sys.argv: additional_args += ["--server.enableCORS", "false"] if "--server.enableXsrfProtection" not in sys.argv: additional_args += ["--server.enableXsrfProtection", "false"] sys.argv += additional_args if __name__ == "__main__": run(main)生产实现相对原型的差异与原因:
- 双路 import 兜底:旧版 Streamlit 把
server与cli放在streamlit.server.server/streamlit.cli,新版本迁移到了streamlit.web.server.server/streamlit.web.cli。仓库用try/except BaseException依次尝试,兼容两类版本; - 命令行包装:通过
dtale.cli.clickutils.run(main)执行 Streamlit 的 CLI(run的实现在 dtale/cli/clickutils.py,负责整理参数顺序、打印版本与耗时日志、统一异常退出码); - XSRF/CORS 的自动处理:见下文第四节。
3.1 入口注册:setup.py中的console_scripts
为了让用户能以命令行方式调用,D-Tale 在 setup.py 中注册了两个 console_scripts 入口:
entry_points={ "console_scripts": [ "dtale = dtale.cli.script:main", "dtale-streamlit = dtale.cli.streamlit_script:main", ] }这样安装 D-Tale 后,就可以像使用streamlit命令一样,用带dtale-前缀的命令启动:
dtale-streamlit run my_script.py同时,setup.py 还提供了streamlit安装 extra:"streamlit": ["streamlit"],一键安装时即可带上 Streamlit 依赖。
3.2 测试佐证
仓库测试 tests/dtale/test_cli.py 中的test_streamlit用例直接验证了这一机制的行为:
- 模拟
dtale.app.build_app,断言其以reaper_on=False被调用; - 分别对
streamlit.server.server.start_listening与streamlit.web.server.server.start_listening打桩(同样体现了新旧版本兼容策略); - 断言
sys.argv末尾被追加了["--server.enableCORS", "false", "--server.enableXsrfProtection", "false"]。
四、EXTRA HACK:为什么必须关闭 XSRF 防护与 CORS
这是原文档中重点标注的“额外黑客手段”,也是 D-Tale 嵌入 Streamlit 能否正常工作的关键:
D-Tale 的图表模块基于 Plotly Dash,而 Dash 大量使用POST 请求;当 Streamlit 默认开启
server.enableXsrfProtection(XSRF 跨站请求伪造防护)时,这些 POST 请求会被拒绝,导致 Dash 功能不可用。
因此在 dtale/cli/streamlit_script.py 中,模块加载时会自动向sys.argv追加两个参数:
additional_args = [] if "--server.enableCORS" not in sys.argv: additional_args += ["--server.enableCORS", "false"] if "--server.enableXsrfProtection" not in sys.argv: additional_args += ["--server.enableXsrfProtection", "false"] sys.argv += additional_args设计上做了两处防御:
- 幂等性:先检查命令行里是否已经显式传入了这两个参数(
"--server.enableCORS" not in sys.argv),如果用户自己指定过,就尊重用户的选择,不重复追加; - 默认关闭:未显式指定时,默认追加
--server.enableCORS false与--server.enableXsrfProtection false。
如果你的嵌入方案中没有 Dash 这类依赖 POST 请求的组件,可以忽略这条 hack;若你的 Flask 应用同样需要 POST,则这条经验直接适用。
五、实战:改造你的 Streamlit 脚本使用 D-Tale
配合 docs/EMBEDDED_DTALE_STREAMLIT.md,官方给出了从安装到改脚本的完整上手路径。
5.1 安装与启动
两种安装方式任选其一:
pip install streamlitpip install dtale[streamlit](推荐,一步到位)
启动时不要用streamlit命令,而要用 D-Tale 提供的入口:
dtale-streamlit run my_streamlit_script.py其余参数与streamlit run的用法一致。
5.2 向 D-Tale 保存数据:dtale.views.startup
在 Streamlit 脚本中,通过startup()把 DataFrame 注册进 D-Tale 的全局状态:
import pandas as pd from dtale.views import startup df = pd.DataFrame([1, 2, 3, 4]) startup(data_id="1", data=df)data_id是这条数据在 D-Tale 全局状态中的唯一标识(startup的完整签名与参数说明见 dtale/views.py)。在 Streamlit 场景下,进程内全局状态(dtale.global_state)随 Streamlit 服务器常驻,因此数据只需注册一次,后续刷新页面时依然可以按data_id取回。
5.3 从 D-Tale 取回数据:dtale.app.get_instance
from dtale.app import get_instance # 记住我们上一步把 data_id 设成了 "1" df = get_instance("1").dataget_instance(data_id)(实现见 dtale/app.py 中get_instance函数)返回一个DtaleData实例,其.data属性就是注册时的 DataFrame。只要记住data_id,就能随时拿到 D-Tale 中任意一份数据。DtaleData还提供了.view_data(应用了 UI 端排序/过滤后的数据)、.kill()、.cleanup()等能力(见 dtale/views.py 起的类定义)。
5.4 前端展示:在<iframe>中渲染 D-Tale
D-Tale 的路由以/dtale/main/{data_id}为前缀(DtaleData.build_main_url的实现在 dtale/views.py),因此可以直接嵌进 Streamlit 页面:
<iframe src="/dtale/main/1" />这会渲染标准的 D-Tale 网格,且网格上的所有 D-Tale 操作(排序、过滤、编辑单元格、图表等)都可以正常使用——因为请求都经由同一台 Tornado 服务器转发到了嵌入的 Flask 应用。
如果需要在新标签页打开,用普通链接即可:
<a href="/dtale/main/1" target="_blank">Dataframe 1</a>六、仓库内置的两个完整示例
6.1 示例一:改造 DataFrame + 嵌入列分析面板
script_columns_and_code.py 演示了三条典型操作:
- 用
startup(data_id="1", data=df)初始化数据,并用get_instance("1")读回、刷新列清单; - 用
<iframe src="/dtale/main/1">嵌入主网格; - 用
<iframe src="/dtale/popup/column-analysis/1?selectedCol={col}">嵌入指定列的列分析弹窗(对应后端路由@dtale.route("/column-analysis/<data_id>"),见 dtale/views.py); - 通过
requests.get("http://localhost:8501/dtale/code-export/1").json()["code"]拉取 D-Tale 根据当前 UI 操作生成的 Pandas 代码,展示在 Streamlit 侧边栏(后端路由见 dtale/views.py)。
其中把生成代码的前缀替换为df = pd.DataFrame([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),是为了让示例代码可直接独立运行,不依赖 D-Tale 会话上下文。
6.2 示例二:爬取 baseball-reference.com 并动态加载数据
script_baseball.py 展示了一个更贴近真实业务的数据流:
- 用户在
st.text_input输入球员姓名; - 脚本用
requests+BeautifulSoup抓取 baseball-reference.com 的搜索结果(带 5 次重试的load_content); - 命中唯一球员时抛出
PlayerPageRedirectException直接跳转,否则用按钮让用户选择; update_dtale_data(player_data)内核对数据管理 API 的典型用法:
def update_dtale_data(player_data): curr_data = get_instance("1") if curr_data is not None: # append data to pre-existing data in D-Tale curr_data = curr_data.data curr_data = curr_data[~(curr_data["name"] == player_data["name"].values[0])] player_data = pd.concat([curr_data, player_data], ignore_index=True) cleanup("1") # load data to D-Tale startup(data_id="1", data=player_data)这里先用cleanup("1")删除旧实例(cleanup的实现见 dtale/global_state.py),再startup重新注册,实现“同一 data_id 下的数据替换”。
- 动态拼接 D-Tale 图表 URL,并利用
uuid.uuid4().hex作为_id参数——这是 Streamlit 页面重载时强制 iframe 重新加载的常用技巧:
def build_chart_url(names): params = { "chart_type": "line", "x": "Season", "y": json.dumps(["HR"]), "group": json.dumps(["name"]), "group_type": "groups", "group_val": json.dumps([{"name": name} for name in names]), } return "/dtale/charts/1?{}".format(urllib.parse.urlencode(params))- 最后用
st.markdown(html, unsafe_allow_html=True)输出含自定义 CSS 的 iframe,让网格与图表铺满 Streamlit 页面。
七、注意事项与边界
- 路由匹配模式:
app.add_handlers中".*dtale.*"的正则决定了哪些 Flask 路由可用,务必按需调整,既不能漏掉业务路由,也不能宽松到吞掉 Streamlit 自身路由; reaper_on=False:嵌入场景下必须关闭 D-Tale 的自动收割机制,避免其“一小时无活动即关停”的后台线程干扰 Streamlit 进程(相关代码见 dtale/app.py 的build_reaper/REAPER_TIMEOUT);- XSRF/CORS 默认关闭:这是 Dash POST 请求能正常工作的前提,涉及安全性取舍时需结合部署环境评估(见第四节);
- 版本兼容:Streamlit 新旧版本模块路径不同(
streamlit.server.*与streamlit.web.*),仓库实现已用try/except兜底,若你在自定义嵌入脚本中复刻此方案,也应保留同样的容错逻辑; - iframe 高度:D-Tale 网格与图表默认不会自适应父容器高度,两个示例脚本中都通过自定义 CSS(如
iframe.grid { height: 700px; })显式指定高度,实际项目中需要按页面布局调整。
八、总结
本文完整还原了 D-Tale 嵌入 Streamlit 的技术路线:从发现 Streamlit 底层是 Tornado、找到_create_app()与可覆写的start_listening(),到用WSGIContainer+FallbackHandler把 Flask 应用挂进 Tornado 路由表,再到仓库中 dtale/cli/streamlit_script.py 的完整生产实现与 tests/dtale/test_cli.py 的测试验证。这套方案不仅适用于 D-Tale,也适用于任何希望寄生在 Streamlit 服务器上的 Flask 应用。配合startup/get_instance的数据管理 API 与 iframe 渲染模式,你可以在一个 Streamlit 页面里同时拥有快速 UI 开发能力和 D-Tale 的完整数据可视化能力,相关思路同样可以参考 D-Tale 嵌入 Django 与 Flask 的姊妹篇文档。
- 数据分析
- 数据可视化
- 后端
- 前端
【免费下载链接】dtale
Visualizer for pandas data structures
相关推荐
在 Streamlit 中嵌入 D-Tale:从 `dtale-streamlit` 命令到 iframe 集成实战指南
在 Streamlit 中嵌入 D Tale:从 dtale streamlit 命令到 iframe 集成实战指南 D Tale 是 pandas 数据结构的
数据分析数据可视化后端前端GitHub_Trending/cl/claude-plugins-official插件市场推广:提升插件可见度的终极策略
GitHub_Trending/cl/claude plugins official插件市场推广:提升插件可见度的终极策略 在当今快速发展的开发者工具生态中,
AI 插件开发工具插件系统Operit 的 ADB 双 applicationId 调试支持:Debug/Release 双包解析、动态路径与热更新隔离
Operit 的 ADB 双 applicationId 调试支持:Debug/Release 双包解析、动态路径与热更新隔离 导读 本文讲解 Operit 仓
数据分析数据可视化后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考