C Sharpの基礎 - CSVファイル

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
2024年9月14日 (土) 23:10時点におけるWiki (トーク | 投稿記録)による版 (→‎使用例)
ナビゲーションに移動 検索に移動

概要

CSV (Comma-Separated Values) は、テキストベースのシンプルなファイル形式である。
各行がデータレコードを表しており、フィールドはカンマ (または他の区切り文字) で区切られている。

C#でCSVファイルを読み込む場合は、主にSystem.IO名前空間のクラスを使用する。
File.ReadAllLinesメソッドを使用してファイルの全行を文字列配列として読み込み、次に、String.Splitメソッドで各行をフィールドに分割する。

CSVファイルへ書き込む場合は、StreamWriterクラスを使用して行うことができる。
各フィールドをカンマで連結して、WriteLineメソッドで1行ずつファイルに書き込む。

より複雑なCSV操作には、サードパーティのライブラリを利用することも可能である。
CsvHelperライブラリは人気のあるライブラリであり、読み書きの柔軟性が高く、大規模なCSVファイルの処理に適している。

CSVファイルを扱う時の注意点として、フィールド内にカンマが含まれる場合の処理がある。
このような場合、フィールドを引用符で囲むなどの対策が必要である。

また、文字エンコーディングの問題にも注意が必要であり、特に異なる言語や地域のデータを扱う場合は重要である。

C#のLINQを活用すると、CSVデータの効率的な操作や分析が可能になる。
例えば、特定の条件に合うレコードのフィルタリング、あるいは、データの集計等を簡単に行うことができる。

セキュリティの観点からは、外部ソースからのCSVファイルを扱う場合には注意が必要である。
悪意のあるデータが含まれている可能性があるため、適切な入力検証とサニタイズを行うことが重要である。

パフォーマンスを考慮する場合、大規模なCSVファイルを扱う場合はストリーミング読み取りを検討する。
これにより、メモリ使用量を抑えつつ効率的に処理を行うことができる。


TextFieldParserクラス

CSVファイルを読み込むには、.NET Framework 2.0で追加されたTextFieldParserクラス(Microsoft.VisualBasic.FileIO名前空間)を使用する。
TextFieldParserクラスを使用することで、CSVファイルを読み込み、各行の各フィールドの文字列を簡潔に取得することができる。

TextFieldParserクラスは、Microsoft.VisualBasic.FileIO名前空間に含まれているため、
C#で使用する場合は、一般的に、Microsoft.VisualBasic.dllファイルへの参照を追加する必要がある。

※注意
このクラスはVisual Basicのライブラリの一部であるため、C#で使用する場合は追加の依存関係が生じることに注意する。

RFC 4180との関係

TextFieldParserクラスは多くのRFC 4180の要件を満たしている。 ただし、いくつかの点で完全な準拠ではない。

  • 準拠している点
    • カンマで区切られたフィールドの処理
    • 引用符で囲まれたフィールド内のカンマの処理
    • 改行文字を含むフィールドの処理

  • 部分的に準拠している点
    • 引用符の扱い
      TextFieldParserクラスは引用符の使用を柔軟に設定できるが、デフォルトの動作がRFC 4180と完全に一致するわけではない。

  • 準拠していない可能性がある点
    • ヘッダ行の特別な扱い
      RFC 4180はヘッダ行について言及しているが、TextFieldParserクラスにはヘッダ行を自動的に識別する機能がない。
    • 厳密なCRLF改行の要求
      RFC 4180はCRLF (\r\n) を改行として指定しているが、TextFieldParserクラスはより柔軟である。


以下の例では、TextFieldParserクラスを使用して、RFC 4180の仕様に近づけている。

 using Microsoft.VisualBasic.FileIO;
 
 using (TextFieldParser parser = new TextFieldParser("sample.csv"))
 {
    parser.TextFieldType = FieldType.Delimited;
    parser.SetDelimiters(",");
    parser.HasFieldsEnclosedInQuotes = true;  // RFC 4180に準拠するため
 
    // ヘッダ行の処理
    string[] headers = parser.ReadFields();
 
    while (!parser.EndOfData)
    {
       string[] fields = parser.ReadFields();
       // フィールドの処理を行う
    }
 }


CSVファイルの読み込み

エンコーディングの指定

TextFieldParserクラスを使用してCSVファイルを読み込むには、
まず、TextFieldParserクラスのコンストラクタに処理するCSVファイルを指定して、インスタンスを生成する。

CSVファイルに日本語が含まれている場合は、文字コードを指定する。

 using System.Text;
 using Microsoft.VisualBasic.FileIO;
 
 var parser = new TextFieldParser(@"hoge.csv", Encoding.GetEncoding("Shift_JIS"));  // Shift-JISを指定


デリミタの指定

デリミタを指定する場合は、TextFieldTypeプロパティにFieldType.Delimitedを指定する。

SetDelimitersメソッドを使用して、区切り文字を指定する。(複数の区切り文字が指定可能)
CSVファイルの場合は、,(カンマ)を指定する。

また、FieldType.FixedWidthを指定する場合、フィールドが固定幅のファイルも扱うことができる。

 parser.TextFieldType = FieldType.Delimited;
 parser.SetDelimiters(",");  // 区切り文字はカンマを指定する


レコードの取得

TextFieldParserクラスのReadFieldsメソッドを実行するごとに、CSVファイルを1行ずつ読むことができる。
ReadFieldsメソッドは、読み込んだレコードの全てのフィールドを文字列配列に変換して返す。

