iT邦幫忙

0

Day 24:改善資料保存與查詢——提升系統的穩定性

  • 分享至 

  • xImage
  •  

一、前言

在前幾天的學習中,我已經完成個人健康習慣追蹤系統的基本功能,包括健康紀錄輸入、資料保存、歷史紀錄查詢、統計分析、圖表呈現與習慣完成率計算,也開始學習如何透過測試案例檢查程式是否正常運作。

然而,當系統逐漸累積更多健康紀錄時,資料保存與查詢的穩定性也會變得更加重要。

例如,使用者可能不小心重複新增同一天的紀錄,或是在讀取 CSV 時遇到格式錯誤。如果系統沒有妥善處理這些問題,就可能造成資料重複、統計結果不準確,甚至無法正常開啟網頁。

因此,今天將進一步改善資料保存與查詢功能,學習如何檢查檔案是否存在、避免重複紀錄、處理異常資料,並讓歷史紀錄的查詢更加方便。

二、學習目標

了解 CSV 資料保存的限制與常見問題。

使用 pathlib 管理資料檔案路徑。

使用 try...except 處理資料讀取與寫入錯誤。

建立重複日期檢查機制。

使用 Pandas 進行日期範圍查詢。

改善健康紀錄保存與查詢的穩定性。

三、認識資料保存的常見問題

目前系統使用 health_records.csv 保存健康紀錄。CSV 是一種常見的純文字表格格式,容易閱讀,也能透過 Pandas 進行資料分析。

不過,使用 CSV 保存資料時,仍然可能遇到一些問題。

https://ithelp.ithome.com.tw/upload/images/20261029/20178903BH2pfQ9X20.png

此外,CSV 不具備資料庫交易與多使用者同步控制等功能。如果未來系統需要支援大量資料、多人同時使用,或更複雜的修改與刪除操作,就可以考慮改用 SQLite 等資料庫。

四、使用 pathlib 管理資料檔案

在 Python 中,可以使用 pathlib 管理檔案路徑。

例如:

from pathlib import Path

FILE_PATH = Path("health_records.csv")

if FILE_PATH.exists():
print("健康紀錄檔案已存在。")
else:
print("目前尚無健康紀錄檔案。")

這段程式會先檢查 health_records.csv 是否存在,再決定後續的處理方式。

相較於直接假設檔案一定存在,先進行檢查可以避免讀取不存在的檔案時發生錯誤。

如果未來想將資料統一存放在專案中的 data 資料夾,也可以使用:

from pathlib import Path

DATA_DIR = Path("data")
DATA_DIR.mkdir(parents=True, exist_ok=True)

FILE_PATH = DATA_DIR / "health_records.csv"

print(FILE_PATH)

mkdir() 可以建立資料夾,而 exist_ok=True 表示如果資料夾已經存在,就不會因此產生錯誤。

但如果改變資料檔案的位置,既有功能也必須一起更新,否則系統可能會讀取到不同位置的檔案。

五、建立穩定的資料讀取與保存函式

為了避免在不同功能中重複撰寫讀取與保存程式碼,可以將這些操作整理成獨立的函式。

以下範例使用 Pandas 讀取 CSV,並在讀取失敗時提供清楚的錯誤訊息。

from pathlib import Path

import pandas as pd

FILE_PATH = Path("health_records.csv")

COLUMNS = [
"date",
"water_ml",
"sleep_hours",
"exercise_minutes",
"vegetables_fruits",
"notes"
]

def load_health_records():
if not FILE_PATH.exists():
return pd.DataFrame(columns=COLUMNS)

try:
    records = pd.read_csv(FILE_PATH)

    missing_columns = [
        column
        for column in COLUMNS
        if column not in records.columns
    ]

    if missing_columns:
        raise ValueError(
            "缺少必要欄位:" + ", ".join(missing_columns)
        )

    return records

except (OSError, pd.errors.ParserError, UnicodeDecodeError, ValueError) as error:
    raise RuntimeError(
        f"讀取健康紀錄失敗:{error}"
    ) from error

這個函式有幾個重點:

如果檔案不存在,就回傳具有正確欄位的空白資料表。

如果檔案存在,就嘗試讀取資料。

讀取後檢查必要欄位是否完整。

如果發生錯誤,就將原因整理成較容易理解的訊息。

這裡選擇在發生錯誤時明確回報,而不是默默回傳空白資料表,因為把真正的讀取錯誤當成「沒有資料」,可能讓使用者誤以為原本的紀錄全部消失。

接著,可以建立保存函式:

def save_health_records(records):
try:
records.to_csv(
FILE_PATH,
index=False,
encoding="utf-8-sig"
)
return True

except (OSError, ValueError) as error:
    raise RuntimeError(
        f"保存健康紀錄失敗:{error}"
    ) from error

to_csv() 會將資料表寫入 CSV 檔案。使用 index=False 可以避免將 Pandas 的索引額外保存成一個欄位,而 utf-8-sig 則有助於讓部分試算表軟體正確辨識中文編碼。

要注意,這個函式會寫入整份資料表,因此應該先確認傳入的資料正確,再呼叫保存函式。

六、避免重複新增同一天的健康紀錄

