Python 課程 ── 日期與時間(Datetime)
主題:時區處理(pytz / zoneinfo)
簡介
在日常開發中,日期與時間是最常碰到的資料型別之一。若只用 datetime.datetime 直接產生或儲存時間,往往會忽略 時區(timezone)的概念,導致跨時區的系統出現「時間錯亂」的問題。
- 跨國服務:如線上訂票、即時通訊、金融交易,都必須正確地把「本地時間」與「UTC」互相轉換。
- 資料庫與 API:大多數的儲存與傳輸格式(ISO 8601、RFC 3339)都要求時間必須帶有時區資訊。
Python 內建的 datetime 模組在 Python 3.2 之後已支援時區物件(tzinfo),但實作上仍有不少限制。為了更方便且正確地處理時區,我們可以使用兩個主要的套件:
- pytz(第三方套件,支援 Python 2 與 3)
- zoneinfo(Python 3.9+ 內建的標準庫)
本篇文章將以 實務角度說明兩者的使用方式、常見陷阱與最佳實踐,讓你在開發時能夠 正確且一致 地處理時區。
核心概念
1. 時區與 UTC 的關係
- UTC(Coordinated Universal Time):全球統一的時間基準,不會因為夏令時間或政治因素改變。
- 本地時區:例如
Asia/Taipei、America/New_York,會根據所在區域的法規自動調整夏令時間(DST)或其他偏移。
建議:在系統內部儲存時間時,盡量使用 UTC,只有在與使用者介面交互時才轉換為本地時區。
2. datetime 物件的三種狀態
| 狀態 | 產生方式 | 說明 |
|---|---|---|
| naive(無時區) | datetime.now() |
沒有 tzinfo,只能代表「本地時間」或「任意時間」。 |
| aware(有時區) | datetime.now(tz) |
帶有 tzinfo,能正確進行時區運算。 |
| offset-aware | datetime.now(timezone.utc) |
tzinfo 為固定 UTC 偏移(+00:00)。 |
重要:只有 aware 的
datetime才能安全地比較、相減或轉換時區。
3. 為什麼 pytz 仍然被廣泛使用?
- 兼容舊版 Python(3.6 以下)。
- 完整的 IANA 時區資料(
tzdata),提供歷史變更紀錄。 localize()與normalize()方法可正確處理夏令時間的「曖昧時間」與「跳躍時間」。
4. zoneinfo 的優勢
- 標準庫,不需要額外安裝套件(Python 3.9+)。
- 使用
tzdata套件(或系統自帶的時區資料)即可取得最新的 IANA 時區資訊。 - 語法直觀:直接使用
datetime.replace(tzinfo=zone)或datetime.astimezone(zone)。
程式碼範例
以下範例分別示範 pytz 與 zoneinfo 的基本用法,並涵蓋常見需求:取得當前時間、時區轉換、夏令時間處理、以及與字串的相互轉換。
1️⃣ 取得 UTC 與本地時區的現在時間(pytz)
import datetime, pytz
# 取得 UTC 現在時間(aware)
utc_now = datetime.datetime.now(pytz.UTC)
print("UTC now :", utc_now.isoformat())
# 取得台北時間(Asia/Taipei)
taipei_tz = pytz.timezone('Asia/Taipei')
taipei_now = utc_now.astimezone(taipei_tz)
print("Taipei now :", taipei_now.isoformat())
說明:
pytz.UTC是一個已經設定好的tzinfo,astimezone()會自動將時間從 UTC 轉換為目標時區。
2️⃣ 使用 localize() 處理 naive 時間(pytz)
import datetime, pytz
# 建立一個沒有時區資訊的 datetime(naive)
naive_dt = datetime.datetime(2023, 11, 5, 2, 30) # 2:30 AM
# 台北時區(沒有夏令時間)
taipei = pytz.timezone('Asia/Taipei')
aware_dt = taipei.localize(naive_dt) # 變成 aware
print("Taipei aware :", aware_dt.isoformat())
注意:若目標時區有夏令時間(如
America/New_York),在「曖昧時間」會拋出AmbiguousTimeError,或在「跳躍時間」拋出NonExistentTimeError。
3️⃣ 轉換夏令時間(pytz)— 以美東時間為例
import datetime, pytz
ny = pytz.timezone('America/New_York')
# 2023 年 11 月第一個星期日 01:30(DST 結束前)
naive_dt = datetime.datetime(2023, 11, 5, 1, 30)
# 這個時間同時存在於 DST 與標準時間,需明確指定 is_dst
aware_dt = ny.localize(naive_dt, is_dst=True) # 仍屬於 DST
print("DST 1:30 :", aware_dt.isoformat())
# 轉成標準時間
aware_std = ny.localize(naive_dt, is_dst=False)
print("Standard 1:30 :", aware_std.isoformat())
技巧:
is_dst=True/False可以用來解決「曖昧時間」的問題;若不確定,可使用pytz的normalize()先轉換為 UTC 再回轉。
4️⃣ 使用 zoneinfo(Python 3.9+)取得本地時間
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# 直接指定時區取得 aware datetime
taipei_now = datetime.now(ZoneInfo('Asia/Taipei'))
print("Taipei now (zoneinfo):", taipei_now.isoformat())
# 從 UTC 轉換
utc_now = datetime.now(timezone.utc)
ny_now = utc_now.astimezone(ZoneInfo('America/New_York'))
print("NY now (from UTC):", ny_now.isoformat())
重點:
ZoneInfo直接作為tzinfo使用,語法與pytz的localize不同,只要在建立時間或astimezone時傳入即可。
5️⃣ 解析與格式化 ISO 8601 字串(含時區)
from datetime import datetime
from zoneinfo import ZoneInfo
# 解析帶有時區的字串
iso_str = "2023-11-05T14:30:00+08:00"
dt = datetime.fromisoformat(iso_str) # 會自動產生 aware datetime
print("Parsed datetime :", dt)
# 轉換為另一時區(例如 UTC)
utc_dt = dt.astimezone(ZoneInfo('UTC'))
print("UTC datetime :", utc_dt.isoformat())
提示:
datetime.fromisoformat在 Python 3.7+ 已支援解析±hh:mm格式的時區偏移,若要支援更完整的 ISO 8601(如 Z 表示 UTC),可使用dateutil.parser.isoparse。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| naive vs aware | 直接比較或相減 naive 與 aware 會拋 TypeError。 |
統一使用 aware,建議在程式入口處將所有時間轉為 UTC。 |
| 夏令時間曖昧 | 同一時刻在 DST 前後都存在(如 2:30 AM 於美東)。 | 使用 pytz.localize(..., is_dst=…) 或在 zoneinfo 中手動檢查 fold 屬性(Python 3.6+)。 |
| 時區字串錯誤 | 輸入錯誤的時區名稱('Asia/Taiwan' → 無效)。 |
只使用 IANA 標準名稱,或透過 pytz.all_timezones_set / zoneinfo.available_timezones() 事前驗證。 |
| 系統時區不同 | 部署環境的系統時區與開發環境不一致,導致 datetime.now() 結果不同。 |
永遠使用 datetime.utcnow() 或 datetime.now(timezone.utc),避免依賴系統時區。 |
| 資料庫儲存 | DB 欄位直接存 datetime(無時區)會失去資訊。 |
在 DB 中使用 TIMESTAMP WITH TIME ZONE(PostgreSQL)或自行將 UTC 轉字串儲存。 |
最佳實踐清單
- 統一 UTC:所有內部計算、儲存、傳輸皆使用 UTC。
- 入口轉換:從外部(API、使用者輸入)取得時間時,立刻轉為 aware UTC。
- 顯示層轉換:僅在 UI、報表或郵件內容等需要本地化的地方才轉換時區。
- 使用
zoneinfo:若專案 Python 版本 ≥ 3.9,優先使用標準庫zoneinfo,減少外部依賴。 - 測試時區:單元測試中加入不同行政區的時區測試,確保夏令時間與歷史變更正確。
實際應用場景
1️⃣ 多國電商平台的訂單時間
- 需求:使用者下單時間應以其本地時區顯示,後端統一以 UTC 儲存,結算與物流系統以 UTC 為基準計算時效。
- 實作:前端送出 ISO 8601 帶時區的字串,後端
datetime.fromisoformat解析 →astimezone(ZoneInfo('UTC'))→ 存入 DB。 - 報表:查詢時再使用
astimezone(ZoneInfo(user_tz))轉回使用者時區。
2️⃣ 金融交易的時間戳記
- 需求:證券交易必須精確到毫秒,且所有時間必須是 UTC,以符合國際結算規範。
- 實作:使用
datetime.utcnow().replace(tzinfo=timezone.utc)取得 UTC aware datetime,轉成字串isoformat(timespec='milliseconds')儲存。 - 驗證:在風控系統中,所有時間比較均直接使用 UTC,避免因時區錯誤產生違規交易。
3️⃣ 日誌(Logging)與分布式追蹤
- 需求:微服務環境中,各服務所在的伺服器時區可能不同,必須統一時間格式。
- 實作:設定
loggingFormatter 為%(asctime)s,並在程式入口呼叫logging.Formatter.converter = time.gmtime,使所有日誌時間以 UTC 輸出。 - 分析:使用 ELK 或 Splunk 時,可直接以 UTC 為基礎進行時間範圍查詢。
總結
時區處理是每個需要與「時間」互動的系統不可迴避的挑戰。透過本文,我們了解了:
- UTC 為唯一可靠的時間基準,內部資料應統一使用 UTC。
- aware datetime 才能正確比較、相減與轉換。
- pytz 提供完整的 IANA 時區資料與
localize/normalize處理夏令時間的機制,適合舊版 Python。 - zoneinfo 為 Python 3.9+ 的標準解決方案,語法更直觀且不需額外依賴。
- 常見陷阱(曖昧時間、系統時區差異)與 最佳實踐(統一 UTC、入口轉換、測試時區)能大幅降低錯誤風險。
掌握這些概念後,你就能在 多國平台、金融交易、日誌系統 等實務情境中,安全且一致地處理時間與時區,避免因時間錯誤而產生的業務損失。祝你在 Python 時間處理的旅程中,一路順時(zone)而行!