次のレコードが存在するかどうかはEndOfDataプロパティにより判定できるため、繰り返し文を使用してCSVファイル全体を処理する。

 while (!parser.EndOfData)
 {
     // 配列rowの要素は読み込んだレコードの各フィールドの値
     string[] row = parser.ReadFields();  // 1レコード読み込む
 }


使用例

以下の例では、sample.csvファイルを読み込み、各フィールドを切り出してタブ区切りで画面に出力する。
改行文字および空白文字がどのように処理されるかを分かりやすくするために、それぞれをnと_に置換して出力している。

  • HasFieldsEnclosedInQuotesプロパティ
    フィールドに改行やデリミタを含める為に引用符を使っているようなフォーマットを考慮する場合は、
    HasFieldsEnclosedInQuotesプロパティをtrueに設定する。(初期値はtrue)

  • TrimWhiteSpaceプロパティ
    フィールドの前後の空白文字を削除しない場合は、TrimWhiteSpaceプロパティをfalseに設定する。(デフォルトは、true)


 // 同期処理
 
 using System;
 using System.Text;
 using Microsoft.VisualBasic.FileIO;
 
 class CSVParser
 {
    static void Main()
    {
       using (var parser = new TextFieldParser("sample.csv", Encoding.GetEncoding("Shift_JIS")))
       {
          parser.TextFieldType = FieldType.Delimited;  // フィールドはデリミタにより区切る(可変)
          parser.Delimiters = new[] {","};             // 区切り文字を指定
          parser.CommentTokens = new[] {"#"};          // #で始まる行をコメントとする
 
          // parser.HasFieldsEnclosedInQuotes = false; // 引用符で括られたフィールドを持つか指定
          // parser.TrimWhiteSpace = false;            // フィールドの前後に含まれる空白を削除するか指定
 
          while (!parser.EndOfData)
          {
             try
             {
                string[] row = parser.ReadFields();    // 1行読み込む
                foreach (string field in row)
                {
                   field = field.Replace("\r\n", "n"); // 改行をnに置換
                   field = field.Replace(" ", "_");    // 空白を_に置換
                   Console.Write(field + "\t");        // タブ区切りで出力
                }
                Console.WriteLine();
             }
             catch(Exception ex)
             {
                Console.WriteLine(ex.Message);
             }
          }
       }
    }
 }


以下の例では、上記の例を非同期で行っている。
非同期処理は、I/O操作が頻繁に行われる場合、または、ユーザインターフェースの応答性を維持する必要がある場合に特に有効である。

 // 非同期処理
 
 using System;
 using System.Text;
 using System.IO;
 using System.Threading.Tasks;
 
 class CSVParser
 {
    static async Task Main()
    {
       string outputFile = "sample.txt";
       await WriteCSVAsync(outputFile);
    }
 
    static async Task WriteCSVAsync(string outputFile)
    {
       try
       {
          using (StreamWriter writer = new StreamWriter(outputFile, false, Encoding.GetEncoding("Shift_JIS")))
          {
             // ヘッダ行を書き込む
             await writer.WriteLineAsync("列1,列2,列3");
 
             // レコード行を書き込む
             await writer.WriteLineAsync("値1,値2,値3");
             await writer.WriteLineAsync("値4,\"値5,カンマを含む\",値6");
             await writer.WriteLineAsync("値7,値8,\"値9\n改行を含む\"");
          }
       }
       catch (Exception ex)
       {
          Console.WriteLine("書き込みエラー: " + ex.Message);
       }
    }
 }


CSVファイルの書き込み

使用例

以下の例では、StreamWriterクラスを使用して、CSVファイルへ書き込んでいる。

 using System;
 using System.Text;
 using System.IO;
 using Microsoft.VisualBasic.FileIO;
 
 class CSVParser
 {
    static void Main()
    {
       // 書き込み処理
       WriteCSV("sample.csv");
    }
 
    static void WriteCSV(string outputFile)
    {
       try
       {
          using (StreamWriter writer = new StreamWriter(outputFile, false, Encoding.GetEncoding("Shift_JIS")))
          {
             // ヘッダ行を書き込む
             writer.WriteLine("列1,列2,列3");
 
             // データ行を書き込む
             writer.WriteLine("値1,値2,値3");
             writer.WriteLine("値4,\"値5,カンマを含む\",値6");
             writer.WriteLine("値7,値8,\"値9\n改行を含む\"");
          }
       }
       catch (Exception ex)
       {
          Console.WriteLine("書き込みエラー: " + ex.Message);
       }
    }
 }


以下の例では、上記の例を非同期で処理している。

 // 非同期処理
 
 using System;
 using System.Text;
 using System.IO;
 using System.Threading.Tasks;
 
 class CSVParser
 {
    static async Task Main()
    {
       string outputFile = "sample.txt";
       await WriteCSVAsync(outputFile);
    }
 
    static async Task WriteCSVAsync(string outputFile)
    {
       try
       {
          using (StreamWriter writer = new StreamWriter(outputFile, false, Encoding.GetEncoding("Shift_JIS")))
          {
             // ヘッダ行を書き込む
             await writer.WriteLineAsync("列1,列2,列3");
 
             // レコード行を書き込む
             await writer.WriteLineAsync("値1,値2,値3");
             await writer.WriteLineAsync("値4,\"値5,カンマを含む\",値6");
             await writer.WriteLineAsync("値7,値8,\"値9\n改行を含む\"");
          }
       }
       catch (Exception ex)
       {
          Console.WriteLine("書き込みエラー: " + ex.Message);
       }
    }
 }