在機器學習與資料科學領域,模型的價值最終體現在實際應用中。然而,將訓練好的模型轉換成可供使用者操作的應用程式,傳統上需要具備前端開發、後端架構甚至 DevOps 的知識,這對許多專注於演算法與模型開發的資料科學家而言是一道不小的門檻。Streamlit 的出現徹底改變了這個局面,它讓開發者能夠純粹使用 Python 程式碼,在數分鐘內建立出功能完整的互動式網頁應用程式。

Streamlit 的設計理念是「將腳本轉換成應用程式」。開發者只需要撰寫一般的 Python 腳本,加入 Streamlit 提供的元件,就能自動生成對應的網頁介面。這種開發方式不僅大幅降低了技術門檻,也讓開發週期從數週縮短到數小時。無論是資料探索、模型展示、原型驗證還是內部工具開發,Streamlit 都能提供快速而有效的解決方案。

本文將從 Streamlit 的基礎概念開始,逐步深入到各種進階功能的實作。我們會介紹核心元件的使用方式、資料快取機制、視覺化工具整合、表單處理、狀態管理以及多頁面應用架構。最後,我們會以一個完整的命名實體識別應用作為案例,展示如何將機器學習模型整合到 Streamlit 應用中,並介紹部署策略,讓讀者能夠將自己的應用程式公開分享。

Streamlit 基礎與環境設定

Streamlit 是一個開源的 Python 框架,專為資料科學家和機器學習工程師設計。它的核心優勢在於簡潔的 API 設計,讓開發者能夠以最少的程式碼建立功能豐富的網頁應用程式。與傳統的網頁開發框架不同,Streamlit 不需要開發者學習 HTML、CSS 或 JavaScript,所有的介面元素都透過 Python 函式呼叫來定義。

要開始使用 Streamlit,首先需要安裝相關套件並設定開發環境。建議使用虛擬環境來隔離專案依賴,以避免套件版本衝突。

# 建立虛擬環境並安裝 Streamlit
# 在終端機執行以下命令

# 建立虛擬環境
# python -m venv streamlit_env

# 啟動虛擬環境 (Windows)
# streamlit_env\Scripts\activate

# 啟動虛擬環境 (macOS/Linux)
# source streamlit_env/bin/activate

# 安裝 Streamlit 及相關套件
# pip install streamlit pandas numpy matplotlib plotly scikit-learn

# 安裝 NLP 相關套件
# pip install spacy
# python -m spacy download en_core_web_lg
# python -m spacy download zh_core_web_lg

安裝完成後,讓我們建立第一個 Streamlit 應用程式來驗證環境設定是否正確。

# app.py
# 第一個 Streamlit 應用程式

import streamlit as st  # 匯入 Streamlit 套件

# 設定頁面配置
# 這應該是程式中第一個 Streamlit 命令
st.set_page_config(
    page_title="我的第一個 Streamlit 應用",  # 瀏覽器標籤頁標題
    page_icon="🚀",                          # 瀏覽器標籤頁圖示
    layout="wide",                            # 頁面布局:wide 或 centered
    initial_sidebar_state="expanded"          # 側邊欄初始狀態
)

# 應用程式標題
st.title("🎯 歡迎使用 Streamlit")

# 副標題
st.header("這是一個示範應用程式")

# 一般文字
st.write("Streamlit 讓你能夠快速建立資料應用程式。")

# Markdown 格式文字
st.markdown("""
### 主要特色
- **簡單易用**:只需要 Python 就能建立網頁應用
- **即時更新**:儲存檔案後應用程式自動重新載入
- **豐富元件**:提供各種互動式元件和視覺化工具
""")

# 顯示程式碼區塊
st.code("""
import streamlit as st
st.write("Hello, Streamlit!")
""", language="python")

# 分隔線
st.divider()

# 資訊提示框
st.info("這是一個資訊提示框")
st.success("操作成功完成")
st.warning("請注意這個警告訊息")
st.error("發生錯誤")

要執行這個應用程式,在終端機中切換到應用程式所在目錄,然後執行 streamlit run 命令。

# 在終端機執行應用程式
# streamlit run app.py

# 預設會在 http://localhost:8501 啟動應用程式
# 瀏覽器會自動開啟顯示應用程式

# 開發模式下,每次儲存檔案都會自動重新載入應用程式
# 也可以在應用程式右上角點選 "Rerun" 手動重新載入

互動式元件與使用者輸入

Streamlit 提供了豐富的互動式元件,讓使用者能夠與應用程式進行互動。這些元件包括按鈕、滑桿、文字輸入、選擇器、日期選擇等,每個元件都會返回使用者輸入的值,開發者可以根據這些值來更新應用程式的顯示內容。

