Streamlit与Snowflake集成构建实时云数据应用

4 阅读13分钟

引言:为何要将Streamlit连接到Snowflake

欢迎来到Streamlit教程系列的最后一课——在这里,本地原型将演进为云就绪的数据应用。

在前两课中,我们学习了如何使用静态CSV文件构建和扩展数据探索器。这对于学习Streamlit的响应式模型、缓存和UI控件来说非常完美。但在真实的分析工作流中,团队分析的不是某个人笔记本电脑上的CSV文件——他们连接到受管控的、实时的数据源,这些数据源可以跨用户和项目安全地扩展。

这就是Snowflake的价值所在。Snowflake是一个基于云的数据仓库,旨在处理海量数据集、实现安全共享,并提供极速的SQL查询。通过将其直接集成到Streamlit应用中,您将把基于文件的简单仪表板转变为一个动态的数据体验平台——一个能够实时与真实业务数据交互的平台。

在本课中,您将学习如何:

  • 使用环境变量或Streamlit内置的secrets.toml安全地配置Snowflake凭证。
  • 在专用模块(snowflake_utils.py)中创建可复用的连接和查询辅助函数。
  • 直接从Streamlit UI运行即席(第1部分)和参数化(第2部分)的SQL查询。
  • 在不离开应用的情况下分析、可视化和导出仓库数据。
  • 将远程的Snowflake表与本地数据混合,以获得混合洞察。
  • 应用轻量级缓存来加速重复查询并降低仓库成本。

到第2部分结束时,您将拥有一个功能完备的Streamlit + Snowflake仪表板,它能安全连接、运行实时查询、可视化模式并导出结果——所有这些都无需单独的后端或API。

这与您在构建内部分析门户、模型监控仪表板或由受管仓库数据驱动的MLOps(机器学习运维)管道时将使用的模式相同。

什么是Snowflake(以及它为何重要)

在深入代码之前,让我们快速了解一下是什么让Snowflake成为现代分析和机器学习管道的首选数据仓库。

其核心是,Snowflake是一个完全托管的云数据平台——这意味着您无需操心配置服务器、维护存储或扩展基础设施。它将计算(运行查询的“引擎”)与存储(数据所在的位置)分离开,允许您独立扩展它们。当您需要运行查询时,您启动一个虚拟仓库;当您完成后,可以暂停它以节省成本。

这种弹性使Snowflake非常适合需要处理大量、不可预测工作负载的应用,例如按需查询模型输出的Streamlit仪表板或MLOps工具。

一些关键优势:

  • 按需性能:查询根据负载自动扩展,即使在海量数据集上也能在几秒内返回结果。
  • 成本效益:仅在仓库激活时付费——暂停它即可立即停止计费。
  • 无缝集成:通过官方的 snowflake-connector-python 包和高级框架(如Snowpark)与Python原生协作。
  • 安全与治理:基于角色的访问控制(RBAC)、列级掩码和时间旅行等功能使其天生具备企业级水准。
  • 协作与共享:团队可以在部门或组织间安全地共享实时数据,而无需复制文件。

通过将Streamlit连接到Snowflake,您弥合了交互式数据应用与企业级数据基础设施之间的差距。您的应用不再依赖于静态上传——它针对一个可扩展的仓库运行实时SQL查询,检索受管控的结果,并立即显示给用户。

此对比凸显了为什么本地CSV工作流在规模扩大时会崩溃。Snowflake用受管控、弹性、实时的数据平台取代了手动文件处理,该平台支持安全协作、细粒度访问控制和成本效益分析。

配置您的开发环境

在编写任何代码之前,让我们确保您的环境已准备好连接Streamlit与Snowflake。您只需要几个工具和一个免费的Snowflake账户即可开始。

步骤1:创建Snowflake试用账户

如果您还没有,请访问Snowflake的免费试用页面并创建一个30天的账户。 登录后,打开Snowsight(Snowflake的现代化Web界面),您可以在其中运行SQL查询和管理计算资源。

步骤2:恢复或创建一个仓库

  1. 转到 管理仓库
  2. 查找默认仓库,例如 COMPUTE_WH。如果已暂停,点击恢复
  3. 如果不存在,创建一个新的XS(超小)仓库。
  4. 启用自动暂停(60秒)以避免不必要的积分消耗。

