ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Streamlit+Firebase+Bucket:Python数据应用后端实战指南

Streamlit+Firebase+Bucket:Python数据应用后端实战指南 做数据应用的朋友大概都听过 Google Firebase——谷歌出的一套云后端服务Streamlit 是 Python 圈子里这两年火得最快的 Web 应用框架Bucket 则是 Firebase/Google Cloud 里用来放文件的云存储空间。这三个词拆开看各有各的生态但组合在一起其实能非常优雅地解决一个现实问题怎么让一个纯 Python 开发的 Streamlit 应用从“本地能跑”变成“线上能用、用户能登录、文件能上传”。今天这篇实战教学不会只丢给你三段官网文档而是按照我做项目时的真实顺序把 Streamlit 后端能力的搭建、Firebase 的接入、Bucket 文件存储的整个链路串起来讲并且把 PyCharm 里跑 Streamlit 和 WebView 加载白屏这两个高频坑单独拆开说透。适合的读者是已经能熟练写点 Python 脚本、想给自己的数据分析工具加上后端能力的开发者。1. 为什么是这哥仨一套数据应用后端的刚需拆解1.1 Streamlit 到底解决了什么痛点Streamlit 这类工具火的根本原因是它把 Web 开发的门槛从“前后端都要会”降到了“会写 Python 就行”。我见过不少数据分析师能写出漂亮的 pandas 处理逻辑但一提 Flask Vue 就头大。Streamlit 的做法是你只管写 Python 脚本页面渲染、组件交互、状态管理都交给框架处理。一个典型的数据看板应用核心元素就是按钮、输入框、表格、图表。在 Streamlit 里一行st.button就是按钮一行st.dataframe就是可交互表格一行st.line_chart出折线图。这背后其实是一套基于 WebSocket 的双向通信机制Python 端执行脚本生成 UI 描述浏览器端接收并渲染每次交互触发重新运行。理解这个机制很重要因为后面所有性能优化和排查基本都围绕“它又从头跑了一遍脚本”这个特性展开。不过 Streamlit 只是一个前端或者说应用层框架它不解决数据存储和用户认证的问题。你做的应用一旦需要“记住用户上一次输入的内容”或者“每个账号只能看自己的数据”就必须引入后端服务这就是 Firebase 登场的理由。1.2 Firebase后端能力全家桶Firebase 最吸引我的地方是它把后端最常见的几件事都抽象成现成的 API。Authentication 管登录注册支持邮箱密码、Google、GitHub 等多种方式Firestore 是一种文档型 NoSQL 数据库数据按“集合-文档-字段”组织JSON 友好实时同步能力很强Cloud Storage 就是对象存储也就是常说的 Bucket专门放大文件和静态资源。为什么要用这套而不是自己写后端个人项目的残酷现实是你不想为一个月访问几百次的工具单独维护一台服务器不想写密码加密、token 刷新、文件分片上传这些边际成本极高的代码。Firebase 的免费额度——认证用户数、每天几万次数据库读写、存储少量数据——对个人项目和中小型工具来说是够用的一键部署、全球 CDN、自动扩缩容还省掉了运维告警的负担。但 Firebase 官方 SDK 偏向 JavaScript 和移动端Python 后端要用的是 Admin SDK它的权限更高不受安全规则限制等于“管理员直连”。这套权限模型决定了后续的架构思路Streamlit 应用作为服务端通过 Admin SDK 操作 Firebase不需要暴露 API Key 给浏览器。1.3 Bucket数据库放不下的那部分文件在 Firebase 体系里Bucket 的角色其实是“备用仓库”。Firestore 单条文档有 1MB 的体积限制实际使用中建议几 KB 以内查询价格也随数据量和调用次数增长。你要是把图片、CSV 文件、训练好的模型权重直接往 Firestore 里塞不是不能是既贵又慢。正确姿势是“文件进 Bucket元数据进 Firestore”。比如用户上传一个 CSV你先把它丢进uploads/user_123/20240112_data.csv这个存储路径拿到一个文件标识再把文件名、大小、上传时间、对应的用户 ID 存到 Firestore 的一条文档里。这样数据库里存的永远是轻量的小 JSON大文件交给专门为存储而生的对象存储服务。还有一个容易忽略的点Bucket 里的对象可以直接通过 HTTPS URL 访问配好规则后图片、附件可以当静态资源用。Streamlit 页面拿到 URL 就能st.image(url)展示不需要把文件内容拖进内存加载速度比从数据库读二进制快得多。2. 环境准备在 PyCharm 里把 Streamlit 跑起来2.1 虚拟环境与依赖安装的正确姿势PyCharm 里跑 Python 项目最大的坑从来不是代码本身而是解释器和包环境张冠李戴。我接过好几个朋友报过来的ModuleNotFoundError: No module named streamlit十有八九是包装到了全局 Python而项目用的是虚拟环境两者互不相认。所以第一步先把环境理顺PyCharm 新建项目时选择VirtualenvPython 版本选 3.9 或 3.10 这类稳定版本然后把依赖装到项目自己的虚拟环境里pip install streamlit firebase-admin google-cloud-storage pyrebase4 pandas requests不要用pip3和python3的混搭方式。项目根目录下执行python -m pip install和python -m streamlit run app.py这样能确保命令和解释器始终指向同一个环境。这是一个小而重要的习惯用python -m而不是裸命令能规避 PATH 指向错误带来的所有诡异问题。2.2 PyCharm 运行 Streamlit 的三种方式第一种也是我日常最常用的在 PyCharm 底部打开 Terminal确认激活了虚拟环境后直接执行streamlit run app.py第二种配置 Run Configuration 方便点绿色的调试按钮选择Python类型Script path 指向 streamlit 包入口文件通常在venv/lib/python3.10/site-packages/streamlit/web/bootstrap.py或者在 Run Configuration 里选择Module name为streamlitParameters 填run app.py这样 PyCharm 就知道要从模块启动而不是直接运行脚本。注意 Working directory 必须设置成项目根目录否则会找不到 app.py。第三种是 PyCharm Professional 自带 Streamlit 支持安装 Data Science and ML 插件后在运行配置里直接选 “Streamlit”会自动带出端口和浏览器选项。免费版没有这个选项的话前两种就够用了。2.3 端口、改代码自动重载这些小事Streamlit 默认跑在 8501 端口这个端口其实经常被各种本地服务占用。冲突时终端会提示端口被占用换一个就行streamlit run app.py --server.port 8502开发时最舒服的是代码保存后页面自动重载Streamlit 默认开启了这个能力原理就是文件系统监听检测到.py文件变更就重新运行脚本并推送给在线会话。如果你发现保存后页面不更新检查一下是不是用了--server.runOnSave false或者 IDE 在保存时只弹了对话框没有真正落盘。另外有个实用参数--server.fileWatcherType poll在 Linux 虚拟机或者 Docker 挂载卷里默认的 watchdog 监听机制经常失效改成 poll 轮询反而稳定。这些参数可以在启动命令里临时加也可以写到项目根目录的.streamlit/config.toml里固化下来[server] port 8501 runOnSave true fileWatcherType poll [browser] gatherUsageStats false3. Firebase 项目从 0 到 1控制台操作与 SDK 接入3.1 创建项目模式选择决定安全基线登录 Firebase 控制台点加号创建项目流程很流畅核心就一件事项目命名。项目名会直接决定默认的云存储地址和后续域名建议起一个全局唯一且语义清晰的名字格式类似my-data-tool-2024。创建完成后先在左侧菜单里找到 Firestore Database进入创建资料库这里会问你选生产模式还是测试模式。这一步很多人随手选了测试模式挺危险的。测试模式意味着数据规则全放行任何人只要拿到项目 ID 就能读写。而生产模式下默认拒绝所有非认证访问你需要在“规则”页面里主动声明哪些场景放行。哪怕是简单的一条allow read, write: if request.auth ! null;至少也能挡住互联网上大多数扫描流量。需要明确一个概念如果你用 Admin SDK 从 Streamlit 后端操作 FirestoreFirestore 安全规则其实是绕过的因为服务账号证书本身就是最高权限。规则主要约束的是浏览器端/移动端直接用 Firebase JS/Android SDK 的访问。但在项目初期就养成收紧权限的习惯能省掉后面很多合规麻烦。3.2 服务账号密钥Python 后端的入场券Firebase Python Admin SDK 的认证方式是服务账号密钥文件。在控制台进入 Project settings → Service accounts点击 Generate new private key浏览器会下载一个 JSON 文件里面包含 project_id、private_key 和 client_email 等信息。这个文件就是你的后端身份的“私章”等同于管理员权限一旦泄露别人可以对你的 Firestore 和 Storage 为所欲为。下载后把 JSON 放到项目目录下例如configs/firebase-service-account.json。然后安装依赖并初始化import firebase_admin from firebase_admin import credentials, firestore, storage cred credentials.Certificate(configs/firebase-service-account.json) firebase_admin.initialize_app(cred, { storageBucket: my-data-tool-2024.appspot.com }) db firestore.client() bucket storage.bucket()注意storageBucket的命名规则通常是项目ID.appspot.com你可以在 Firebase 控制台的 Storage 页面看到完整字符串第一次写错的话初始化不会报错但后面所有 bucket 操作都会提示找不到存储桶所以不如一开始就复制粘贴。3.3 用环境变量保护密钥的小习惯把服务账号 JSON 明文放进项目目录对于本地开发来说问题不大但如果代码要推到 GitHub 或者部署到服务器这就是一个事故隐患。我的做法是用环境变量存证书内容代码里动态加载import os, json, tempfile from firebase_admin import credentials cert_json os.environ.get(FIREBASE_SERVICE_ACCOUNT) if cert_json: with tempfile.NamedTemporaryFile(modew, suffix.json, deleteFalse) as f: f.write(cert_json) tmp_path f.name cred credentials.Certificate(tmp_path) else: cred credentials.Certificate(configs/firebase-service-account.json)同时把configs/*.json、.streamlit/secrets.toml写进.gitignore。这样别人 clone 你的项目时只需要自己去控制台生成一份密钥并配置环境变量代码里的逻辑完全不需要改动。4. Streamlit Firestore让应用有数据库4.1 Admin SDK 初始化与缓存Streamlit 的特殊执行模型决定了你在代码里不能像普通 Python 后端那样随手全局初始化因为每次页面交互整个脚本都会从头跑一遍。如果每次重跑都调用firebase_admin.initialize_app()会直接报Default Firebase app already exists。解决办法是把初始化包在st.cache_resource里。这个装饰器会按函数参数缓存返回值只要没触发缓存失效后续重跑直接取缓存import streamlit as st st.cache_resource def get_firebase_bucket(): if not firebase_admin._apps: cred credentials.Certificate(configs/firebase-service-account.json) firebase_admin.initialize_app(cred, {storageBucket: my-data-tool-2024.appspot.com}) return firestore.client(), storage.bucket() db, bucket get_firebase_bucket()判断firebase_admin._apps是否为空是为了防止重复初始化这段代码在 Streamlit 热重载和云端容器重启时都能稳定工作。缓存资源在脚本重跑时不会丢Session 结束才释放这一点对 Firebase 持久的连接池来说非常友好。4.2 用户登录与会话保持Streamlit 本身没有用户体系前端不能安全地存密码最稳的方案是让 Firebase Authentication 来做身份认证。Firebase Admin SDK 其实不完全适合做“输入密码换取登录态”这件事因为它走的是服务端管理接口不校验普通用户的密码。社区通用解法是用 Firebase Auth 的 REST API 或者 Pyrebase 这类封装库。Pyrebase 虽然维护步调慢但 API 简单适合内部工具。配置信息从 Firebase 控制台的应用设置里复制import pyrebase firebase_config { apiKey: AIza..., authDomain: my-data-tool-2024.firebaseapp.com, projectId: my-data-tool-2024, storageBucket: my-data-tool-2024.appspot.com, messagingSenderId: 1234567890, appId: 1:1234567890:web:abcdef } firebase_auth pyrebase.initialize_app(firebase_config).auth()登录放进 Streamlit 的 session_state 里实现“页面刷新不退出”的效果if user_token not in st.session_state: st.session_state.user_token None with st.form(login_form): email st.text_input(邮箱) password st.text_input(密码, typepassword) if st.form_submit_button(登录): try: st.session_state.user_token firebase_auth.sign_in_with_email_and_password(email, password)[idToken] st.rerun() except Exception: st.error(登录失败)如果不想引入 Pyrebase 这个几十MB的依赖也可以直接请求 identitytoolkit 接口核心代码只有十几行原理和上面一致。用 REST 的意义在于少一层黑盒依赖排查起来更可控。4.3 一个完整的读写示例用户登录后把用户信息和项目数据存进 Firestore。下面这段代码演示了最典型的 CRUD 场景按用户 ID 隔离数据、写入带服务端时间戳、读取后渲染到页面。uid firebase_auth.get_account_info(st.session_state.user_token)[users][0][localId] user_ref db.collection(users).document(uid) # 写入用户资料 user_ref.set({ email: email, created_at: firestore.SERVER_TIMESTAMP, plan: free }, mergeTrue) # 写入一条任务数据 db.collection(tasks).add({ user_id: uid, title: st.text_input(任务标题), status: todo, created_at: firestore.SERVER_TIMESTAMP }) # 查询当前用户的任务列表 tasks db.collection(tasks).where(user_id, , uid).stream() for doc in tasks: st.write(doc.id, doc.to_dict())Firestore 查询天然支持按字段过滤where(user_id, , uid)这行代码就是后面所有“按账号隔离数据”功能的基础。注意使用mergeTrue时set只更新传入的字段而不会覆盖整个文档时间戳用SERVER_TIMESTAMP由数据库服务端生成避免客户端时钟不准造成排序混乱。4.4 Firestore 与 Bucket 怎么分工结合前面说的做一个数据应用时的存储策略可以简单概括成数据表中放的字段尽量都是基础类型比如字符串、数字、布尔值、时间。凡是“文件”性质的内容在数据库里只记录一条引用。内容类型存储位置数据库字段示例用户名、邮箱、订阅状态Firestore 文档字段email: userexample.com用户上传的CSV数据文件Bucket 对象file_path: uploads/uid123/a.csv图片、Logo、附件Bucket 对象image_url: https://...操作日志、事件记录Firestore 集合每行为一条文档这条规则本质上是一个成本优化策略。Firestore 按文档读写次数计费而请求的 body 大小本身也计入流量。对象存入 Firestore 时还会额外增加各种开销得不偿失。文件和数据的分离也方便你将来把文件迁移到 CDN 或者换存储服务商不会动数据库结构。5. Bucket 实战上传、下载、展示一条龙5.1 创建存储桶与访问控制在 Firebase 控制台进入 Storage 页面首次进入会让你设置起始规则。这里的规则和 Firestore 类似默认拒绝所有非认证访问是最稳的。如果你有文件需要公开展示最好不要把整个桶都置为公开而是只开放特定前缀目录。比如只允许匿名用户读取public/目录下的对象rules_version 2; service firebase.storage { match /b/{bucket}/o { match /public/{allPaths**} { allow read: if true; } match /{allPaths**} { allow read, write: if false; } } }这个规则写的很好懂凡是对象路径以public/开头的任何人不带凭证就能读其他路径全部拒绝。这种“最小公开”思路既方便了某些场景下直接贴图片链接又不会无意间把用户上传的私有数据暴露出去。5.2 文件上传到 Bucket 的完整代码Streamlit 的上传组件st.file_uploader返回的是UploadedFile对象它是一个类文件对象可以直接传给blob.upload_from_file。注意要给每个文件一个唯一路径否则不同用户上传同名文件会互相覆盖import time from datetime import timedelta uploaded st.file_uploader(选择文件, type[csv, txt, xlsx, png, jpg]) if uploaded is not None: uid st.session_state.get(uid, anonymous) object_path fuploads/{uid}/{int(time.time())}_{uploaded.name} blob bucket.blob(object_path) blob.upload_from_file(uploaded, content_typeuploaded.type) st.success(f已上传{object_path}) # 把元数据写入 Firestore方便后续查询 db.collection(files).add({ user_id: uid, path: object_path, size: uploaded.size, name: uploaded.name, uploaded_at: firestore.SERVER_TIMESTAMP })content_type这个参数值得多说一句。不传的话对象默认会被标记为application/octet-stream浏览器访问时大概率触发下载而不是预览传了才能保证图片直接在页面展示、文本文件能内联查看。这属于那种“影响观感但不报错”的坑等到线上才发现就晚了。5.3 下载与签名 URL 展示下载文件有两种常见姿势。第一种是在数据应用内部导出场景直接读成字节流给用户blob bucket.blob(exports/result.csv) data blob.download_as_bytes() st.download_button(下载结果, datadata, file_nameresult.csv, mimetext/csv)第二种是把文件变成临时访问链接适合传给第三方系统或者在浏览器里直接预览。由于默认规则是非公开的需要生成一个带签名的 URL有效期可以精确控制到秒url blob.generate_signed_url(timedelta(minutes15), methodGET) st.image(url, caption预览图)这里有个前置条件签名 URL 依赖服务账号的私钥而你的 Admin SDK 已经持有私钥所以生成过程是免费的、不需要额外的权限配置。如果遇到ValueError: requires a private key之类的报错基本可以确认是初始化证书时传了路径而不是文件内容或者临时文件被提前删除检查一下证书加载那一段就好。需要特别提醒的是generate_signed_url生成的链接理论上任何持有者都能访问所以有效期要按最小够用原则设置外部展示场景 515 分钟足够时间太长等于变相公开了私有文件。6. 高频问题排查白屏、模块找不到、连不上6.1 WebView 加载 Streamlit 白屏的排查清单“web_view 加载 streamlit url 白屏”是一个非常有代表性的问题。我排过几次发现白屏本质上不是 Streamlit 的错误而是 WebView 和现代 Web 应用之间的兼容性问题。Streamlit 前端是个重度依赖 JavaScript、WebSocket 和 localStorage 的单页应用任何一个环节被掐断表现都是白屏。Android WebView 至少要满足三个条件才能正常渲染JS 必须开启、DOM Storage 必须开启、混合内容策略要允许。在原生 Android 代码里配置webView.settings.javaScriptEnabled true webView.settings.domStorageEnabled true if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) { webView.settings.mixedContentMode WebSettings.MIXED_CONTENT_MODE_ALWAYS_ALLOW }第一行是基本中的基本很多自定义 WebView 默认javaScriptEnabledfalse这不是 Streamlit 的问题。第二行解决 WebSocket 握手阶段对 localStorage 写入的依赖少了它页面经常卡在初始化。第三行解决页面主体是 HTTPS 但内部请求走了 HTTP 的情况。如果你的 Streamlit 服务是本地 HTTP比如http://192.168.1.10:8501Android 9 以上系统默认禁止明文流量你需要给 WebView 配置 Network Security Config 允许访问该 IP或者直接在 manifest 里给application节点加android:usesCleartextTraffictrue否则可能连页面标题都看不到。如果服务跑在开发机上还有个大坑Streamlit 默认只监听localhost局域网设备访问不了。要让同一网络里的手机能访问启动时可以加上几个参数streamlit run app.py --server.address 0.0.0.0 --server.enableCORS false --server.enableXsrfProtection false其中enableCORSfalse是让同网段设备访问时不因为来源校验被拦enableXsrfProtectionfalse是关闭 WebSocket 升级阶段的可信来源校验。在公网部署如果用了反向代理这两个配置同样适用但要注意在真正开放公网前先确认网络安全策略。最后注意 WebView 内核版本。Android 7 及以下设备的系统 WebView 可能停留在早期 Chromium而 Streamlit 后端对浏览器特性的要求逐年提高白屏之后按上面条件全查过一遍还不行就把设备升级一下 WebView 组件很多疑难杂症直接消失。以下是我整理的速查表白屏/异常现象优先怀疑处理动作页面一片空白无任何文字JS未开启javaScriptEnabled true白屏但偶尔闪现页面标题DOM Storage 未开启domStorageEnabled true加载的是HTTP页面无法打开明文流量被拦截usesCleartextTraffictrue或 Network Security Config局域网手机访问失败服务绑定在127.0.0.1--server.address 0.0.0.0能加载但WebSocket频繁断来源校验拦截--server.enableXsrfProtection false老设备长期白屏系统WebView过旧升级 Android System WebView6.2 PyCharm 里 ModuleNotFoundError 的根源ModuleNotFoundError: No module named streamlit是 PyCharm 新手最常碰到的问题。根源几乎都是解释器错位左边项目设置用的是 Python 3.10 虚拟环境右边终端激活的却是 conda base 或者系统全局 Python。装包时你在界面右下角选择了解释器但 Terminal 里却默认进了全局环境。解决办法是让终端和项目解释器严格保持一致。最简单的方式在 PyCharm 右下角看到当前解释器路径然后在终端执行python -m pip install streamlit用python -m确保装进当前解释器的 site-packages。运行同样用python -m streamlit run app.py。这样不管界面上点运行还是终端敲命令用的都是同一个环境。还有一种情况是代码能跑但 IDE 画红线标没有包这种通常是 IDE 的索引缓存没刷新去 Settings → Project → Python Interpreter 里点一下刷新按钮即可不需要重装任何东西。6.3 Firebase 连接不上的环境排查连接 Firebase 失败多数不是代码问题。我遇到过两个影响面最广的坑一个是系统时间偏差一个是依赖版本。服务账号证书本质上是一个 JWTFirebase 服务端会校验它的签发时间如果你服务器系统时间快了或慢了三分钟以上就会收到Invalid assertion或Token used before issued at之类的错误。排查方法很简单先看服务器date输出的时间再对一下当前时间偏差大的就同步系统时间。另一个是 Python 版本。新版本的firebase-admin对旧版 Python 已停止支持而在某些预装旧版 Python 的服务器上pip 会静默安装一个旧版 firebase-admin旧版 API 和文档完全对不上。解决办法是使用虚拟环境装一个明确的版本组合Python 3.9 以上firebase-admin6.0google-cloud-storage2.0。如果初始化时卡住还可以先做一个连通性测试。在服务器上用curl -I https://firestore.googleapis.com看返回码如果是 403 之类说明 TLS 连通没问题但请求被拒如果直接 timeout说明网络层到对应域名存在连通性问题。注意这不是代码层面的问题属于环境网络策略问题需要调整部署环境。最后分享一个我自己的实践体会。这套组合最适合的场景是数据产品原型和内部工具核心价值是一周内把想法落地而不是压榨性能。踩过几次坑之后我现在每次新建项目都固定做三件事把 Firebase 初始化代码抽成独立工具函数、把 Bucket 目录规划写死进 README 模板、把所有 WebView 调试开关做成一份速查清单。这些看起来很琐碎的习惯反而是被线上事故逼出来的。希望这篇实战记录能让你少走几段弯路。
返回列表