目前的系統可能允許同一天新增多筆完整健康紀錄,進而影響平均值、運動天數與完成率等統計。

如果本專案採用「每天只保存一筆完整健康紀錄」的規則,就可以在新增前檢查日期是否已存在。

def add_health_record(record):
records = load_health_records()

record_date = pd.to_datetime(
    record["date"],
    errors="coerce"
)

if pd.isna(record_date):
    raise ValueError("紀錄日期格式不正確。")

normalized_date = record_date.strftime("%Y-%m-%d")

existing_dates = pd.to_datetime(
    records["date"],
    errors="coerce"
).dt.strftime("%Y-%m-%d")

if existing_dates.eq(normalized_date).any():
    raise ValueError(
        "這個日期已經有健康紀錄,請先修改既有紀錄。"
    )

new_record = record.copy()
new_record["date"] = normalized_date

updated_records = pd.concat(
    [
        records,
        pd.DataFrame([new_record])
    ],
    ignore_index=True
)

save_health_records(updated_records)

return updated_records

這個函式會依序執行以下步驟:

讀取目前保存的健康紀錄。

檢查新增日期是否有效。

比對既有紀錄的日期。

如果日期已存在,就拒絕重複新增。

如果日期尚未存在,就加入新紀錄並保存。

需要注意,這個範例假設每一天只會有一筆完整紀錄。如果未來想記錄一天內多次喝水、不同運動時段,或多次睡眠紀錄,就應該重新設計資料結構,而不是單純限制日期只能出現一次。

另外,如果先前已經存在重複日期,這個函式不會自動刪除舊資料。若要整理舊資料,應先備份 CSV,再決定要保留哪一筆紀錄。

七、改善歷史紀錄查詢功能

除了保存資料之外,讓使用者能快速找到特定日期的紀錄,也是一項重要功能。

例如,使用者可能只想查看某一週的健康紀錄,而不是每次都瀏覽整份資料表。

以下範例示範如何利用 Streamlit 的日期選擇器與 Pandas 進行篩選。

import streamlit as st
import pandas as pd

st.title("健康紀錄查詢")

try:
records = load_health_records()
except RuntimeError as error:
st.error(str(error))
st.stop()

if records.empty:
st.info("目前尚無健康紀錄。")
st.stop()

records["date"] = pd.to_datetime(
records["date"],
errors="coerce"
)

records = records.dropna(subset=["date"])

if records.empty:
st.info("目前沒有有效的日期資料。")
st.stop()

min_date = records["date"].min().date()
max_date = records["date"].max().date()

start_date = st.date_input(
"開始日期",
value=min_date
)

end_date = st.date_input(
"結束日期",
value=max_date
)

if start_date > end_date:
st.error("開始日期不可晚於結束日期。")
else:
filtered_records = records[
records["date"].dt.date.between(
start_date,
end_date
)
].sort_values("date", ascending=False)

if filtered_records.empty:
    st.info("這個日期區間沒有健康紀錄。")
else:
    st.dataframe(
        filtered_records,
        use_container_width=True,
        hide_index=True
    )

這段程式會讓使用者選擇開始日期與結束日期,再顯示符合條件的紀錄。

其中,between() 可以檢查每筆日期是否位於指定範圍內,而 sort_values() 則可以將最新的紀錄排在前面。

透過日期範圍查詢,使用者就能更方便地查看某段期間的喝水量、睡眠時間與運動情況。

八、實際執行與測試

完成相關程式碼後,應該使用測試資料確認保存與查詢功能是否正常。

https://ithelp.ithome.com.tw/upload/images/20261029/20178903IzoPETMnct.png

測試時,建議先備份原本的 health_records.csv,再使用另外建立的測試檔案進行新增、查詢與錯誤測試。

如果想將本日函式整合至既有的 app.py,必須注意不要直接把獨立範例全部貼在同一個檔案中,否則可能出現重複的匯入、頁面標題或表單。應將共用的讀取與保存函式集中管理,再由各個頁面呼叫。

九、今日成果與學習心得

今天完成了資料保存與查詢穩定性的改善規劃,學習使用 pathlib 管理檔案路徑,透過例外處理回報讀取與寫入錯誤,並建立日期檢查與範圍查詢功能。

透過這次學習,我了解到系統穩定性不只取決於程式能否執行,也取決於資料是否能正確保存,以及錯誤發生時系統能否清楚回應。

例如,避免重複日期可以降低統計失真的風險;日期範圍查詢可以提升使用便利性;讀取失敗時提供明確訊息,也能讓使用者知道問題可能出在哪裡。

同時,我也了解到 CSV 適合目前的小型學習專案,但隨著資料量與功能增加,可能需要更完整的資料管理方式。未來若要支援更複雜的資料修改、刪除與查詢,就可以考慮使用 SQLite,並為每筆紀錄設計獨立識別碼。

十、下一步學習方向

第二十五天的主題是「整合核心功能——完成健康習慣追蹤系統初版」。

下一階段將把目前學習過的健康紀錄輸入、資料保存、歷史查詢、資料管理、統計分析、圖表呈現與目標完成率整合起來,逐步建立一個可以實際操作的系統初版,並為後續的測試與成果展示做準備。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言