將僅含日期的出版值解析為日曆日期,並以該日期為準,即可保留此值。若匯出格式要求 UTC 時間戳記,請明確採用 UTC 午夜慣例。這份 Mac 終端機操作指南會說明兩者的差異,並測試往返轉換。

判斷出版欄位實際代表什麼

假設出版清單中有合成值 2028-06-15。你的任務是驗證此值並為匯出做好準備,同時避免意外顯示為 6 月 14 日。在選擇解析器之前,先判斷此欄位記錄的是出版日期,還是實際出版時刻。

以下四個概念各有不同用途:

概念 合成範例 意義
民用日期 2028-06-15 日曆上的某一天,不含時間或時差
時刻 2028-06-15T00:00:00+00:00 以 UTC 表示的特定時刻
UTC 時差 +09:00 當地時鐘讀數與 UTC 之間的差異
命名時區 America/Los_Angeles 用來取得適用時區規則的識別碼

RFC 3339 將 full-date 與 date-time 分別定義;其時間戳記語法會在日期上加上時間和時差。因此,單獨的出版日期缺少識別出版時刻所需的資訊。選擇午夜會加入原始輸入中不存在的資訊。RFC 3339,第 5.6 節

命名時區所含資訊也多於單一數值時差。Python 的 zoneinfo 說明文件示範了時區如何在日光節約時間轉換期間使用不同的時差。對僅含日期的標籤而言,無須解析這些規則。Python:IANA 時區支援

匯出前請依照以下規則判斷:

  • **欄位表示「在這一天出版」:**儲存和顯示時都保留僅含日期的值。
  • **接收格式要求時間戳記:**保留日期,並依據有文件記載的慣例,衍生出 UTC 午夜載體值。
  • **欄位表示「在這一刻出版」:**取得實際時間和時差,或取得能確定該時刻所需的時間和命名時區。

載體慣例有助於相容性,但無法還原未知的出版時間。像 utc_carrier 這樣的欄位名稱能清楚表達此限制;若稱為 published_at,則會暗示來源從未提供的資訊。

了解前一日錯誤從何而來

以 2028-06-15 為例,考慮兩種可能的處理路徑。第一種路徑中,程式在固定時差 +09:00 下自行假定午夜,然後將該時刻轉換為 UTC。結果是 2028-06-14T15:00:00+00:00:減去九小時後,日期跨過了界線。

第二種路徑中,程式刻意建立 UTC 午夜:2028-06-15T00:00:00+00:00。以固定時差 -07:00 顯示同一時刻,會得到 2028-06-14T17:00:00-07:00。這些是示範性的時差計算,並非宣稱某個命名時區在該日期使用該時差。

兩種計算都保留了各自代表的時刻,但無論在哪個顯示時區,都無法保留原始日曆標籤。Python 說明,astimezone() 會調整時鐘欄位,同時保留所表示的 UTC 時間。Python:datetime.astimezone()

實務上,解析和顯示需要遵循同一份契約。如果日期已放入 UTC 午夜載體值,請在 UTC 中還原其日曆欄位。若希望無論何人開啟匯出檔,出版標籤都保持不變,請顯示保留的民用日期。

不要假設每種語言都會補入相同的缺失資訊。ECMAScript 規定,不含時差的僅日期格式會解讀為 UTC,而不含時差的日期時間格式會解讀為當地時間。因此,在字串後面附加 T00:00:00 可能會改變 JavaScript 對它的解讀。ECMAScript:Date.parse

在 Mac 終端機中執行完整的出版日期檢查

這份操作指南會驗證三個合成日期、列印以定位字元分隔的預覽,並測試是否拒絕格式錯誤的輸入。它不會修改出版清單,也不會建立匯出檔案。

在 Finder 中開啟 /Applications/Utilities,然後連按兩下 Terminal。Apple 說明,這是開啟命令列 App 的支援方式。Apple:在 Mac 上開啟或結束終端機

執行以下命令以檢查直譯器:

python3 --version

這份操作指南需要 Python 3.11 或更新版本。如果無法使用 python3 或顯示的版本較舊,請先依照官方 macOS 安裝說明完成安裝,接著重新開啟 Terminal 再次檢查。Python 說明了 macOS 安裝程式的使用方式,以及如何透過 python3 執行指令碼。Python:在 macOS 上使用 Python

貼上以下完整區塊,包括最後的 PY,然後按下 Return。- 引數會告訴 Python 從標準輸入讀取程式。所有範例資料和匯入項目都已包含在內。Python:命令列與環境

python3 - <<'PY'
import re
from datetime import date, datetime, time, timezone


def parse_publication_date(text):
    if not isinstance(text, str) or re.fullmatch(
        r"[0-9]{4}-[0-9]{2}-[0-9]{2}", text
    ) is None:
        raise ValueError("Expected YYYY-MM-DD")
    return date.fromisoformat(text)