# interactive_components.py
# 互動式元件示範

import streamlit as st
import pandas as pd
import numpy as np
from datetime import datetime, date

st.title("🎛️ 互動式元件示範")

# === 基本輸入元件 ===
st.header("基本輸入元件")

# 文字輸入
name = st.text_input(
    "請輸入您的名字",          # 標籤
    placeholder="在此輸入..."  # 佔位文字
)
if name:
    st.write(f"你好,{name}!")

# 數字輸入
age = st.number_input(
    "請輸入您的年齡",
    min_value=0,      # 最小值
    max_value=150,    # 最大值
    value=25,         # 預設值
    step=1            # 增減步長
)

# 文字區域(多行輸入)
feedback = st.text_area(
    "請輸入您的意見回饋",
    height=150,
    max_chars=500     # 最大字元數
)

# === 選擇元件 ===
st.header("選擇元件")

# 選擇框(下拉選單)
programming_language = st.selectbox(
    "您最喜歡的程式語言?",
    options=["Python", "JavaScript", "Java", "C++", "Go", "Rust"],
    index=0  # 預設選中的索引
)

# 多選框
hobbies = st.multiselect(
    "您的興趣?",
    options=["閱讀", "運動", "音樂", "旅遊", "電影", "烹飪"],
    default=["閱讀"]  # 預設選中的項目
)

# 單選按鈕
experience_level = st.radio(
    "您的經驗等級?",
    options=["初學者", "中級", "進階", "專家"],
    horizontal=True   # 水平排列
)

# === 滑桿元件 ===
st.header("滑桿元件")

# 單一值滑桿
temperature = st.slider(
    "選擇溫度",
    min_value=0.0,
    max_value=100.0,
    value=50.0,
    step=0.1,
    format="%.1f°C"  # 顯示格式
)

# 範圍滑桿
age_range = st.slider(
    "選擇年齡範圍",
    min_value=0,
    max_value=100,
    value=(20, 40)   # 元組表示範圍
)
st.write(f"選擇的範圍:{age_range[0]}{age_range[1]} 歲")

# === 日期與時間元件 ===
st.header("日期與時間元件")

# 日期選擇器
selected_date = st.date_input(
    "選擇日期",
    value=date.today(),
    min_value=date(2000, 1, 1),
    max_value=date(2030, 12, 31)
)

# 時間選擇器
selected_time = st.time_input(
    "選擇時間",
    value=datetime.now().time()
)

# === 核取方塊與切換 ===
st.header("核取方塊與切換")

# 核取方塊
agree = st.checkbox("我同意使用條款")
if agree:
    st.success("感謝您的同意!")

# 切換開關
dark_mode = st.toggle("啟用深色模式")
if dark_mode:
    st.write("深色模式已啟用")

# === 按鈕元件 ===
st.header("按鈕元件")

col1, col2, col3 = st.columns(3)

with col1:
    if st.button("主要按鈕", type="primary"):
        st.write("主要按鈕被點擊")

with col2:
    if st.button("次要按鈕"):
        st.write("次要按鈕被點擊")

with col3:
    # 下載按鈕
    sample_data = pd.DataFrame({
        'A': [1, 2, 3],
        'B': [4, 5, 6]
    })
    st.download_button(
        label="下載資料",
        data=sample_data.to_csv(index=False),
        file_name="sample_data.csv",
        mime="text/csv"
    )

資料顯示與視覺化

Streamlit 提供了多種方式來顯示資料和建立視覺化圖表。它原生支援 Pandas DataFrame 的顯示,並且可以整合 Matplotlib、Plotly、Altair 等主流視覺化函式庫。

# data_visualization.py
# 資料顯示與視覺化示範

import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go

st.title("📊 資料視覺化示範")

# === 建立示範資料 ===
@st.cache_data  # 快取資料,避免重複計算
def create_sample_data():
    # 建立時間序列資料
    np.random.seed(42)
    dates = pd.date_range('2023-01-01', periods=100, freq='D')
    data = pd.DataFrame({
        '日期': dates,
        '銷售額': np.random.randint(1000, 5000, 100),
        '訂單數': np.random.randint(10, 100, 100),
        '類別': np.random.choice(['電子產品', '服飾', '食品', '家具'], 100),
        '地區': np.random.choice(['北部', '中部', '南部', '東部'], 100)
    })
    return data

data = create_sample_data()

# === DataFrame 顯示 ===
st.header("資料表格顯示")

# 靜態表格
st.subheader("靜態表格")
st.dataframe(
    data.head(10),
    use_container_width=True,  # 使用容器寬度
    hide_index=True             # 隱藏索引
)

