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 區塊中,這樣即使例外中斷了迴圈,檔案也會在迴圈結束時立即關閉。