Watch at Ease看見世界,讀懂生活。

技術 · 實用指南

Python CSV分不清None與空字串?先查執行版本與交換約定

有這個引號選項,不代表讀取端已實作其行為。20項本機測試區分可解決的解析問題、不可逆的匯出遺失,以及僅適用文字或缺漏值的替代約定。

原創示意圖:匯出與3.12讀取兩處可能遺失缺漏值與空文字的區別。
原創示意圖:保留原始欄位,分別檢查寫入和讀取;單欄None另有寫入例外。 Watch at Ease

在CPython3.12.14中,使用csv.QUOTE_NOTNULL寫入[None, ""],得到未加引號的空欄位與帶引號的空欄位。但使用同一選項讀回時,兩者都成為空字串。常數存在,不代表部署中的讀取器能保留這個區別。

先保留原始位元組,確認產生端如何約定缺漏值。若舊匯出已將缺漏值與刻意留空的文字寫成相同形式,更換讀取選項也無法還原。需要區分時,應保留檔案並從原始資料重新匯出。

1. 找出資訊在哪一環遺失

不要變更來源檔。用repr()查看不含敏感資料的小樣本,讓逗號、引號與紀錄結尾可見。我們的預設csv.writer往返測試把[None, ""]變成["", ""],資訊在匯出時遺失。NOTNULL寫入器保留兩種欄位,3.12讀取器卻再次合併它們,這是解析器相容性邊界。

不要把所有解析後的空字串改成None當作修復。雙欄位樣本會因此成為兩個None,抹去刻意留空的值。未定義跳脫規則的替代標記也會衝突:若<NULL>代表None,原本就是<NULL>的文字便無法區分。

2. 同時測試寫入、讀取與單欄紀錄

輸出sys.version,再用正式環境相同選項寫入、讀取包含None、空字串及"001"的樣本。Python3.12參考文件明確提示,QUOTE_NOTNULL與QUOTE_STRINGS不影響讀取器行為;相關讀取變更記錄於3.13。我們實際執行的只有3.12.14。

還要測試僅含None的一列。我們的QUOTE_NOTNULL寫入器拋出csv.Error: single empty field record must be quoted。只有一個空字串的列讀回為[""],空白紀錄則為[]。在靠引號區分缺漏值的約定下,為消除錯誤而替缺漏值加引號,會將它寫成空文字。

3. 轉換之前先約定格式

對相容的現代讀取端,文件中的QUOTE_NOTNULL規則將未加引號的空欄位讀為None,其餘欄位保留為字串。它不會還原整數或任意Python物件。QUOTE_STRINGS另有數值轉換規則,也不是通用物件序列化器。採用前,應在兩端實際環境測試支援的列結構。

PostgreSQL COPY CSV顯示接收約定的重要性:預設未加引號的空欄位代表NULL,帶引號的空欄位代表空文字;FORCE_NULL與FORCE_NOT_NULL可改變解讀。這是PostgreSQL文件的規則,不是所有CSV通用的保證。我們沒有執行資料庫或Python3.13整合測試。

4. 兩端皆可控制時的有限替代方案

若舊環境交換只允許str或None,我們測試的應用層約定將None編碼為字面欄位N,每個字串則編碼為S加完整原文。空文字因此為S,文字N為SN,真實文字<NULL>為S<NULL>。再交由一般csv.writer處理,逗號、引號與內嵌換行仍由CSV層負責。

解碼時,只有完全等於N的欄位成為None;以S起頭者只移除第一個字元,其他欄位全部視為錯誤標記拒絕。寫入前拒絕非字串且非None的輸入。這是必須明確協議的新應用格式,不能直接套用既有一般CSV,也不能假設COPY、pandas或試算表理解這些標記。

5. 用值、型別與列結構判定完成

20項斷言測試涵蓋兩處資訊遺失、單欄缺漏值寫入例外、空白紀錄與單個空欄位、空值轉換、替代標記衝突及13種值的標記往返。矩陣包括None、空文字、"001"、"0"、看似缺漏值或標記的文字、逗號、引號、內嵌換行與Unicode;讀回的值與型別皆一致。

其餘測試確認單個缺漏值、單個空文字與兩個缺漏值紀錄,拒絕整數、布林值、錯誤標記及未編碼的空欄位。這只證明上述文字或缺漏值約定在CPython3.12.14/Linux的表現,不能認證其他執行環境或下游軟體。每個邊界皆應保留代表樣本;原始區別已遺失時重新匯出,不要猜測。

原創示意圖:None、空文字與真實標記文字的N/S編碼。
原創示意圖:僅用於雙方約定的str或None交換;錯誤標記必須拒絕。 Watch at Ease