# 可編輯表格
st.subheader("可編輯表格")
edited_data = st.data_editor(
    data.head(5),
    num_rows="dynamic",        # 允許新增或刪除列
    use_container_width=True
)

# 指標顯示
st.subheader("關鍵指標")
col1, col2, col3, col4 = st.columns(4)

with col1:
    st.metric(
        label="總銷售額",
        value=f"${data['銷售額'].sum():,.0f}",
        delta=f"{np.random.randint(-10, 20)}%"
    )

with col2:
    st.metric(
        label="平均訂單數",
        value=f"{data['訂單數'].mean():.1f}",
        delta=f"{np.random.randint(-5, 10)}%"
    )

with col3:
    st.metric(
        label="總訂單數",
        value=f"{data['訂單數'].sum():,}",
        delta=f"{np.random.randint(-5, 15)}%"
    )

with col4:
    st.metric(
        label="資料筆數",
        value=len(data),
        delta=None
    )

# === Streamlit 原生圖表 ===
st.header("Streamlit 原生圖表")

# 折線圖
st.subheader("折線圖")
chart_data = data.set_index('日期')[['銷售額', '訂單數']]
st.line_chart(chart_data)

# 面積圖
st.subheader("面積圖")
st.area_chart(chart_data)

# 長條圖
st.subheader("長條圖")
bar_data = data.groupby('類別')['銷售額'].sum()
st.bar_chart(bar_data)

# === Plotly 互動式圖表 ===
st.header("Plotly 互動式圖表")

# 散點圖
st.subheader("散點圖:銷售額 vs 訂單數")
fig_scatter = px.scatter(
    data,
    x='訂單數',
    y='銷售額',
    color='類別',
    size='銷售額',
    hover_data=['日期', '地區'],
    title='銷售額與訂單數關係'
)
st.plotly_chart(fig_scatter, use_container_width=True)

# 長條圖(依類別分組)
st.subheader("各類別銷售分析")
category_sales = data.groupby(['類別', '地區'])['銷售額'].sum().reset_index()
fig_bar = px.bar(
    category_sales,
    x='類別',
    y='銷售額',
    color='地區',
    barmode='group',
    title='各類別各地區銷售額'
)
st.plotly_chart(fig_bar, use_container_width=True)

# 圓餅圖
st.subheader("類別佔比")
category_total = data.groupby('類別')['銷售額'].sum().reset_index()
fig_pie = px.pie(
    category_total,
    values='銷售額',
    names='類別',
    title='各類別銷售額佔比'
)
st.plotly_chart(fig_pie, use_container_width=True)

# 時間序列圖
st.subheader("銷售趨勢")
daily_sales = data.groupby('日期')['銷售額'].sum().reset_index()
fig_line = px.line(
    daily_sales,
    x='日期',
    y='銷售額',
    title='每日銷售額趨勢'
)
fig_line.update_xaxes(rangeslider_visible=True)  # 新增範圍滑桿
st.plotly_chart(fig_line, use_container_width=True)

# 熱力圖
st.subheader("銷售熱力圖")
pivot_data = data.pivot_table(
    values='銷售額',
    index='類別',
    columns='地區',
    aggfunc='sum'
)
fig_heatmap = px.imshow(
    pivot_data,
    labels=dict(x="地區", y="類別", color="銷售額"),
    title='類別與地區銷售熱力圖',
    color_continuous_scale='Viridis'
)
st.plotly_chart(fig_heatmap, use_container_width=True)
@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

title Streamlit 應用程式架構

rectangle "使用者介面" as ui {
    card "互動元件" as input
    card "資料顯示" as display
    card "視覺化圖表" as chart
}

rectangle "應用程式邏輯" as logic {
    card "資料處理" as process
    card "模型推論" as model
    card "快取管理" as cache
}

rectangle "資料來源" as data {
    card "檔案系統" as file
    card "資料庫" as db
    card "API" as api
}

input --> process
process --> cache
cache --> display
process --> model
model --> chart

file --> process
db --> process
api --> process

note right of cache
    使用 @st.cache_data
    和 @st.cache_resource
    提升效能
end note

@enduml

快取機制與效能最佳化

Streamlit 的快取機制是其效能最佳化的核心功能。由於 Streamlit 會在每次使用者互動時重新執行整個腳本,若沒有適當的快取,載入大型資料集或執行複雜計算會導致應用程式變得緩慢。Streamlit 提供了兩個主要的快取裝飾器:st.cache_data 用於快取資料,st.cache_resource 用於快取全域資源如模型或資料庫連線。

# caching_demo.py
# 快取機制示範

import streamlit as st
import pandas as pd
import numpy as np
import time

st.title("⚡ 快取機制示範")

