HOWTO · Csharp
C# 讀取 CSV 檔案並將其值儲存到陣列中
本文介紹如何讀取 CSV 檔案並將其值儲存在 C# 中的陣列中的不同方法。它介紹了 OpenRead(),ReadLine()和 TextFieldParser 之類的方法
本頁內容
CSV 檔案是一種以逗號分隔的檔案,用於以有組織的方式儲存資料。它通常以表格形式儲存資料。大多數企業組織都將資料儲存在 CSV 檔案中。
在 C# 中,我們可以對 CSV 檔案執行各種操作。我們可以讀取、寫入和修改 CSV 檔案。本文重點介紹讀取 CSV 檔案並將其值儲存到陣列中的方法:使用 StreamReader 逐行讀取、使用 File.ReadAllLines() 一次載入全部行,以及使用 TextFieldParser 解析欄位。
前三個範例使用同一個範例 Data.csv 檔案:
Name,Age
Alice,30
Bob,25
使用 StreamReader 讀取 CSV 檔案
StreamReader 類別將檔案作為文字串流讀取。File.OpenRead() 開啟檔案,ReadLine() 一次返回一行,直到到達 EndOfStream。將每一行按逗號拆分即可得到各個值,範例將這些值按欄儲存到一個清單中。
使用 OpenRead() 和 ReadLine() 方法的正確語法如下:
// OpenRead() Syntax
File.OpenRead(@"FilePath");
// ReadLine() Syntax
StreamReaderObject.ReadLine();
範例程式碼:
using System.IO;
using System.Collections.Generic;
using System;
class ReadingCSV {
static void Main(string[] args) {
var reader = new StreamReader(File.OpenRead(@"D:\New folder\Data.csv"));
List<string> listA = new List<string>();
List<string> listB = new List<string>();
while (!reader.EndOfStream) {
var line = reader.ReadLine();
var values = line.Split(',');
listA.Add(values[0]);
listB.Add(values[1]);
}
foreach (var column1 in listA) {
Console.WriteLine(column1);
}
foreach (var column2 in listB) {
Console.WriteLine(column2);
}
}
}
輸出:
Name
Alice
Bob
Age
30
25
按逗號拆分適用於欄位本身不包含逗號的情況。像 "Smith, John" 這樣帶引號的欄位會在錯誤的位置被拆開;對於這類檔案,請使用 TextFieldParser。
使用 File.ReadAllLines 一次讀取全部行
當檔案小到可以裝入記憶體時,File.ReadAllLines() 是得到陣列的最短路徑:它讀取整個檔案,並將每一行作為 string[] 的一個元素傳回。下面的範例拆分每一行,將每個資料行儲存為自己的 string[],並用 ToArray() 將行列表轉換為交錯陣列,因此 data[i] 就是完整的第 i 行。迴圈從索引 1 開始以跳過標題行,跳過空白行,並在索引前檢查欄位數量,因此過短或為空的行不會讓它崩潰。
範例程式碼:
using System;
using System.Collections.Generic;
using System.IO;
class ReadingAllLines {
static void Main() {
string[] lines = File.ReadAllLines("Data.csv");
List<string[]> rows = new List<string[]>();
// Start at index 1 to skip the header row.
for (int i = 1; i < lines.Length; i++) {
if (string.IsNullOrWhiteSpace(lines[i])) {
continue;
}
string[] values = lines[i].Split(',');
if (values.Length < 2) {
continue;
}
rows.Add(values);
}
string[][] data = rows.ToArray();
foreach (string[] row in data) {
Console.WriteLine(row[0] + ": " + row[1]);
}
}
}
輸出:
Alice: 30
Bob: 25
取捨在於記憶體與簡單性。ReadAllLines() 在處理第一行之前就載入整個檔案,並傳回一個可以立即索引、計數和重讀的陣列。StreamReader 迴圈一次只保留一行,因此對於只需處理一次就丟棄的超大檔案,它仍然是更安全的選擇。對於會多次查看的小檔案,一次全部載入更簡單,而且夠快。
使用 TextFieldParser 解析 CSV 欄位
TextFieldParser 類別定義在 Microsoft.VisualBasic.FileIO 命名空間中。它逐個欄位地解析帶分隔符的檔案,並能理解帶引號的欄位。使用 SetDelimiters() 設定分隔符,然後呼叫 ReadFields() 將目前行的欄位作為字串陣列取得;解析器在每次呼叫時移動到下一行。在 .NET Framework 專案中,請在執行下面的程式之前加入對 Microsoft.VisualBasic 的參考;在現代 .NET 中,該命名空間是共用框架的一部分。
使用此解析器的正確語法如下:
TextFieldParser ParserName = new TextFieldParser(PathString);
範例程式碼:
using System;
using System.Collections.Generic;
using Microsoft.VisualBasic.FileIO;
class ReadingCSV {
public static void Main() {
var path = @"D:\New folder\Data.csv";
List<string> listA = new List<string>();
List<string> listB = new List<string>();
using (TextFieldParser csvReader = new TextFieldParser(path)) {
csvReader.CommentTokens = new string[] { "#" };
csvReader.SetDelimiters(new string[] { "," });
csvReader.HasFieldsEnclosedInQuotes = true;
// Skip the row with the column names
csvReader.ReadLine();
while (!csvReader.EndOfData) {
// Read current line fields, pointer moves to the next line.
string[] fields = csvReader.ReadFields();
listA.Add(fields[0]);
listB.Add(fields[1]);
}
}
foreach (var column1 in listA) {
Console.WriteLine(column1);
}
foreach (var column2 in listB) {
Console.WriteLine(column2);
}
}
}
輸出:
Alice
Bob
30
25
這一次,標題行透過最初的 ReadLine() 被跳過,並且由解析器而不是手動拆分來決定每個欄位在哪裡結束,因此欄位內部帶引號的逗號會保持完整。
處理帶引號的欄位和逸出引號
真實的匯出檔案會給欄位加引號。包含分隔符的欄位會被雙引號包圍,而帶引號欄位內部的雙引號會被寫成兩個。下面的範例 People.csv 檔案同時使用了這兩條規則:
Name,Note
"Smith, John","Likes ""quoted"" text"
Jones,Plain note
Split(',') 對這些規則一無所知:它會在名字內部的逗號處,把第一個資料行切成 3 段。TextFieldParser 則會套用這些規則:它去掉包圍欄位的引號,並把每一對重複引號合併成一個,因此欄位會原封不動地按預期傳回。這個程式先輸出 Split(',') 的段數,然後輸出解析後的欄位。
範例程式碼:
using System;
using Microsoft.VisualBasic.FileIO;
class ReadingQuotedCsv {
public static void Main() {
string rawLine = "\"Smith, John\",\"Likes \"\"quoted\"\" text\"";
Console.WriteLine("Split(',') pieces: " + rawLine.Split(',').Length);
using (TextFieldParser parser = new TextFieldParser("People.csv")) {
parser.SetDelimiters(new string[] { "," });
parser.HasFieldsEnclosedInQuotes = true;
// Skip the row with the column names
parser.ReadLine();
while (!parser.EndOfData) {
string[] fields = parser.ReadFields();
Console.WriteLine(fields[0] + " | " + fields[1]);
}
}
}
}
輸出:
Split(',') pieces: 3
Smith, John | Likes "quoted" text
Jones | Plain note
如果手動拆分得到的欄位數比標題行的欄數還多,原因通常就是帶引號的欄位。對這類檔案應改用 TextFieldParser,而不是事後試圖拼湊這些片段。
讀取 CSV 檔案時的常見錯誤
-
values[1]處的IndexOutOfRangeException:末尾的空白行或缺少某欄的行會被拆成比程式碼預期更少的欄位。應跳過純空白行,並在索引前檢查欄位數量,就像ReadAllLines()範例所做的那樣。 -
FileNotFoundException或DirectoryNotFoundException:執行程式的機器上不存在該路徑。像D:\New folder\Data.csv這樣寫死的絕對路徑只在作者的電腦上有效;應優先使用相對路徑,或用Path.Combine()建構路徑,並在偵錯時用File.Exists()檢查。 -
標題行被當成資料出現:第一個
StreamReader範例把Name和Age與真實資料行一起儲存。當檔案帶有標題時,應像其他範例那樣,在迴圈之前跳過第一行。 -
檔案一直被鎖定:從未關閉的
StreamReader會一直持有其控制代碼,直到垃圾回收器執行。應將讀取器宣告在using區塊中,這樣即使例外中斷了迴圈,檔案也會在迴圈結束時立即關閉。