def utc_carrier(day):
    return datetime.combine(day, time(0), tzinfo=timezone.utc)


samples = ["2028-02-29", "2028-06-15", "2028-12-31"]
parsed = [(text, parse_publication_date(text)) for text in samples]

print("publication_date\tutc_carrier")
for original, day in parsed:
    carrier = utc_carrier(day)
    assert day.isoformat() == original
    assert carrier.astimezone(timezone.utc).date() == day
    print(day.isoformat(), carrier.isoformat(timespec="seconds"), sep="\t")

for invalid in [
    "2027-02-29",
    "2028-6-15",
    "2028-06-15T00:00:00",
    " 2028-06-15",
]:
    try:
        parse_publication_date(invalid)
    except ValueError:
        pass
    else:
        raise AssertionError(f"Unexpected acceptance: {invalid!r}")

print("Round-trip and rejection checks passed.")
PY

預期輸出如下:

publication_date	utc_carrier
2028-02-29	2028-02-29T00:00:00+00:00
2028-06-15	2028-06-15T00:00:00+00:00
2028-12-31	2028-12-31T00:00:00+00:00
往返轉換與拒絕檢查均已通過。

此範例和兩種固定時差計算已在可用的 Python 直譯器中成功執行。Mac 專屬的開啟方式和直譯器操作說明則依據連結的官方文件。

為什麼此範例能保留出版日期

解析器會分別檢查兩項內容:字串格式是否完全符合,以及日期是否為有效日曆日期。其正規表示式要求四位年份、兩位月份和兩位日期。接著,日期解析器會拒絕不存在的日期。這可避免出版欄位悄悄接受時間戳記、前後空格或縮寫輸入。

明確檢查格式十分重要,因為 date.fromisoformat() 也接受其他 ISO 表示形式,包括緊湊日期和週日期。接著,datetime.combine() 會刻意提供午夜和 UTC。Python:基本日期與時間型別

程式會先解析整份範例清單,再列印標頭。如果你將某個範例替換為無效值,程式會在顯示任何預覽列之前停止執行。最後明確列出的無效輸入檢查屬於獨立測試;預期會發生的例外都會被捕捉,因此可以檢查所有拒絕情況。

兩個往返轉換斷言表達了此契約。第一個會確認解析並格式化日期後,能重現原始標籤。第二個會確認衍生的載體值在 UTC 中解讀時會得到相同日期。兩者共同檢查的是日期是否確實保留,而非只確認能否產生某種時間戳記。

輸出會明確使用 +00:00。RFC 3339 也允許使用 Z 作為 UTC 時差標記,但即使在單獨的日期後加上其中任何一種尾碼,仍無法補上缺少的時間部分。請保留僅含日期的格式,或刻意建構完整時間戳記。RFC 3339,第 5.6 節

診斷日期仍然變動的匯出結果

如果下游預覽顯示前一天,請檢查每個處理邊界上的值。使用合成範例中的中間日期 2028-06-15,即可將每個階段與一個已知標籤進行比較。

檢查點 採用載體慣例時的預期值 出現差異時的可能原因
原始出版欄位 2028-06-15 輸入在解析前已變更
解析後以文字顯示的日期 2028-06-15 解析或正規化改變了標籤
衍生的 UTC 載體值 2028-06-15T00:00:00+00:00 引入了不同時間或時差
以 UTC 還原的日期 2028-06-15 轉換或擷取採用了錯誤的契約
顯示給讀者的出版標籤 2028-06-15 檢視器可能正以另一個時區顯示載體值

請獨立檢查顯示器和解析器。即使 UTC 載體值正確,使用當地時區設定顯示時,仍可能出現不同的日曆日期。若要顯示出版標籤,請將保留的僅含日期值傳給顯示器。如果它只接受時間戳記,請使用有文件記載的 UTC 顯示選項,並確認結果。

若接收工具只支援時刻,且一律以當地時間顯示,就無法透過 UTC 午夜慣例保證日期標籤不變。請另外保留僅含日期的文字欄位、使用受支援的僅日期欄位,或選擇能明確指定 UTC 格式的顯示器。這項限制源自上述時差計算;單靠更改解析器無法消除問題。

也不要把既有時間戳記當成僅含日期的輸入來處理。對於真正的時刻,改變其時區代表進行轉換。Python 區分了這項操作和替換 tzinfo 的做法;後者是在不調整時鐘欄位的情況下附加時區資訊。Python:時區轉換行為

對你的出版清單而言,請將 publication_date 保留為權威的日曆值。僅在需要時間戳記的介面衍生 UTC 載體值,並將慣例與欄位一併保存;在相信預覽結果之前,先確認能以 UTC 擷取日期。如此便能以可重複的方法保留提供的日期,同時不虛構出版時刻。

資料來源