# === @st.cache_data:快取資料 ===
st.header("資料快取 (@st.cache_data)")

@st.cache_data  # 快取傳回的資料
def load_large_dataset(rows):
    """
    模擬載入大型資料集
    @st.cache_data 會快取傳回值,當函式參數相同時直接傳回快取結果
    """
    # 模擬耗時的資料載入過程
    time.sleep(2)  # 暫停 2 秒模擬 I/O 操作

    # 產生隨機資料
    data = pd.DataFrame({
        'id': range(rows),
        'value': np.random.randn(rows),
        'category': np.random.choice(['A', 'B', 'C'], rows)
    })
    return data

# 使用滑桿選擇資料列數
row_count = st.slider("選擇資料列數", 1000, 100000, 10000, 1000)

# 載入資料(第一次會耗時 2 秒,之後會直接使用快取)
start_time = time.time()
data = load_large_dataset(row_count)
elapsed_time = time.time() - start_time

st.write(f"載入時間:{elapsed_time:.3f} 秒")
st.write(f"資料形狀:{data.shape}")
st.dataframe(data.head())

# === @st.cache_data 進階用法 ===
st.header("快取進階設定")

@st.cache_data(
    ttl=3600,           # 快取存活時間(秒)
    max_entries=10,     # 最大快取條目數
    show_spinner=True   # 顯示載入動畫
)
def process_data_with_ttl(data):
    """
    有時效限制的快取
    適用於需要定期更新的資料
    """
    time.sleep(1)
    return data.describe()

# === @st.cache_resource:快取資源 ===
st.header("資源快取 (@st.cache_resource)")

@st.cache_resource  # 快取全域資源
def load_ml_model():
    """
    載入機器學習模型
    @st.cache_resource 適用於需要跨所有使用者共享的資源
    例如:ML 模型、資料庫連線、外部 API 客戶端
    """
    # 模擬載入模型
    time.sleep(3)

    # 這裡示範傳回一個模擬的模型物件
    class MockModel:
        def predict(self, x):
            return np.random.randn(len(x))

    return MockModel()

# 載入模型
if st.button("載入機器學習模型"):
    start_time = time.time()
    model = load_ml_model()
    elapsed_time = time.time() - start_time
    st.success(f"模型載入完成!耗時:{elapsed_time:.3f} 秒")

# === 清除快取 ===
st.header("快取管理")

col1, col2 = st.columns(2)

with col1:
    if st.button("清除資料快取"):
        st.cache_data.clear()
        st.success("資料快取已清除")

with col2:
    if st.button("清除資源快取"):
        st.cache_resource.clear()
        st.success("資源快取已清除")

# === 快取使用建議 ===
st.header("快取最佳實踐")

st.markdown("""
### @st.cache_data 適用情境
- 載入 DataFrame(CSV、Parquet、資料庫查詢)
- 資料轉換和處理
- 產生視覺化圖表
- API 呼叫結果

### @st.cache_resource 適用情境
- 機器學習模型
- 資料庫連線
- 外部 API 客戶端
- 全域設定物件

### 注意事項
- 避免快取會改變的物件
- 適當設定 TTL 避免過期資料
- 監控快取大小避免記憶體問題
""")

側邊欄與頁面布局

Streamlit 提供了靈活的頁面布局選項,包括側邊欄、多欄布局、展開面板和分頁等。良好的布局設計可以提升使用者體驗,讓應用程式更加直觀易用。

# layout_demo.py
# 頁面布局示範

import streamlit as st
import pandas as pd
import numpy as np

st.set_page_config(layout="wide")  # 使用寬版布局

st.title("📐 頁面布局示範")

# === 側邊欄 ===
with st.sidebar:
    st.header("⚙️ 設定面板")

    # 側邊欄元件
    dataset_size = st.selectbox(
        "資料集大小",
        ["小型 (100)", "中型 (1000)", "大型 (10000)"]
    )

    chart_type = st.radio(
        "圖表類型",
        ["折線圖", "長條圖", "散點圖"]
    )

    show_raw_data = st.checkbox("顯示原始資料", value=False)

    color_theme = st.color_picker("選擇主題色", "#1f77b4")

    st.divider()

    st.markdown("### 關於此應用")
    st.info("這是一個 Streamlit 布局示範應用程式。")

# === 多欄布局 ===
st.header("多欄布局")

# 兩欄布局(相等寬度)
col1, col2 = st.columns(2)

with col1:
    st.subheader("左側欄位")
    st.write("這是左側的內容區域")
    st.metric("指標 A", "123", "5%")

with col2:
    st.subheader("右側欄位")
    st.write("這是右側的內容區域")
    st.metric("指標 B", "456", "-3%")

