Unix 時間戳解析:換算方法與常見陷阱
Unix 時間戳是程式設計中最簡單卻最常被誤解的概念之一。它是自 1970 年 1 月 1 日 00:00:00 UTC 以來經過的秒數——這個時間點被稱為 Unix Epoch。儘管概念簡單,時間戳卻是與時區、精度和溢位相關 Bug 的常見來源。
什麼是 Unix Epoch?
Unix Epoch——1970 年 1 月 1 日 00:00:00 UTC——被選為 Unix 時間的起始點。每個時間戳都是相對於此刻的測量值:
| 時間戳 | 日期與時間 (UTC) |
|---|---|
| 0 | 1970 年 1 月 1 日 00:00:00 |
| 86400 | 1970 年 1 月 2 日 00:00:00 |
| 1000000000 | 2001 年 9 月 9 日 01:46:40 |
| 1700000000 | 2023 年 11 月 14 日 22:13:20 |
| 2000000000 | 2033 年 5 月 18 日 03:33:20 |
負數時間戳代表 Epoch 之前的日期。例如,-86400 是 1969 年 12 月 31 日。
使用我們的時間戳轉換器即時轉換時間戳。
秒 vs. 毫秒
這是最常見的混淆來源。不同的系統使用不同的精度:
| 系統 | 精度 | 範例 |
|---|---|---|
| Unix/POSIX | 秒 | 1700000000 |
| JavaScript | 毫秒 | 1700000000000 |
| Java (System.currentTimeMillis) | 毫秒 | 1700000000000 |
| Python (time.time) | 秒(浮點數) | 1700000000.123 |
| PostgreSQL (extract epoch) | 秒(浮點數) | 1700000000.123456 |
經驗法則:如果數字有 13 位數,那是毫秒。如果有 10 位數,那是秒。
// JavaScript returns milliseconds
const nowMs = Date.now(); // 1700000000000
const nowSec = Math.floor(nowMs / 1000); // 1700000000
時區處理
Unix 時間戳一律是 UTC。它們不包含時區資訊。這其實是一個優點——它提供了一個通用的參考點。
混淆通常發生在將時間戳轉換為人類可讀的日期時:
const ts = 1700000000;
const date = new Date(ts * 1000);
date.toUTCString(); // "Tue, 14 Nov 2023 22:13:20 GMT"
date.toLocaleString(); // Depends on user's local time zone
date.toISOString(); // "2023-11-14T22:13:20.000Z"
最佳實踐:以 UTC 儲存和傳輸時間戳。只在展示層將其轉換為本地時間,越靠近使用者越好。
2038 年問題
傳統 Unix 系統將時間戳儲存為 32 位元有號整數。最大值為 2,147,483,647,對應到 2038 年 1 月 19 日 03:14:07 UTC。
超過這個時刻,32 位元時間戳會溢位為負數,回繞到 1901 年 12 月 13 日。這類似於千年蟲問題。
目前狀況:
- 大多數現代系統使用 64 位元時間戳(可用到 2920 億年後)
- Linux 核心自 5.6 版本(2020 年)起已完成 64 位元時間戳的清理
- 嵌入式系統和舊版資料庫仍有風險
- 如果你正在開發處理 2038 年以後日期的軟體,請驗證你的時間戳儲存方式
各語言中的轉換
JavaScript
// Current timestamp (seconds)
const now = Math.floor(Date.now() / 1000);
// Timestamp to Date
const date = new Date(1700000000 * 1000);
// Date to timestamp
const ts = Math.floor(new Date('2023-11-14').getTime() / 1000);
Python
import time, datetime
# Current timestamp
now = int(time.time())
# Timestamp to datetime
dt = datetime.datetime.fromtimestamp(1700000000, tz=datetime.timezone.utc)
# Datetime to timestamp
ts = int(dt.timestamp())
SQL (PostgreSQL)
-- Current timestamp
SELECT EXTRACT(EPOCH FROM NOW());
-- Timestamp to date
SELECT TO_TIMESTAMP(1700000000);
-- Date to timestamp
SELECT EXTRACT(EPOCH FROM '2023-11-14'::timestamp);
常見陷阱
1. 混淆秒和毫秒
如果日期顯示為 1970 年 1 月,表示你可能在預期毫秒的地方傳入了秒(或反過來)。務必確認 API 預期的精度。
2. 日期字串中忽略時區
解析 "2023-11-14" 但未指定時區,會以本機時區建立日期,而這會因伺服器位置不同而改變。務必包含時區:"2023-11-14T00:00:00Z"。
3. 浮點數精度
將時間戳儲存為浮點數時,超過毫秒可能會失去精度。對於需要微秒或奈秒精度的場景,使用整數並搭配適當的倍數。
4. 閏秒
Unix 時間戳不考慮閏秒。Unix 的一天永遠是剛好 86,400 秒,即使實際的 UTC 天偶爾有 86,401 秒。對大多數應用來說這不影響。對於科學或衛星應用,請改用 TAI(國際原子時)。
ISO 8601:人類可讀的替代方案
雖然時間戳很適合計算,ISO 8601 是人類可讀日期表示的標準:
2023-11-14T22:13:20Z # UTC
2023-11-14T17:13:20-05:00 # Eastern Time
2023-11-14 # Date only
大多數 API 應該接受並回傳 ISO 8601 字串。在內部使用時間戳進行計算和儲存。
常見問題
為什麼 Unix 時間從 1970 年 1 月 1 日開始?
這個日期是在 1970 年代初期 Bell Labs 開發 Unix 時隨意選擇的。需要一個夠近的日期來避免將位元浪費在遙遠的過去。由於 32 位元整數在兩個方向各能儲存約 68 年,從 1970 年開始可以涵蓋 1901 年到 2038 年。
我應該在資料庫中將日期存為時間戳還是格式化字串?
將日期存為時間戳(整數或原生 datetime 類型),以便高效排序、比較和運算。格式化字串更難正確查詢和排序。大多數資料庫都有原生 datetime 類型能妥善處理。將字串格式化保留給展示和 API 回應。
相關資源
- 時間戳轉換器 — 在 Unix 時間戳和人類可讀日期之間轉換
- JSON 格式化最佳實踐 — 處理 JSON 回應中的日期
- UUID 指南 — 另一種包含時間變體的常見識別碼格式