步骤3:确认数据库和模式 Snowflake试用账户包含一个示例数据库:SNOWFLAKE_SAMPLE_DATATPCH_SF1

TPCH_SF1 是基于TPC-H基准数据集(比例因子1)的Snowflake内置示例模式,通常用于演示和测试。您可以安全地查询这些表,而不会影响您的账户或产生大量成本。 在里面,您可以找到诸如 CUSTOMERORDERSLINEITEM 之类的表。 稍后我们将在Streamlit应用中使用这些表来运行测试查询。 在Snowsight中运行这个快速检查:

USE WAREHOUSE COMPUTE_WH;
USE DATABASE SNOWFLAKE_SAMPLE_DATA;
USE SCHEMA TPCH_SF1;
SELECT CURRENT_TIMESTAMP();

如果您看到一个UTC时间戳结果,说明您的仓库和模式已正确配置。

步骤4:安装依赖项 将以下包添加到您的 requirements.txt 文件中(如果尚不存在):

streamlit>=1.38,<2
pandas>=2.2,<3
snowflake-connector-python>=3.10,<4

然后安装它们:

pip install -r requirements.txt

步骤5:配置Snowflake凭证 您可以将凭证存储为环境变量,或者更方便地,存储在Streamlit内置的secrets文件中。 在项目根目录创建一个名为 .streamlit/secrets.toml 的文件:

[snowflake]
user = "您的用户名"
password = "您的密码"
account = "您的账户标识符"
warehouse = "COMPUTE_WH"
database = "SNOWFLAKE_SAMPLE_DATA"
schema = "TPCH_SF1"
role = "ACCOUNTADMIN"

注意:切勿将此文件提交到GitHub。它应在您的本地系统上保持私密。 一旦您的Snowflake试用账户、依赖项和凭证准备就绪,您就可以继续了。 接下来,让我们看看我们的辅助模块(即 config.pysnowflake_utils.py)如何在幕后安全地处理连接逻辑。

项目结构回顾:为Snowflake扩展基础

到本课结束时,您的Streamlit项目将从一个简单的原型演变为一个能够查询云仓库的实时数据应用。其结构保持熟悉——模块化、组织良好且易于扩展——但现在包含了用于安全凭证处理和数据库连接的工具。 以下是完整的目录布局,新组件已高亮显示:

streamlit_project/
├── lesson1_main.py
├── lesson2_main.py
├── lesson3_main.py              ← 新增:连接到Snowflake
├── pyimagesearch/
│   ├── __init__.py
│   ├── config.py                ← 现在加载Snowflake凭证
│   ├── data_loader.py
│   ├── visualization.py
│   ├── snowflake_utils.py       ← 新增:用于仓库查询的辅助函数
├── data/
│   └── iris_sample.csv
├── .streamlit/
│   ├── config.toml              ← 应用主题和布局
│   └── secrets.toml             ← 安全存储Snowflake凭证
├── requirements.txt

本课中新增和更新的模块:

  • pyimagesearch/snowflake_utils.py:使用官方的Snowflake连接器封装身份验证和查询执行逻辑。
  • pyimagesearch/config.py:已更新,可从st.secrets或环境变量读取凭证,并暴露一个snowflake_enabled标志。
  • lesson3_main.py:您的主Streamlit应用,它连接到Snowflake、运行SQL查询、可视化数据、混合本地和远程数据集,并演示缓存(第2部分)。
  • .streamlit/secrets.toml:存放您用于本地开发的私有Snowflake凭证。

项目的其余部分(即 data_loader.pyvisualization.py__init__.py)与之前课程保持不变,强化了贯穿整个系列的一致结构和可重用性。

理解辅助模块

在我们连接Streamlit到Snowflake之前,让我们先了解一下两个默默完成大部分繁重工作的核心模块(即 config.pysnowflake_utils.py)。 这些模块负责处理凭证加载、创建数据库连接,并将查询结果作为Pandas DataFrame返回以供可视化。现在就熟悉它们将使本课其余部分的学习变得轻松。

config.py:管理配置和凭证

我们从 config.py 开始。这个文件定义了本系列每一课都依赖的全局配置。它小巧、简单且非常有用。 文件顶部以几个导入开始,奠定了基石:

from dataclasses import dataclass
import os
from typing import Optional

try:
    import streamlit as st
except Exception:
    st = None

try:
    from dotenv import load_dotenv  # type: ignore
    load_dotenv()
except Exception:
    pass

这里发生了什么: 我们导入Python内置的dataclass,以便用最少的样板代码定义一个配置容器。os模块用于读取环境变量,而typing中的Optional允许我们定义可能设置也可能不设置的字段——这对于有时可以省略的凭证(例如角色或模式)来说很方便。 然后我们进行两个可选导入。 首先是Streamlit:包装在try块中,以防止在未安装Streamlit时发生导入时错误(例如,在Streamlit运行时之外运行实用程序测试或课程脚本时)。如果导入失败,我们简单地将st = None并继续。 接下来是dotenv的可选导入。如果安装了python-dotenv包,它会从本地的.env文件加载环境变量。这主要是为了方便——开发人员在本地实验时经常将凭证放在那里。没有它,脚本仍然可以正常工作。 现在我们到达文件的核心:Settings数据类。

@dataclass(frozen=True)
class Settings:
    app_name: str = "Streamlit Tutorial Series"
    default_sample_path: str = os.path.join("data", "iris_sample.csv")

    snowflake_user: Optional[str] = None
    snowflake_password: Optional[str] = None
    snowflake_account: Optional[str] = None
    snowflake_role: Optional[str] = None
    snowflake_warehouse: Optional[str] = None
    snowflake_database: Optional[str] = None
    snowflake_schema: Optional[str] = None

此类将所有配置集中在一个地方。frozen=True标志意味着一旦创建了settings对象,其任何值都不能被更改——当Streamlit自动重新运行脚本时,这是一个重要的安全措施。 顶部是通用的应用级变量(例如app_name和默认的Iris数据集路径)。其余的是Snowflake凭证,初始为None,并会被动态填充。 在类内部,有一个名为 __post_init__ 的特殊方法,它在初始化之后立即运行。这就是真正魔法发生的地方。

    def __post_init__(self):
        """从Streamlit secrets或环境变量加载Snowflake凭证。"""
        # 首先从Streamlit secrets加载
        snowflake_secrets = {}
        if st is not None:
            try:
                snowflake_secrets = st.secrets["snowflake"]
            except Exception:
                pass

当Streamlit可用时,它首先查找存储在 st.secrets["snowflake"] 中的凭证,这是Streamlit Cloud安全管理secrets的方式。如果找不到这些,它会回退到从环境变量(例如 SNOWFLAKE_USERSNOWFLAKE_ACCOUNT 等)读取。 这种分层方法旨在同时支持云环境和本地开发环境。

        # 如果secrets缺失,回退到环境变量
        def get_value(key: str, env_key: str) -> Optional[str]:
            return snowflake_secrets.get(key) or os.getenv(env_key)

接下来是一个名为 get_value() 的小辅助函数。 它的目的很简单:首先尝试从Streamlit secrets获取一个值,如果失败,则回退到相应的环境变量。这种模式使配置具有可移植性——无论是在云端部署还是在本地机器上测试,它都以相同的方式工作。

        object.__setattr__(self, "snowflake_user", get_value("user", "SNOWFLAKE_USER"))
        object.__setattr__(self, "snowflake_password", get_value("password", "SNOWFLAKE_PASSWORD"))
        object.__setattr__(self, "snowflake_account", get_value("account", "SNOWFLAKE_ACCOUNT"))
        object.__setattr__(self, "snowflake_role", get_value("role", "SNOWFLAKE_ROLE"))
        object.__setattr__(self, "snowflake_warehouse", get_value("warehouse", "SNOWFLAKE_WAREHOUSE"))
        object.__setattr__(self, "snowflake_database", get_value("database", "SNOWFLAKE_DATABASE"))
        object.__setattr__(self, "snowflake_schema", get_value("schema", "SNOWFLAKE_SCHEMA"))