# 三欄布局(不等寬度)
st.subheader("不等寬度多欄")
col_a, col_b, col_c = st.columns([2, 1, 1])  # 2:1:1 比例

with col_a:
    st.write("寬欄(佔 50%)")
    chart_data = pd.DataFrame(
        np.random.randn(20, 3),
        columns=['A', 'B', 'C']
    )
    st.line_chart(chart_data)

with col_b:
    st.write("窄欄 1(佔 25%)")
    st.metric("數值", "789")

with col_c:
    st.write("窄欄 2(佔 25%)")
    st.metric("數值", "012")

# === 展開面板 ===
st.header("展開面板 (Expander)")

with st.expander("點擊展開詳細資訊"):
    st.write("這裡是隱藏的詳細內容")
    st.code("""
    def example_function():
        return "Hello, World!"
    """)
    st.image("https://streamlit.io/images/brand/streamlit-logo-primary-colormark-darktext.png", width=200)

with st.expander("進階設定", expanded=False):
    advanced_option_1 = st.slider("進階選項 1", 0, 100, 50)
    advanced_option_2 = st.text_input("進階選項 2")

# === 分頁 ===
st.header("分頁 (Tabs)")

tab1, tab2, tab3 = st.tabs(["📈 圖表", "📋 資料", "⚙️ 設定"])

with tab1:
    st.subheader("圖表分頁")
    st.line_chart(np.random.randn(50, 3))

with tab2:
    st.subheader("資料分頁")
    st.dataframe(pd.DataFrame({
        '欄位 A': range(10),
        '欄位 B': np.random.randn(10)
    }))

with tab3:
    st.subheader("設定分頁")
    st.text_input("設定項目 1")
    st.number_input("設定項目 2", 0, 100, 50)

# === 容器 ===
st.header("容器 (Container)")

container = st.container(border=True)
container.write("這是一個有邊框的容器")
container.line_chart(np.random.randn(20))

# 空白容器(稍後填入內容)
placeholder = st.empty()

if st.button("填入內容"):
    placeholder.success("內容已填入!")

表單與狀態管理

在許多應用場景中,我們希望使用者能夠一次填寫多個欄位,然後統一提交,而不是每次輸入都觸發應用程式重新執行。Streamlit 的表單元件和狀態管理功能可以滿足這些需求。

# forms_and_state.py
# 表單與狀態管理示範

import streamlit as st
import pandas as pd

st.title("📝 表單與狀態管理")

# === 表單 ===
st.header("表單 (Form)")

# 使用表單避免每次輸入都重新執行
with st.form("user_registration"):
    st.subheader("使用者註冊表單")

    # 表單內的元件不會立即觸發重新執行
    username = st.text_input("使用者名稱")
    email = st.text_input("電子郵件")
    password = st.text_input("密碼", type="password")

    col1, col2 = st.columns(2)
    with col1:
        birth_date = st.date_input("出生日期")
    with col2:
        country = st.selectbox("國家", ["台灣", "日本", "美國", "其他"])

    interests = st.multiselect(
        "興趣",
        ["程式設計", "資料科學", "機器學習", "網頁開發"]
    )

    agree = st.checkbox("我同意服務條款")

    # 表單提交按鈕
    submitted = st.form_submit_button("註冊", type="primary")

    if submitted:
        if not username or not email:
            st.error("請填寫所有必填欄位")
        elif not agree:
            st.error("請同意服務條款")
        else:
            st.success(f"註冊成功!歡迎 {username}")
            st.json({
                "username": username,
                "email": email,
                "birth_date": str(birth_date),
                "country": country,
                "interests": interests
            })

# === Session State ===
st.header("Session State 狀態管理")

# 初始化 session state
if 'counter' not in st.session_state:
    st.session_state.counter = 0

if 'items' not in st.session_state:
    st.session_state.items = []

# 計數器範例
st.subheader("計數器")
col1, col2, col3 = st.columns(3)

with col1:
    if st.button("增加"):
        st.session_state.counter += 1

with col2:
    if st.button("減少"):
        st.session_state.counter -= 1

with col3:
    if st.button("重置"):
        st.session_state.counter = 0

st.metric("目前計數", st.session_state.counter)

# 待辦事項清單範例
st.subheader("待辦事項清單")

# 新增項目
new_item = st.text_input("新增待辦事項", key="new_todo")
if st.button("新增") and new_item:
    st.session_state.items.append({
        "text": new_item,
        "done": False
    })
    st.rerun()  # 重新執行以更新顯示

