Streamlit应用Heroku部署实战:从本地到公网的全流程解析

📅 2026/7/21 22:51:19
Streamlit应用Heroku部署实战:从本地到公网的全流程解析
1. 项目概述一个能跑通的 Streamlit Heroku 全流程不是教程拼凑而是真实部署现场复盘Streamlit 是我过去三年里在数据科学团队内部推广最顺利的工具——不是因为它多炫酷而是它把“写完分析代码 → 做成可交互界面 → 让业务同事点开就用”这个链条压缩到了 20 分钟以内。但真正卡住绝大多数人的从来不是st.write(df)这一行而是最后一步怎么让同事、客户、甚至老板在浏览器里输入一个网址就能看到你的分析看板Heroku 曾经是这个问题最平滑的解法尤其对没有 DevOps 资源的个人开发者和小团队而言。它不强制你懂 Dockerfile 的每一行也不要求你配置 Nginx 反向代理更不用你去理解什么是 ingress controller。它只要求你把代码放进 Git 仓库配好Procfile和requirements.txt然后敲一条git push heroku main—— 五分钟后一个带 HTTPS 的公网 URL 就生成了。这篇内容就是围绕这个标题展开的完整实操记录从零写一个带文件上传、参数滑块、动态图表的 Streamlit 应用到它真正在 Heroku 上稳定运行、被外部用户访问、甚至应对突发流量的小规模压力测试。它不是教你怎么复制粘贴命令而是告诉你为什么streamlit1.32.0不能写成streamlit1.30.0为什么webbrowser.open()在 Heroku 上必须删掉为什么st.cache_data的ttl3600比ttlNone更安全以及——最关键的一点——当你的应用在 Heroku 上第一次启动失败时heroku logs --tail里那几行红色报错信息到底在告诉你什么。如果你正卡在“本地能跑线上报错”的阶段或者刚收到业务方说“链接打不开”那么接下来的内容就是你真正需要的部署现场笔记。2. 整体设计与思路拆解为什么选 Streamlit Heroku 这个组合它解决的是什么层级的问题2.1 不是技术选型而是工作流瓶颈的针对性突破很多人把 Streamlit 当成“Python 版的 Shiny”这没错但它背后解决的其实是更底层的协作断层问题。我在上一家公司做过一个销售漏斗分析工具数据科学家用 Jupyter 写完模型和可视化导出 PDF 给销售总监总监再把截图发给区域经理经理再手动填进 Excel 表格……整个周期平均 3.7 天。后来我们用 Streamlit 重构核心逻辑没变只是把plt.show()换成st.pyplot(fig)把print(result)换成st.metric(转化率, f{rate:.1%})然后部署到 Heroku。结果是销售总监每天早上 9 点打开那个 URL看到的是实时更新的昨日数据区域经理用手机点开滑动时间范围选择器立刻看到自己辖区的漏斗变化甚至新入职的实习生也能通过上传 CSV 文件快速跑通自己的小样本分析。这个转变的关键不在于技术多先进而在于它抹掉了“导出-转发-再处理”这个中间环节。Heroku 的价值也在此它不提供 Kubernetes 集群的弹性伸缩能力但它提供了“零运维部署”的确定性。你不需要申请云主机、配置安全组、申请 SSL 证书、设置域名解析——这些事 Heroku 全包了。它的免费层Hobby tier虽然有 30 分钟休眠限制但对于内部工具、MVP 验证、非实时报表类应用完全够用。我统计过团队过去一年部署的 27 个 Streamlit 应用其中 21 个至今仍运行在 Hobby 层日均访问量在 50–200 次之间从未触发过休眠或超时。2.2 架构极简主义三层结构每层只做一件事整个部署链路被我严格控制在三层且每层职责清晰互不越界第一层应用层Streamlit App它只负责“呈现逻辑”。所有数据获取、清洗、建模都封装在函数里用st.cache_data或st.cache_resource标记UI 元素按钮、滑块、上传框只负责触发这些函数并展示结果。绝不出现os.system(curl ...)这类系统调用也不硬编码数据库连接字符串。我坚持一个原则这个.py文件在本地streamlit run app.py能跑在 Heroku 上也必须能跑且行为一致。第二层运行时层Procfile runtime.txt这是 Streamlit 和 Heroku 的“握手协议”。Procfile明确告诉 Heroku“请用streamlit run app.py启动我的进程”而不是默认的python app.py。runtime.txt则锁定 Python 版本如python-3.11.8避免 Heroku 自动升级到不兼容的 3.12 导致pandas编译失败。这一层的存在本质上是在告诉平台“我不要你猜我要怎么运行我明确告诉你。”第三层依赖层requirements.txt它不是简单的pip freeze requirements.txt输出结果。我坚持手动维护只保留真正被app.pyimport 的包版本号精确到小数点后两位如pandas2.0.3而非pandas2.0.0对streamlit本身必须指定1.40.0因为 1.40.0 引入了新的会话状态管理机制与旧版 Heroku buildpack 兼容性不佳。这里有个血泪教训去年我疏忽写了streamlit~1.35.0结果 Heroku 自动装了 1.39.2导致st.session_state在页面刷新后丢失业务方反馈“每次点按钮都要重新上传文件”排查了两天才发现是版本漂移。2.3 为什么不是其他方案直面现实约束的取舍为什么不选 Streamlit Community Cloud它确实一键部署但有两个硬伤一是私有仓库支持弱需 GitHub 私有库付费 Plan二是无法自定义环境变量比如你不能传入DB_PASSWORD或API_KEY。我们有个客户数据看板必须连接内网 PostgreSQLCommunity Cloud 根本连不通 VPC 对端。为什么不选 Vercel / Netlify它们擅长静态网站和 Serverless 函数但 Streamlit 是长连接 Web 应用基于 Tornado需要持续运行的后台进程。Vercel 的 Serverless 函数有 10 秒超时限制根本撑不住一个st.file_uploader上传 50MB CSV 的过程。我试过用vercel.json强制延长 timeout结果被平台自动拒绝部署。为什么不选 AWS EC2 / GCP Compute Engine可以但成本和复杂度陡增。一个最小规格的 EC2 t3.micro 每月约 $7.2还要自己装 Nginx、配置 Lets Encrypt、写 systemd service、监控进程崩溃——这些工作加起来远超写一个 Streamlit App 本身的时间。Heroku 的 Hobby tier 是免费的且自带 HTTPS、域名、日志、重启策略这才是小团队的真实 ROI。提示Heroku 的免费层已于 2022 年 11 月取消当前最低可用层为 Hobby tier$7/月。但请注意Hobby tier 依然有 30 分钟无请求自动休眠的限制这对需要 24/7 响应的生产应用不适用。本文所有操作均基于 Hobby tier部署后首次访问会有 5–10 秒冷启动延迟这是正常现象无需恐慌。3. 核心细节解析与实操要点那些文档里不会写的“为什么”3.1 Streamlit App 的编写规范不只是能跑更要“能稳”一个能在 Heroku 上长期存活的 Streamlit App和本地调试版有本质区别。我总结了三条铁律第一禁用所有本地路径硬编码。本地开发时你可能习惯写pd.read_csv(data/sales.csv)但在 Heroku 上/app目录是只读的你无法写入任何文件data/目录也根本不存在。正确做法是所有外部数据源必须通过环境变量或网络请求获取。例如import os import pandas as pd # ✅ 正确从环境变量读取 API 地址或使用默认的 demo 数据 API_URL os.getenv(DATA_API_URL, https://example.com/api/sales) df pd.read_json(API_URL) # ❌ 错误绝对路径、相对路径、本地文件读取 # df pd.read_csv(/home/user/data/sales.csv) # 权限错误 # df pd.read_csv(./data/sales.csv) # 文件不存在我甚至养成了一个习惯在app.py开头加一段检查import os if os.getenv(HEROKU) 1: st.warning(⚠️ 正在 Heroku 环境运行所有本地文件操作已被禁用)这样当开发同事误推了本地路径代码一眼就能看到警告。第二st.cache_data必须设ttl且值要合理。st.cache_data默认是永久缓存ttlNone这在本地没问题但在 Heroku 上意味着一旦缓存建立即使你更新了后端 API 返回的数据前端永远显示旧结果。我见过太多次业务方说“数据没更新”结果发现是缓存没失效。我的标准配置是st.cache_data(ttl3600) # 缓存 1 小时 def load_sales_data(): return pd.read_json(os.getenv(SALES_API))为什么是 3600因为太短如 60 秒频繁请求后端增加 API 压力且用户滑动参数时可能看到“闪退式”刷新太长如 86400业务数据隔天才更新但用户上午 9 点看到的还是昨天下午 5 点的数据体验割裂3600 是平衡点覆盖一个典型工作日的大部分时段又保证午休后打开能看到最新数据。第三st.file_uploader的文件处理必须异步化。Heroku Hobby dyno 的内存上限是 512MB。如果用户上传一个 200MB 的 Excelst.file_uploader默认会把整个文件加载进内存瞬间 OOMOut of Memorydyno 直接崩溃重启。解决方案是用BytesIO流式处理不落地import io uploaded_file st.file_uploader(上传销售数据 (CSV/Excel), type[csv, xlsx]) if uploaded_file is not None: # ✅ 正确用 BytesIO 包装pandas 直接读流 if uploaded_file.name.endswith(.csv): df pd.read_csv(io.BytesIO(uploaded_file.getvalue())) else: # xlsx df pd.read_excel(io.BytesIO(uploaded_file.getvalue())) # ❌ 错误先保存到临时文件再读浪费 I/O且可能满磁盘 # with open(/tmp/upload.xlsx, wb) as f: # f.write(uploaded_file.getvalue()) # df pd.read_excel(/tmp/upload.xlsx)3.2 Heroku 部署的“隐形契约”平台规则必须敬畏Heroku 不是一个通用 Linux 服务器它是一套有明确定义的运行时契约。违背它就会失败。以下是三个最关键的“隐形规则”规则一Procfile是唯一入口且必须用web:前缀。Heroku 通过Procfile识别你的应用类型。如果你写成# ❌ 错误没有 web: 前缀Heroku 不知道这是 Web 应用 streamlit run app.py --server.port$PORT --server.address0.0.0.0Heroku 会把它当成一个后台 worker不会分配$PORT环境变量也不会开放 HTTP 端口。正确写法必须是# ✅ 正确web: 告诉 Heroku 这是 Web 进程会自动注入 $PORT web: streamlit run app.py --server.port$PORT --server.address0.0.0.0注意$PORT是 Heroku 动态注入的每次启动都不同绝不能写死为8501。规则二requirements.txt中的streamlit必须显式指定--no-cache-dir。这是 Heroku Buildpack 的一个已知坑。如果不加pip install在构建时会尝试使用本地缓存而 Heroku 的构建环境是干净的容器没有缓存导致安装超时失败。解决方案是在requirements.txt最后一行加# 在 requirements.txt 末尾添加 --no-cache-dir或者更稳妥的做法是在Procfile中直接控制web: pip install --no-cache-dir -r requirements.txt streamlit run app.py --server.port$PORT --server.address0.0.0.0规则三.env文件在 Heroku 上无效必须用heroku config:set。本地开发用python-dotenv加载.env很方便但 Heroku 完全忽略.env文件。所有敏感配置API keys、数据库密码必须通过 Heroku CLI 设置为环境变量# ✅ 正确设置环境变量 heroku config:set DATA_API_URLhttps://api.yourcompany.com/v1/sales heroku config:set SECRET_KEYyour-super-secret-key-here # ❌ 错误以为 .env 会被自动加载 # .env 文件不应提交到 Git它只用于本地然后在app.py中用os.getenv(DATA_API_URL)读取。我建议在app.py开头加一个健康检查required_envs [DATA_API_URL] missing [e for e in required_envs if not os.getenv(e)] if missing: st.error(f❌ 缺少必需环境变量: {, .join(missing)}。请检查 Heroku config:set 配置。) st.stop() # 立即停止执行避免后续报错淹没关键信息3.3 安全与健壮性加固让应用在无人值守时也能扛住部署上线只是开始真正的考验是它能否在你睡觉时、休假时、甚至忘记它存在时依然稳定服务。以下是我在 27 个上线应用中沉淀出的加固清单HTTP 超时设置Streamlit 默认的--server.maxUploadSize是 200MB但 Heroku Router 的请求超时是 30 秒。如果用户上传大文件30 秒内没传完Router 就会断开连接Streamlit 进程却还在等造成资源泄漏。解决方案是在Procfile中显式设置web: streamlit run app.py --server.port$PORT --server.address0.0.0.0 --server.maxUploadSize100 --server.headlessTrue--server.headlessTrue是必须的它禁用 Streamlit 的本地浏览器自动打开功能webbrowser.open()否则在 Heroku 上会报No module named tkinter错误。内存监控与优雅降级Heroku 会监控 dyno 内存超过 512MB 触发 OOM Kill。我们无法阻止用户上传大文件但可以提前拦截。我在app.py中加入内存预估import psutil import os def get_memory_usage(): process psutil.Process(os.getpid()) return process.memory_info().rss / 1024 / 1024 # MB if get_memory_usage() 400: st.warning(⚠️ 系统内存紧张部分高级分析功能已临时关闭) # 此处跳过耗内存的计算返回简化结果错误页面友好化默认的 Streamlit 报错页面对非技术人员极其不友好满屏 traceback。我用st.exception()包装关键区块并提供一键重试try: result_df heavy_computation(df, param_a, param_b) st.dataframe(result_df) except Exception as e: st.error(f 计算过程中出现意外请稍后重试。错误代码{type(e).__name__}) st.button( 重新计算, on_clicklambda: st.experimental_rerun())注意st.experimental_rerun()在 Streamlit 1.32 已更名为st.rerun()但 Heroku 上若用新版需确保requirements.txt中streamlit1.32.0。我目前统一用st.rerun()并锁定streamlit1.32.0。4. 实操过程与核心环节实现从空目录到可访问 URL 的逐行记录4.1 环境初始化本地开发机的最小必要配置我假设你已安装 Python 3.11推荐 3.11.8与 Heroku 当前默认 runtime 匹配并已注册 Heroku 账号。以下步骤在 macOS/Linux 终端或 Windows WSL 中执行# 1. 创建项目目录并进入 mkdir streamlit-heroku-demo cd streamlit-heroku-demo # 2. 创建虚拟环境强烈建议避免污染全局 Python python -m venv venv source venv/bin/activate # macOS/Linux # venv\Scripts\activate # Windows # 3. 安装 Streamlit仅本地开发用Heroku 会从 requirements.txt 重装 pip install streamlit1.32.0 # 4. 初始化 Git 仓库 git init此时目录结构为streamlit-heroku-demo/ ├── venv/ # 虚拟环境不提交到 Git └── .git/4.2 编写核心应用app.py一个真实的销售分析看板我们不写“Hello World”而是一个有真实业务逻辑的最小可行产品MVP销售漏斗分析器。它支持上传 CSV、选择日期范围、调整转化率权重、实时生成漏斗图。# app.py import streamlit as st import pandas as pd import numpy as np import plotly.express as px import os import io from datetime import datetime, timedelta # 页面配置 st.set_page_config( page_title销售漏斗分析器, page_icon, layoutwide, initial_sidebar_stateexpanded ) # 标题与说明 st.title( 销售漏斗分析器Heroku 部署版) st.markdown( 这是一个演示 Streamlit Heroku 全流程部署的最小可行应用。 ✅ 支持 CSV 文件上传 ✅ 动态日期范围筛选 ✅ 滑块调节转化率权重 ✅ 实时 Plotly 漏斗图 ⚠️ 注意Heroku Hobby dyno 有 30 分钟休眠首次访问需等待 5–10 秒冷启动。 ) # 环境变量检查 required_envs [DEMO_MODE] missing_envs [e for e in required_envs if not os.getenv(e)] if missing_envs: st.warning(f⚠️ 环境变量未设置: {, .join(missing_envs)}。将启用 DEMO 模式。) os.environ[DEMO_MODE] 1 # 数据加载逻辑 st.cache_data(ttl3600) def load_data(uploaded_fileNone): 加载数据优先用上传文件否则用内置 demo 数据 if uploaded_file is not None: try: if uploaded_file.name.endswith(.csv): df pd.read_csv(io.BytesIO(uploaded_file.getvalue())) else: df pd.read_excel(io.BytesIO(uploaded_file.getvalue())) st.success(f✅ 成功加载 {len(df)} 行数据) return df except Exception as e: st.error(f❌ 文件解析失败: {e}) return None else: # DEMO 模式生成模拟销售数据 np.random.seed(42) dates pd.date_range(2024-01-01, periods100, freqD) df pd.DataFrame({ date: np.random.choice(dates, 500), lead_id: range(1, 501), stage: np.random.choice([线索, 需求确认, 方案演示, 商务谈判, 赢单], 500, p[0.4, 0.25, 0.15, 0.12, 0.08]), amount: np.random.lognormal(12, 0.5, 500) # 金额对数正态分布 }) st.info(ℹ️ 当前处于 DEMO 模式使用模拟数据。请上传 CSV 文件以分析真实数据。) return df # 侧边栏控件 st.sidebar.header(⚙️ 分析设置) # 文件上传 uploaded_file st.sidebar.file_uploader( 上传销售数据 (CSV/Excel), type[csv, xlsx], help文件应包含 date, stage, amount 列 ) # 日期范围选择器仅当有数据时启用 df load_data(uploaded_file) if df is not None and not df.empty: min_date df[date].min() max_date df[date].max() date_range st.sidebar.date_input( 选择日期范围, value(min_date, max_date), min_valuemin_date, max_valuemax_date ) # 确保 date_range 是 tuple且长度为 2 if isinstance(date_range, tuple) and len(date_range) 2: start_date, end_date date_range df df[(df[date] pd.to_datetime(start_date)) (df[date] pd.to_datetime(end_date))] else: st.sidebar.warning(⚠️ 请选择起止日期) # 转化率权重滑块 weight st.sidebar.slider( 转化率权重影响漏斗宽度, min_value0.1, max_value2.0, value1.0, step0.1, help值越大高价值阶段如赢单在漏斗中占比越宽 ) # 主内容区 if df is not None and not df.empty: st.subheader( 数据概览) col1, col2, col3 st.columns(3) col1.metric(总线索数, len(df)) col2.metric(最早日期, df[date].min().strftime(%Y-%m-%d)) col3.metric(最新日期, df[date].max().strftime(%Y-%m-%d)) # 漏斗计算 stages [线索, 需求确认, 方案演示, 商务谈判, 赢单] funnel_data [] for stage in stages: count len(df[df[stage] stage]) amount df[df[stage] stage][amount].sum() funnel_data.append({ stage: stage, count: count, amount: amount }) funnel_df pd.DataFrame(funnel_data) # 应用权重仅影响视觉宽度不影响数值 funnel_df[weighted_count] funnel_df[count] * (weight ** np.arange(len(stages))) # 漏斗图 st.subheader( 销售漏斗图) fig px.funnel( funnel_df, xweighted_count, ystage, colorstage, color_discrete_sequencepx.colors.qualitative.Set3, titlef销售漏斗{start_date} 至 {end_date}, labels{weighted_count: 加权线索数, stage: 销售阶段} ) fig.update_layout(height500, showlegendFalse) st.plotly_chart(fig, use_container_widthTrue) # 详细表格 st.subheader( 各阶段明细) st.dataframe( funnel_df[[stage, count, amount]].style.format({ amount: ¥{:,.0f} }), use_container_widthTrue ) else: st.info( 请在左侧边栏上传文件或等待 DEMO 数据加载...)这段代码已经包含了前文提到的所有最佳实践环境变量检查、st.cache_data带ttl、BytesIO流式处理、st.rerun()友好错误处理、st.set_page_config优化体验。保存为app.py。4.3 构建 Heroku 所需的元文件requirements.txt、Procfile、runtime.txt现在我们需要告诉 Heroku 如何构建和运行这个应用。第一步生成requirements.txt在激活的虚拟环境中只安装应用真正依赖的包不包括streamlit的 dev 依赖# 确保在 venv 中 pip install pandas2.0.3 numpy1.24.3 plotly5.18.0 # 注意不要 pip install streamlit 这里我们手动写入 requirements.txt echo streamlit1.32.0 requirements.txt echo pandas2.0.3 requirements.txt echo numpy1.24.3 requirements.txt echo plotly5.18.0 requirements.txt echo python-dateutil2.8.2 requirements.txt echo --no-cache-dir requirements.txt第二步创建Procfile新建文件Procfile无后缀内容为web: streamlit run app.py --server.port$PORT --server.address0.0.0.0 --server.maxUploadSize100 --server.headlessTrue第三步创建runtime.txt新建文件runtime.txt内容为python-3.11.8提示Heroku 支持的 Python 版本列表见 https://devcenter.heroku.com/articles/python-runtimes。务必选择与你本地开发环境一致的版本避免ModuleNotFoundError。此时目录结构为streamlit-heroku-demo/ ├── app.py ├── requirements.txt ├── Procfile ├── runtime.txt ├── venv/ # 忽略 └── .git/4.4 Heroku CLI 配置与首次部署从零到 URL 的 7 分钟前提安装 Heroku CLI 并登录下载地址https://devcenter.heroku.com/articles/heroku-cli安装后在终端执行heroku login # 会打开浏览器让你授权完成后终端显示 Logged in as youremail.com部署四步曲创建 Heroku 应用命名唯一heroku create sales-funnel-analyzer-2024 # 如果名字已被占用换一个如 sales-funnel-xyz123 # 成功后会输出类似https://sales-funnel-analyzer-2024.herokuapp.com/ | https://git.heroku.com/sales-funnel-analyzer-2024.git设置环境变量启用 DEMO 模式heroku config:set DEMO_MODE1 # 如果你有真实 API这里设置heroku config:set DATA_API_URLhttps://...提交代码到 Heroku Git 远程git add . git commit -m feat: initial commit with Streamlit app and Heroku config git push heroku main # 注意Heroku 默认监听 main 分支不是 master查看日志确认启动成功heroku logs --tail你会看到类似输出2024-05-20T08:12:34.56789000:00 app[web.1]: Collecting streamlit1.32.0 2024-05-20T08:12:45.12345600:00 app[web.1]: Successfully installed streamlit-1.32.0 ... 2024-05-20T08:12:48.78901200:00 app[web.1]: Starting new Streamlit server... 2024-05-20T08:12:49.34567800:00 app[web.1]: You can now view your Streamlit app in your browser. 2024-05-20T08:12:49.34567800:00 app[web.1]: Network URL: http://0.0.0.0:20627 2024-05-20T08:12:49.34567800:00 app[web.1]: External URL: https://sales-funnel-analyzer-2024.herokuapp.com第五步打开 URL见证成果在浏览器中访问https://sales-funnel-analyzer-2024.herokuapp.com。首次访问会有 5–10 秒延迟冷启动之后页面将完全加载你可以上传 CSV、拖动滑块、切换日期——一切与本地streamlit run app.py无异。实测心得我部署这个 demo 应用的完整耗时是 6 分 42 秒。其中git push上传代码约 15 秒Heroku 构建install dependencies compile约 2 分 30 秒启动 Streamlit 进程约 3 秒首次访问冷启动约 8 秒。这个时间是可以预测和接受的远低于配置一台 EC2 并部署 Nginx 的 45 分钟。4.5 部署后验证与基础监控如何确认它真的“活”着部署完成不等于结束。你需要一套快速验证清单确保应用在 Heroku 上是健康、可访问、可交互的验证项操作方法预期结果不通过怎么办URL 可达性在 Incognito 窗口访问https://xxx.herokuapp.com显示 Streamlit 页面无Application Error检查heroku logs --tail看是否有ImportError或ModuleNotFoundError环境变量生效在页面底部找DEMO 模式提示显示 “当前处于 DEMO 模式”运行heroku config确认DEMO_MODE1已设置检查app.py中os.getenv(DEMO_MODE)是否被正确读取文件上传功能上传一个 5MB 的 CSV如test.csv页面显示✅ 成功加载 X 行数据漏斗图更新检查heroku logs --tail搜索FileNotFoundError或UnicodeDecodeError确认requirements.txt中pandas版本与 CSV 编码兼容冷启动延迟关闭所有标签页5 分钟后再访问 URL首次加载时间 ≤ 12 秒之后响应 1 秒这是 Hobby tier 正常现象无需处理。如超 30 秒检查Procfile中$PORT是否被正确注入HTTPS 强制跳转访问http://xxx.herokuapp.com自动 301 重定向到https://Heroku 默认开启无需配置。如未跳转检查Procfile是否遗漏--server.address0.0.0.0我通常会把这个清单做成一个 Markdown 文档放在项目根目录QA-CHECKLIST.md每次部署新版本都过一遍。这比靠记忆可靠得多。5. 常见问题与排查技巧实录那些让我凌晨三点爬起来的报错5.1 “Application Error” —— 最常见的黑屏其实只有三种原因当你访问 URL 只看到白底黑字的Application Error别慌。99% 的情况它只对应以下三种heroku logs报错模式。我按出现频率排序高频原因 1ModuleNotFoundError: No module named xxx这是requirements.txt不完整或版本冲突的直接体现。典型日志2024-05-20T08:12:49.34567800:00 app[web.1]: Traceback (most recent call last): 2024-05-20T08:12:49.34567800:00 app[web.1]: File app.py, line 5, in module 2024-05-20T08:12:49.34567800:00 app[web.1]: import plotly.express as px 2024-05-20T08:12