CSV不是「純文字」而已 ── C# 業務應用程式的 CSV 實務(字元編碼・Excel 相容性・注入攻擊防範)
· Go Komura · CSharp, .NET, CSV, Excel, 字元編碼, 檔案整合, Windows 開發, 技術諮詢
透過 CSV 進行資料整合,在業務應用程式的世界中至今仍是活躍的主角。而正因為它看起來只是「用逗號把文字接起來」而已,自行寫的解析器被欄位內的逗號弄壞、用 Excel 開啟時亂碼、電話號碼開頭的零消失──這類事故在這個領域反覆發生。
本文以 C# 讀寫 CSV 的業務應用程式為前提,依照實務上容易猶豫不決的順序,整理 CSV 的規格(RFC 4180)與現實中的方言、字元編碼的決定方式、與 Excel 的相容性問題、CSV 注入攻擊防範,以及維運上的陷阱。
1. 先講結論(判斷表)
CSV 的設計判斷,要從「是誰(或什麼)要開啟這個檔案」往回推算。
| 用途 | 字元編碼的首選 | 設計上的要點 |
|---|---|---|
| 人用 Excel 雙擊開啟 | UTF-8(有 BOM) | 沒有 BOM 的話,某些環境中 Excel 會誤判字元編碼而造成亂碼。開頭零消失、日期化的問題無法靠字元編碼解決(第 5 章) |
| 新的系統間整合 | UTF-8(無 BOM) | 在規格書中明確寫出字元編碼、引號、換行符號、是否有標頭。是否有 BOM 也要明確記載 |
| 與舊系統整合 | 依對方規格(多為 Shift_JIS) | 在 .NET(Core) 中使用 Shift_JIS 需要註冊 CodePagesEncodingProvider(第 4 章)。特殊符號的處理方式要先約定好 |
| 大量資料的批次整合 | 依規格 | 不要把全部資料載入記憶體,而是逐行串流處理(第 7 章)。也要設計互斥控制與寫入中檔案的處理方式 |
| 主要目的是人用 Excel 檢視、列印 | ── | 應該考慮不用 CSV 而輸出 xlsx(參見 FAQ) |
先寫出結論。
- 用
String.Split(',')解析 CSV,即使看起來能動,實際上在規格上是有問題的。 因為它無法處理欄位內的逗號、換行、引號。若侷限於 .NET 標準範圍,請使用TextFieldParser(第 3 章)。 - 字元編碼的事故是因「Excel 如何解讀」而發生的。 人要用 Excel 開啟的 CSV,目前比較穩妥的選擇是採用有 BOM 的 UTF-8。此外,在 .NET 中是否會加上 BOM,會因
Encoding的指定方式而不同,這是一個陷阱(第 4 章)。 - 在 .NET(Core) 中,Shift_JIS 若不額外安裝套件就無法使用。 必須先安裝
System.Text.Encoding.CodePages套件並註冊CodePagesEncodingProvider,Encoding.GetEncoding("shift_jis")才會成功。這是從 .NET Framework 移轉過來的 CSV 處理,在正式環境中第一次拋出例外的典型情況。 - 開頭零消失、日期化的問題無法在 CSV 這一側防止。 重要的是,即使用雙引號包起來也無法防止(第 5 章)。
- 會將使用者輸入內容輸出成 CSV 的應用程式,需要防範 CSV 注入攻擊。 以
=或+開頭的欄位會被 Excel 解讀為公式(第 6 章)。
2. CSV 的「規格」──RFC 4180 與現實中的方言
CSV 有一個事實上的標準,也就是 RFC 41801。實務上需要掌握的規則只有以下 4 點。
- 紀錄的分隔是換行(CRLF),欄位的分隔是逗號。
- 若欄位內含有逗號、換行、雙引號,則整個欄位要用雙引號包起來。
- 欄位內的雙引號要用
""兩個重疊的方式跳脫。 - 不需要包起來的欄位,包或不包都可以。
也就是說,以下這一行是3 個欄位的紀錄。
1001,"株式会社サンプル 営業部","住所は""東京都○○区1-2-3""です,と入力された備考"
而 RFC 4180只是「事實上的標準」,現實中的 CSV 有各式各樣的方言。換行只用 LF、不做引號處理、分隔符號是 Tab 或分號、標頭行有時有有時沒有──正因如此,在整合規格書中寫上「遵循 RFC 4180」這一句話,並明確記載字元編碼、BOM、換行符號、是否有標頭,才是防止日後糾紛中投資報酬率最高的工作。
3. 不該用 Split(‘,’) 自行實作 ── 讀取的實務
依照上一章的規則,String.Split(',') 為什麼會壞掉就很清楚了。欄位內的逗號會造成多餘的分割,引號內的換行會讓一行在中途被切斷。「現在收到的檔案裡沒有含逗號的資料,所以能動」只是碰巧而已,只要資料一變就隨時可能壞掉。
若要在 .NET 標準範圍內正確讀取,可以使用 Microsoft.VisualBasic.FileIO.TextFieldParser。雖然命名空間是 Visual Basic,但在 C# 中也可以正常使用。
using Microsoft.VisualBasic.FileIO; // Microsoft.VisualBasic.Core(隨附於 Windows 桌面應用程式 SDK)
using var parser = new TextFieldParser(path, System.Text.Encoding.UTF8)
{
TextFieldType = FieldType.Delimited,
HasFieldsEnclosedInQuotes = true, // 正確處理用引號包起來的欄位
TrimWhiteSpace = false, // 預設為 true(前後空白會被靜默移除)。若空白本身有意義,請停用此設定
};
parser.SetDelimiters(",");
while (!parser.EndOfData)
{
try
{
string[]? fields = parser.ReadFields();
if (fields is null) break;
// 處理 fields[0], fields[1], ...
}
catch (MalformedLineException ex)
{
// 格式錯誤的行:可用 ErrorLine / ErrorLineNumber 記錄內容與行號
logger.LogWarning("CSV 第 {Line} 行格式不正確: {Content}",
ex.LineNumber, parser.ErrorLine);
}
}
TextFieldParser 在實務上的優點,除了引號處理(HasFieldsEnclosedInQuotes)之外,還能以 MalformedLineException 搭配行號來偵測格式錯誤的行。在業務上的 CSV 匯入場景中,「若有一行壞掉,是要拒絕整個檔案,還是只跳過該行並回報」往往是規格上的分歧點,因此可以直接把這個例外處理的設計落實到規格中。
若還需要欄位與物件的對應、型別轉換、乃至數百萬行規模的效能,請考慮導入有實績的 CSV 函式庫(NuGet 上廣泛使用的那些)。無論選擇哪個函式庫,都應確認以下 4 個觀點:「RFC 4180 的引號處理」「字元編碼指定」「格式錯誤行的處理方式」「是否支援串流讀取」。
輸出端因為規格較單純,自行實作也算現實可行,但請務必把跳脫處理集中在一個函式中。
// 輸出端的跳脫處理集中在這一個函式中
static string ToCsvField(string? value)
{
value ??= string.Empty;
bool needsQuoting = value.Contains(',') || value.Contains('"')
|| value.Contains('\r') || value.Contains('\n');
return needsQuoting ? $"\"{value.Replace("\"", "\"\"")}\"" : value;
}
只要有一個「金額欄位是手寫 string.Join 拼出來的地方」殘留著,那裡就會成為未來的事故現場。
4. 字元編碼 ── Excel 的行為與 .NET 的陷阱
4.1 若要用 Excel 開啟,選擇有 BOM 的 UTF-8
CSV 亂碼的大多數,是因為Excel 誤判了檔案的字元編碼而發生的。若雙擊開啟沒有 BOM(檔案開頭附加的幾個位元組標記)的 UTF-8 CSV,在某些環境下會被解讀為 ANSI(在日文環境中即 Shift_JIS),導致日文全部變成亂碼。若是有 BOM 的 UTF-8,就會被辨識為 UTF-8,因此人要用 Excel 雙擊開啟的 CSV,目前比較穩妥的選擇是採用有 BOM 的 UTF-8。
這裡還疊加了 .NET 端的陷阱。是否會加上 BOM,會依 Encoding 的傳遞方式而不同。
// 無 BOM 的 UTF-8:省略編碼指定時的預設行為
File.WriteAllText(path, csv);
// 有 BOM 的 UTF-8:Encoding.UTF8(靜態屬性)的設定是會輸出 BOM 的
File.WriteAllText(path, csv, Encoding.UTF8);
// 若想明確表達意圖,用建構函式指定會比較沒有誤解
File.WriteAllText(path, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
傳入 Encoding.UTF8 和省略時所得到的檔案結果不同,這是不知道的話就不會察覺的規格。若已將「系統間整合用無 BOM」「給 Excel 用的用有 BOM」訂為規格,建議在程式碼上也用 new UTF8Encoding(...) 明確表達意圖。
4.2 在 .NET(Core) 中使用 Shift_JIS 需要事先註冊
在與舊系統整合時,Shift_JIS(編碼頁 932)的 CSV 至今仍很活躍。這裡重要的是,.NET(Core)/.NET 5 以後,預設只支援 Unicode 系列與 ASCII 等極少數的編碼。Encoding.GetEncoding("shift_jis") 在加入 System.Text.Encoding.CodePages 套件並註冊 CodePagesEncodingProvider 之前,都會拋出例外。
using System.Text;
// 在應用程式啟動時只註冊一次(Program.cs 等)
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// 這樣就能取得 Shift_JIS(編碼頁 932)了
Encoding sjis = Encoding.GetEncoding("shift_jis");
從 .NET Framework 時代的 CSV 處理移轉到 .NET 的應用程式,在正式環境中第一次收到 Shift_JIS 檔案時才拋出例外──這是一個經典的事故案例。移轉時應確認的觀點,整理在「將 .NET Framework 遷移到 .NET 之前該確認的事」中。
此外,既然使用 Shift_JIS,特殊符號(如①、㈱等)、波浪號問題、僅存在於 Unicode 中的字元缺失等問題就無法避免。這方面字元編碼的基礎知識,在「釐清 Windows 的文字編碼」與「整理 Windows 的字元編碼與換行符」中有詳細說明。
5. 用 Excel 開啟就會壞掉的資料 ── 開頭零消失、日期化、指數表示
即使字元編碼正確,用 Excel 檢視還有另一組問題。Excel 在開啟 CSV 時會對值進行「解讀」。
| 原始資料 | Excel 中的顯示 | 發生了什麼事 |
|---|---|---|
090123456xx(電話號碼) |
90123456xx |
被解讀為數值,開頭的零消失 |
007(商品代碼) |
7 |
同上 |
1-1(分支編號) |
1月1日 |
被解讀為日期 |
1234567890123456(16 位管理編號) |
1.23457E+15 |
變成指數表示,低位數字消失 |
而重點在於,即使把欄位用雙引號包起來,也無法防止這些問題。引號只是表示 CSV 語法上的分隔而已,Excel 還是會重新解讀內容。
實務上的因應方式有以下 3 種選擇。
- 請對方用 Excel 正確的操作方式開啟。 不要用雙擊,而是用 Excel 的「匯入資料(從文字檔/CSV)」功能,將欄位類型指定為「文字」再讀取,就不會壞掉。這值得寫進操作手冊中。
- 若主要目的是檢視,輸出 xlsx。 因為可以由輸出端控制儲存格格式,這一整組問題會從根本上消失。Excel 檔案輸出的做法可參考「Excel 報表輸出該怎麼做」。此外,在伺服器或服務中使用 Excel COM 自動化並不推薦,如「C# 操作 Excel 時 EXCEL.EXE 殘留的問題」中所寫,容易造成程序殘留,請選擇以函式庫為基礎的輸出方式。
- 以
="007"形式輸出是最後手段。 這是一種讓 Excel 開啟時顯示為文字的技巧,但作為 CSV 而言會變成「含有公式的非標準檔案」,會破壞 Excel 以外的匯入對象。在與其他系統共用的檔案中請勿使用。
6. CSV 注入攻擊 ── 不要讓使用者輸入變成公式
若應用程式會將使用者輸入的值(姓名、備註、地址等)輸出成 CSV 並讓人用 Excel 開啟,就存在CSV 注入攻擊這個攻擊途徑。以 =、+、-、@ 等開頭的欄位會被 Excel 解讀為公式,因此惡意輸入(例如像 =cmd|'/C calc'!A0 這種企圖啟動外部程序的公式),可能會在開啟該 CSV 的另一位使用者環境中被執行,這就是問題所在。
因應的基本做法,是在輸出時將危險的開頭字元無害化。
// 在可能被解讀為公式的欄位開頭加上單引號。
// 在日文環境中,全形的=+-@也可能被解讀為公式的開頭,因此也要納入對象
static string SanitizeForExcel(string value)
{
if (value.Length > 0 && value[0]
is '=' or '+' or '-' or '@' or '\t' or '\r' or '\n'
or '=' or '+' or '-' or '@')
{
return "'" + value;
}
return value;
}
開頭的單引號在 Excel 中是「當作文字處理」的標記。此外,由於危險的開頭字元清單會因試算表軟體的實作與版本而有所不同,這類無害化處理若設計成「只有開頭符合此允許清單(例如英數字等)時才原樣通過」的方向,會更能抵禦未預想到的遺漏。不過要注意,若其他系統要匯入這個 CSV,單引號會以資料的形式殘留,因此也可能需要判斷「給 Excel 開啟用」與「給系統匯入用」要分成不同檔案。是否要搭配輸入值驗證(例如一開始就不允許開頭為 =),乃至整個應用程式的安全性設計,可參考「Windows 應用程式開發中遵守最低限度安全性的檢核表」。
7. 維運上的陷阱 ── 格式以外會出事故的地方
最後,整理 CSV 語法本身以外,實務上容易發生事故的地方。
- 不要讓數值、日期的格式依賴作業系統的地區設定。
value.ToString()會受作業系統地區設定的影響。在小數點以逗號表示的地區設定下,會輸出成1,5,與 CSV 的分隔符號衝突。用於檔案整合的轉換,請明確指定CultureInfo.InvariantCulture,日期則使用像yyyy-MM-dd這樣的固定格式。日期的設計方式在「業務應用程式的日期時間與時區」中有詳細說明。 - 大量資料要用串流處理──但是否能「逐行」處理要看規格而定。
File.ReadAllLines會把所有行都讀入陣列並載入記憶體,不適合大檔案。這裡可以改用File.ReadLines(延遲列舉)或StreamReader的逐行讀取,但僅限規格上明確聲明「不允許欄位內換行」的檔案。在允許引號內換行、遵循 RFC 4180 的 CSV 中,逐行分割會在中途切斷一筆紀錄。這種情況下,請使用可直接從檔案路徑讀取的TextFieldParser(不需將整體載入記憶體即可處理),或 CSV 函式庫的串流讀取功能。 - 不要讀取「寫到一半的檔案」,也不要讓別人讀取。 輸出端先以暫存檔名寫入後再重新命名,匯入端先確認互斥後再讀取──這種檔案整合的基本形式,整理在「檔案整合的互斥控制基礎」中;透過資料夾監控來匯入時的注意事項,整理在「FileSystemWatcher 的使用方法與注意事項」中。
- 要為欄位的增減做準備。 應在規格中明確寫出是「依標頭行的欄位名稱來對應」還是「欄位順序固定、以欄位數檢查、不一致就拒絕」。像隱含的
fields[7]這種魔術數字參照,會因整合對象一個小小的欄位新增而悄悄壞掉。 - 要先決定結尾換行與空行的處理方式。 最後一行之後是否有換行、空行要跳過還是拒絕,這些是不同解析器實作行為會有分歧的部分。
TextFieldParser會跳過空行。
結語
CSV 是一種外表看似「只是用逗號分隔」,實際上充滿方言的整合格式。實務上的要點有 4 個──讀取要用能處理引號的元件(不要用 Split 自行實作)、字元編碼要從開啟方(尤其是 Excel)反推決定並在規格中寫明是否有 BOM、要知道 Excel 對數值的解讀(零消失、日期化)無法在 CSV 端防止、若要輸出使用者輸入內容就要加入注入攻擊防範──只要做到這些,CSV 整合的大多數事故都能在設計階段消除。
既有 CSV 整合的亂碼或資料損毀調查、與舊系統整合規格的釐清、CSV 匯入・輸出功能的全新實作,往往需要一邊查看實際檔案與整合對象的規格一邊做判斷,若有疑慮歡迎隨時諮詢。
相關文章
- 釐清 Windows 的文字編碼
- 整理 Windows 的字元編碼與換行符
- Excel 報表輸出該怎麼做
- C# 操作 Excel 時 EXCEL.EXE 殘留的問題
- 檔案整合的互斥控制基礎
- FileSystemWatcher 的使用方法與注意事項
- 業務應用程式的日期時間與時區
- 將 .NET Framework 遷移到 .NET 之前該確認的事
- Windows 應用程式開發中遵守最低限度安全性的檢核表
相關諮詢領域
合同會社小村軟體(小村ソフト)提供包含 CSV、檔案整合在內的業務應用程式開發,既有整合的亂碼・資料損毀原因調查,以及與舊系統整合規格釐清等相關技術諮詢。
參考資料
-
IETF, RFC 4180 - Common Format and MIME Type for Comma-Separated Values (CSV) Files. CSV 事實上的標準規格。關於欄位內逗號、換行、雙引號的引號處理,以及用
""進行跳脫。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
WinForms/WPF 應用程式的多語言化 ── resx、附屬組件與文化特性切換的實務
本文從實務角度整理 Windows 桌面應用程式的多語言化,包括 CurrentCulture 與 CurrentUICulture 的差異、resx 與附屬組件(Satellite Assembly)所構成的資源機制、WinForms 的 Localizable 屬性、W...
不要用 using 包住 HttpClient ── C# 商用應用程式的 HTTP 通訊實務(生成模式・逾時・重試)
C# 的 HttpClient 若每次都用 using 生成,會導致 Socket 枯竭;改用 static 又無法跟上 DNS 變更──本文從 Windows 商用應用程式的實務角度,整理這兩大問題的成因,以及透過 PooledConnectionLifetime、IHt...
用 PerfView 與 dotnet-trace 找出「變慢」的原因 ── .NET 效能調查實務入門
當商用應用程式「變慢」「CPU 卡住不動」「偶爾當掉」時,該用哪個工具看什麼?本文整理 PerfView 與 dotnet-trace 的角色分工、CPU 取樣的讀法(inclusive/exclusive)、用 ThreadTime 調查阻塞時間,以及與 EventSou...
不只是 appsettings.json ── Windows 商用應用程式的組態管理實務(環境別設定・機密資訊・寫入位置)
本文從實務角度整理 Windows 商用應用程式的組態管理,涵蓋 appsettings.json 的分層、IConfiguration 與 IOptions/IOptionsSnapshot/IOptionsMonitor 的選用方式、環境別設定、可寫入設定的存放位置、機...
Windows 業務應用程式的列印與 PDF 輸出 ── System.Drawing.Printing / WPF / 報表函式庫的取捨
本文以要件別判斷表整理 PrintDocument 的 WinForms 列印、WPF 的 FlowDocument/FixedDocument 列印,以及 PDF 輸出的各種選項。並從實務角度說明分頁控制、DPI 落差等實作陷阱,以及從 Windows 服務列印、依賴預設...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
透過 CSV 進行系統間整合與報表輸出的設計與實作,屬於 Windows 應用程式開發的實務諮詢範圍。
技術諮詢 & 設計審查
調查既有 CSV 整合的亂碼、資料損毀原因並釐清規格,屬於伴隨設計審查的技術諮詢。
常見問題
整理諮詢這個主題時常見的問題。
- CSV 的字元編碼應該選什麼?
- 如果接收方已經確定,配合接收方的規格是最基本的原則。若是可以重新決定的情況,人要用 Excel 開啟的檔案採用「有 BOM 的 UTF-8」,系統間整合則在規格書中明確寫出「無 BOM 的 UTF-8」,是比較穩妥的起點。若因與舊系統整合而被要求使用 Shift_JIS,若不先約定好特殊符號與外字的處理方式,事後容易演變成亂碼責任歸屬的問題。
- 應該導入 CSV 函式庫嗎?還是自行實作即可?
- 就讀取而言,自行用 String.Split 在規格上有無法正確處理的情況(欄位內的逗號、換行、引號),因此應該使用某種現成元件。若侷限於 .NET 標準範圍,TextFieldParser 支援相當於 RFC 4180 的引號處理。若還需要欄位與物件的對應、型別轉換、大量資料的效能,請考慮導入有實績的 CSV 函式庫。輸出的規格較單純,自行實作也算現實可行,但請務必準備一個共用的引號跳脫函式,讓所有輸出都經過該函式。
- 應該改用 Tab 分隔(TSV)而不是逗號分隔嗎?
- 如果資料中經常出現逗號(地址、姓名、金額表示等),改用 TSV 可以降低需要加引號的頻率,也比較方便肉眼確認。不過 Tab 或換行同樣可能出現在資料中,並不代表就不需要引號處理。請注意,「因為是 TSV 所以不用考慮分隔符號」這種想法是錯的。若能與整合對象達成共識,比起分隔符號本身,「在規格書中明確寫出字元編碼、引號、換行符號、是否有標頭」更能有效防止事故。
- 如果目的是要用 Excel 開啟,是否應該輸出 Excel 檔而非 CSV?
- 如果主要目的是「讓人用 Excel 檢視、列印」,則值得考慮輸出 xlsx 格式。CSV 是一種在 Excel 開啟的瞬間就會發生開頭零消失、日期化等問題的格式,而且無法在 CSV 這一側完全防止這些問題。若是 xlsx,則可以由輸出端控制儲存格的格式。另一方面,如果主要目的是匯入其他系統,用 Excel 檢視只是附帶用途,那麼維持 CSV 格式、並將「匯入步驟(使用資料匯入功能)」作為操作流程說明,會是比較實際的做法。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。