# 顯示清單
for i, item in enumerate(st.session_state.items):
    col1, col2 = st.columns([4, 1])
    with col1:
        checked = st.checkbox(
            item["text"],
            value=item["done"],
            key=f"item_{i}"
        )
        st.session_state.items[i]["done"] = checked
    with col2:
        if st.button("刪除", key=f"delete_{i}"):
            st.session_state.items.pop(i)
            st.rerun()

# 顯示統計
if st.session_state.items:
    total = len(st.session_state.items)
    done = sum(1 for item in st.session_state.items if item["done"])
    st.progress(done / total if total > 0 else 0)
    st.write(f"完成進度:{done}/{total}")

# === 回呼函式 ===
st.header("回呼函式 (Callbacks)")

def on_slider_change():
    """當滑桿值改變時執行"""
    st.session_state.slider_message = f"滑桿值已更改為 {st.session_state.my_slider}"

st.slider(
    "滑動我",
    0, 100, 50,
    key="my_slider",
    on_change=on_slider_change
)

if 'slider_message' in st.session_state:
    st.info(st.session_state.slider_message)

檔案上傳與處理

Streamlit 支援檔案上傳功能,讓使用者能夠上傳資料檔案進行分析。這對於建立資料分析工具或機器學習應用特別有用。

# file_upload.py
# 檔案上傳與處理示範

import streamlit as st
import pandas as pd
import io

st.title("📁 檔案上傳與處理")

# === 單一檔案上傳 ===
st.header("單一檔案上傳")

uploaded_file = st.file_uploader(
    "上傳 CSV 檔案",
    type=['csv', 'xlsx'],                    # 允許的檔案類型
    help="請上傳 CSV 或 Excel 檔案"
)

if uploaded_file is not None:
    # 顯示檔案資訊
    st.write("檔案名稱:", uploaded_file.name)
    st.write("檔案大小:", f"{uploaded_file.size / 1024:.2f} KB")
    st.write("檔案類型:", uploaded_file.type)

    # 讀取檔案
    try:
        if uploaded_file.name.endswith('.csv'):
            df = pd.read_csv(uploaded_file)
        else:
            df = pd.read_excel(uploaded_file)

        # 顯示資料概覽
        st.subheader("資料概覽")
        st.write(f"資料形狀:{df.shape[0]} 列 × {df.shape[1]} 欄")

        # 顯示欄位資訊
        col1, col2 = st.columns(2)
        with col1:
            st.write("欄位名稱:")
            st.write(df.columns.tolist())
        with col2:
            st.write("資料類型:")
            st.write(df.dtypes)

        # 顯示資料
        st.subheader("資料預覽")
        st.dataframe(df.head(10))

        # 基本統計
        st.subheader("描述性統計")
        st.dataframe(df.describe())

    except Exception as e:
        st.error(f"讀取檔案時發生錯誤:{str(e)}")

# === 多檔案上傳 ===
st.header("多檔案上傳")

uploaded_files = st.file_uploader(
    "上傳多個檔案",
    type=['csv', 'txt', 'json'],
    accept_multiple_files=True
)

if uploaded_files:
    st.write(f"已上傳 {len(uploaded_files)} 個檔案")
    for file in uploaded_files:
        with st.expander(f"📄 {file.name}"):
            if file.name.endswith('.csv'):
                df = pd.read_csv(file)
                st.dataframe(df.head())
            elif file.name.endswith('.json'):
                import json
                data = json.load(file)
                st.json(data)
            else:
                content = file.read().decode('utf-8')
                st.text(content[:500])

# === 圖片上傳 ===
st.header("圖片上傳")

uploaded_image = st.file_uploader(
    "上傳圖片",
    type=['png', 'jpg', 'jpeg', 'gif']
)

if uploaded_image is not None:
    from PIL import Image

    image = Image.open(uploaded_image)

    col1, col2 = st.columns(2)
    with col1:
        st.image(image, caption="原始圖片", use_column_width=True)
    with col2:
        st.write("圖片資訊:")
        st.write(f"格式:{image.format}")
        st.write(f"大小:{image.size}")
        st.write(f"模式:{image.mode}")

NLP 命名實體識別應用實作

現在讓我們將所學的知識整合起來,建立一個完整的 NLP 命名實體識別應用程式。這個應用程式將使用 spaCy 模型來識別文本中的實體,並提供視覺化的結果呈現。

# ner_app.py
# 命名實體識別應用程式

import streamlit as st
import spacy
from spacy import displacy
import pandas as pd

# 頁面配置
st.set_page_config(
    page_title="NER 命名實體識別",
    page_icon="🏷️",
    layout="wide"
)

# 應用程式標題
st.title("🏷️ 命名實體識別 (NER) 應用")
st.markdown("使用 spaCy 模型識別文本中的實體,包括人名、組織、地點等。")