有了 get_value() 辅助函数,我们现在可以在冻结的数据类中填充所有Snowflake字段。因为 Settings 是不可变的,不允许常规赋值——相反,代码在 __post_init__ 作用域内使用 object.__setattr__() 来安全地设置每个属性。 这部分是配置系统的真正核心。它确保即使 Settings 对象是冻结的,您的所有凭证也仅在其创建时被加载一次,并在应用的整个生命周期中保持不变。Streamlit的重新运行模型不会重置或改变这些值,使您的环境可预测并减少意外修改。 在文件末尾,还有一个有用的属性:snowflake_enabled

    @property
    def snowflake_enabled(self) -> bool:
        """如果所有必需的Snowflake凭证都可用,则返回True。"""
        required = [
            self.snowflake_user,
            self.snowflake_password,
            self.snowflake_account,
            self.snowflake_warehouse,
            self.snowflake_database,
            self.snowflake_schema,
        ]
        return all(bool(x) for x in required)

此属性旨在由应用用来决定是否尝试实时的Snowflake连接。 最后,在底部,文件创建了此数据类的单个全局实例:

settings = Settings()

这样,每个脚本都可以通过一行代码导入相同的配置:

from pyimagesearch import settings

并立即访问诸如以下的值:

settings.snowflake_user
settings.snowflake_enabled

snowflake_utils.py:处理连接和查询逻辑

现在凭证已加载,我们需要一种方法来连接到Snowflake并运行查询。这就是 snowflake_utils.py 的作用所在。 像 config.py 一样,它从一些关键的导入开始:

from dataclasses import dataclass
from typing import Optional
import pandas as pd

try:
    import snowflake.connector  # type: ignore
except Exception:
    snowflake = None

这里的导入体现了相同的理念:最小化、有目的且安全。 我们再次使用 dataclass 来清晰地构建我们的凭证。Optional 让我们可以将角色参数标记为非必需的。Pandas被使用,因为Snowflake连接器可以直接将查询结果作为Pandas DataFrame返回,这对于Streamlit来说非常完美。 最后,围绕 snowflake.connectortry/except 可以防止在尚未安装Snowflake依赖项的环境中发生导入错误。它不是让整个应用崩溃,而是简单地将 snowflake = None。稍后,应用可以捕获这个错误并优雅地通知用户。 接下来是 SnowflakeCredentials 数据类:

@dataclass
class SnowflakeCredentials:
    user: str
    password: str
    account: str
    warehouse: str
    database: str
    schema: str
    role: Optional[str] = None

这个类是一个简洁的容器,包含了连接到Snowflake所需的一切。没有它,我们将不得不在每个连接调用中传递7个参数。该数据类将它们整齐地分组,并为清晰起见提供了类型提示。 该模块真正的明星是 run_query 函数:

def run_query(creds: SnowflakeCredentials, query: str) -> pd.DataFrame:
    if "snowflake" not in globals() or snowflake is None:  # type: ignore
        raise RuntimeError("未安装 snowflake-connector-python。")
    ctx = snowflake.connector.connect(
        user=creds.user,
        password=creds.password,
        account=creds.account,
        warehouse=creds.warehouse,
        database=creds.database,
        schema=creds.schema,
        role=creds.role,
    )
    try:
        cs = ctx.cursor()
        try:
            cs.execute(query)
            df = cs.fetch_pandas_all()
        finally:
            cs.close()
    finally:
        ctx.close()
    return df

让我们一步步来看。 函数首先快速检查Snowflake连接器是否可用。如果没有,它会引发一个清晰的错误,而不是在稍后的连接过程中失败。 连接本身很直接:它使用存储在 SnowflakeCredentials 数据类中的凭证来创建一个上下文(ctx)。在该上下文中,一个游标执行SQL查询。数据被检索后,使用 fetch_pandas_all() 直接提取到一个Pandas DataFrame中,这是Snowflake Python连接器最方便的特性之一。 最后,游标和连接都在 finally 块中关闭——这确保了即使在查询过程中出现问题,资源也能被正确清理。 总之,config.pysnowflake_utils.py 构成了一个小而强大的基础。 前者确保凭证被安全且可预测地加载。后者提供了一种干净、可重用的方式来查询Snowflake,而无需将连接逻辑分散在您的代码库中。 接下来,我们将通过构建我们的Snowflake连接的Streamlit应用来使这些模块发挥作用,从如何检查连接和运行第一个实时查询开始。 本课的其余内容将在第2部分中完成。FINISHED