# === 載入模型 ===
@st.cache_resource
def load_spacy_model(model_name):
    """
    載入 spaCy 模型
    使用 @st.cache_resource 快取模型避免重複載入
    """
    try:
        nlp = spacy.load(model_name)
        return nlp
    except OSError:
        st.error(f"無法載入模型:{model_name}")
        st.info("請執行:python -m spacy download {model_name}")
        return None

# === 側邊欄設定 ===
with st.sidebar:
    st.header("⚙️ 設定")

    # 模型選擇
    model_options = {
        "英文 (小)": "en_core_web_sm",
        "英文 (中)": "en_core_web_md",
        "英文 (大)": "en_core_web_lg",
        "中文": "zh_core_web_lg"
    }

    selected_model = st.selectbox(
        "選擇模型",
        list(model_options.keys()),
        index=2
    )

    model_name = model_options[selected_model]

    # 實體類型篩選
    st.subheader("實體類型篩選")
    entity_types = {
        "PERSON": "人名",
        "ORG": "組織",
        "GPE": "地點",
        "DATE": "日期",
        "TIME": "時間",
        "MONEY": "金額",
        "PERCENT": "百分比",
        "PRODUCT": "產品",
        "EVENT": "事件",
        "WORK_OF_ART": "作品"
    }

    selected_entities = st.multiselect(
        "顯示的實體類型",
        list(entity_types.keys()),
        default=["PERSON", "ORG", "GPE", "DATE"],
        format_func=lambda x: f"{x} ({entity_types.get(x, x)})"
    )

    # 視覺化選項
    st.subheader("視覺化選項")
    show_table = st.checkbox("顯示實體表格", value=True)
    show_stats = st.checkbox("顯示統計資訊", value=True)

# === 主要內容區域 ===
# 載入模型
nlp = load_spacy_model(model_name)

if nlp:
    # 文字輸入
    st.header("📝 輸入文字")

    # 範例文字
    sample_texts = {
        "英文範例": """
        Apple Inc. CEO Tim Cook announced today that the company will invest
        $1 billion in a new research facility in Austin, Texas. The facility
        is expected to create 5,000 jobs and will focus on artificial intelligence
        and machine learning technologies. The announcement was made during the
        annual shareholders meeting held on March 15, 2024.
        """,
        "中文範例": """
        台積電董事長劉德音今日宣布,公司將在新竹科學園區投資新台幣三千億元
        興建先進製程研發中心。該中心預計於2025年第三季完工,届時將創造
        超過兩千個高科技就業機會。這項投資是台灣半導體產業發展的重要里程碑。
        """
    }

    sample_choice = st.selectbox(
        "選擇範例文字或自行輸入",
        ["自行輸入"] + list(sample_texts.keys())
    )

    if sample_choice == "自行輸入":
        input_text = st.text_area(
            "請輸入要分析的文字",
            height=200,
            placeholder="在此輸入文字..."
        )
    else:
        input_text = st.text_area(
            "請輸入要分析的文字",
            value=sample_texts[sample_choice].strip(),
            height=200
        )

    # 分析按鈕
    if st.button("🔍 分析實體", type="primary"):
        if input_text:
            # 執行 NER
            doc = nlp(input_text)

            # === 視覺化結果 ===
            st.header("📊 分析結果")

            # 實體視覺化
            st.subheader("實體標註")

            # 過濾要顯示的實體類型
            options = {"ents": selected_entities}

            # 使用 displacy 渲染
            html = displacy.render(
                doc,
                style="ent",
                options=options
            )

            # 顯示 HTML
            st.markdown(html, unsafe_allow_html=True)

            # === 實體表格 ===
            if show_table and doc.ents:
                st.subheader("實體列表")

                entities_data = []
                for ent in doc.ents:
                    if ent.label_ in selected_entities:
                        entities_data.append({
                            "實體文字": ent.text,
                            "實體類型": ent.label_,
                            "類型說明": entity_types.get(ent.label_, ent.label_),
                            "起始位置": ent.start_char,
                            "結束位置": ent.end_char
                        })

                if entities_data:
                    df_entities = pd.DataFrame(entities_data)
                    st.dataframe(df_entities, use_container_width=True)
                else:
                    st.info("未找到選定類型的實體")

            # === 統計資訊 ===
            if show_stats and doc.ents:
                st.subheader("統計資訊")

                # 計算實體統計
                entity_counts = {}
                for ent in doc.ents:
                    label = ent.label_
                    if label not in entity_counts:
                        entity_counts[label] = 0
                    entity_counts[label] += 1

                # 顯示統計
                col1, col2 = st.columns(2)

                with col1:
                    st.metric("總實體數", len(doc.ents))
                    st.metric("實體類型數", len(entity_counts))

                with col2:
                    st.metric("文字長度", len(input_text))
                    st.metric("Token 數", len(doc))

                # 實體類型分布圖
                if entity_counts:
                    import plotly.express as px

                    df_counts = pd.DataFrame({
                        "實體類型": list(entity_counts.keys()),
                        "數量": list(entity_counts.values())
                    })

                    fig = px.bar(
                        df_counts,
                        x="實體類型",
                        y="數量",
                        title="實體類型分布"
                    )
                    st.plotly_chart(fig, use_container_width=True)

            # === 依存句法分析 ===
            with st.expander("依存句法分析"):
                # 只顯示前幾個句子避免圖表過大
                sentences = list(doc.sents)[:2]
                for i, sent in enumerate(sentences):
                    st.markdown(f"**句子 {i+1}:** {sent.text}")
                    html = displacy.render(
                        sent,
                        style="dep",
                        options={"compact": True}
                    )
                    st.markdown(html, unsafe_allow_html=True)

        else:
            st.warning("請輸入要分析的文字")

# === 頁尾資訊 ===
st.divider()
st.markdown("""
### 關於此應用
這個應用程式使用 [spaCy](https://spacy.io/) 進行命名實體識別(NER)。
NER 是自然語言處理的重要任務,能夠自動識別文本中的實體並將其分類。

**支援的實體類型:**
- PERSON:人名
- ORG:組織名稱
- GPE:地理政治實體(國家、城市等)
- DATE:日期
- TIME:時間
- MONEY:金額
- 等等...
""")
@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

title NER 應用程式流程

start
:使用者輸入文字;
:選擇 spaCy 模型;
:載入模型(快取);
note right
    使用 @st.cache_resource
    避免重複載入
end note
:執行 NER 分析;
fork
    :視覺化實體標註;
fork again
    :建立實體表格;
fork again
    :計算統計資訊;
end fork
:顯示分析結果;
stop

@enduml

多頁面應用與部署

當應用程式功能較多時,可以將其組織成多頁面結構。Streamlit 支援原生的多頁面應用,只需要在專案目錄中建立 pages 資料夾即可。

# 多頁面應用結構
#
# my_app/
# ├── app.py              # 主頁面
# ├── pages/
# │   ├── 1_📊_資料分析.py
# │   ├── 2_🤖_模型預測.py
# │   └── 3_⚙️_設定.py
# └── requirements.txt

# app.py - 主頁面
import streamlit as st

st.set_page_config(
    page_title="多頁面應用",
    page_icon="🏠"
)

st.title("🏠 歡迎使用多頁面應用")

st.markdown("""
## 功能介紹

請從左側選單選擇頁面:

- **📊 資料分析**:上傳資料並進行探索性分析
- **🤖 模型預測**:使用機器學習模型進行預測
- **⚙️ 設定**:調整應用程式設定

""")

# 顯示當前 session state
if st.checkbox("顯示 Session State"):
    st.json(dict(st.session_state))

部署 Streamlit 應用程式有多種選項,最簡單的方式是使用 Streamlit Community Cloud,它提供免費的託管服務。

# requirements.txt
# 部署時需要的套件清單

streamlit==1.28.0
pandas==2.0.0
numpy==1.24.0
plotly==5.15.0
spacy==3.6.0
scikit-learn==1.3.0
pillow==10.0.0

# 下載 spaCy 模型
# 在 packages.txt 中加入:
# python -m spacy download en_core_web_lg

部署步驟包括將程式碼推送到 GitHub、在 Streamlit Community Cloud 連結 GitHub 儲存庫,以及設定應用程式配置。整個過程通常只需要幾分鐘即可完成。

本文詳細介紹了如何使用 Streamlit 建構機器學習網頁應用程式。我們從基礎環境設定開始,逐步探討了互動式元件、資料視覺化、快取機制、頁面布局、表單處理、檔案上傳等核心功能,並以命名實體識別應用作為完整的實務案例。Streamlit 的簡潔設計讓開發者能夠專注於應用程式的核心邏輯,而不必花費大量時間在前端開發上。

雖然 Streamlit 在快速原型開發和內部工具建構方面表現出色,但它也有一些限制。例如,它的客製化能力較為有限,不適合需要複雜互動邏輯或高度客製化介面的應用。對於需要更高效能或更複雜架構的生產環境應用,可能需要考慮其他解決方案。然而,對於大多數資料科學和機器學習的展示需求而言,Streamlit 仍然是一個極具價值的工具,它能夠讓我們在最短的時間內將模型轉化為可互動的應用程式,讓更多人能夠體驗和使用我們的工作成果。