C# Tips | ファイル・ディレクトリ操作:CSV書き込み

C sharp C#
スポンサーリンク

はじめに 「CSV書き込み」ができると“外部連携の入口”が開く

CSV 読み込みが「外部からデータをもらう入口」だとしたら、
CSV 書き込みは「外部にデータを渡す出口」です。

集計結果を CSV で出力して Excel で確認してもらう。
別システムにインポートしてもらうためのファイルを出力する。
ログやレポートを人間が読める形で残しておく。

こういう場面で、「ちゃんとした CSV を書けるかどうか」が効いてきます。
ここでは、初心者向けに、シンプルな書き込みから「CSV ならではの罠(カンマ・改行・ダブルクォート)」、
そして実務で使えるユーティリティの形まで、例題付きで解説していきます。


まずは超シンプル版:カンマ区切りで行を書くだけ

「カンマも改行も含まない」前提なら、これはアリ

値の中にカンマも改行もダブルクォートも出てこない、
という前提が守られているなら、まずはこんなレベルから始められます。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

public static class SimpleCsvWriter
{
    public static void WriteLines(
        string path,
        IEnumerable<string[]> rows,
        Encoding encoding)
    {
        string? dir = Path.GetDirectoryName(path);
        if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }

        using var writer = new StreamWriter(path, false, encoding);

        foreach (var columns in rows)
        {
            string line = string.Join(",", columns);
            writer.WriteLine(line);
        }
    }
}
C#

使い方の例です。

var rows = new List<string[]>
{
    new[] { "Id", "Name", "Price" },
    new[] { "1", "Apple", "120" },
    new[] { "2", "Banana", "80" },
};

SimpleCsvWriter.WriteLines(
    @"C:\data\items.csv",
    rows,
    Encoding.UTF8);
C#

ここでの前提はかなり重要です。
この実装は「値の中にカンマや改行が出てこない」ことを信じ切っています。
もし「東京都,千代田区」のような値が出てきた瞬間、CSV として壊れます。

だからこれは「簡易 CSV 用」と割り切るか、
「値にカンマは絶対に入れない」という仕様があるときだけ使う、という位置づけにしておくのが安全です。


CSV ならではの罠:カンマ・改行・ダブルクォート

なぜ「エスケープ」が必要になるのか

CSV の正式なルールでは、
値の中にカンマや改行、ダブルクォートが含まれる場合、
その値をダブルクォートで囲み、さらに中のダブルクォートは二重にします。

例えば、次のような値です。

Tokyo, Japan
Line1
Line2
He said "Hello"

これらを CSV として正しく書くと、こうなります。

"Tokyo, Japan"
"Line1
Line2"
"He said ""Hello"""

つまり、CSV に書き出すときは、

  1. 値の中にカンマ・改行・ダブルクォートがあるかチェックする
  2. あればダブルクォートで囲む
  3. 中のダブルクォートは "" に置き換える

という処理が必要になります。

これをやらないと、「読み込んだ側が正しくパースできない CSV」になってしまいます。


正しい CSV 1セル分を作る「エスケープ関数」を用意する

まずは「1 つの値」を CSV 用に整形する

行全体を作る前に、「1 つの値を CSV セルとして安全な形にする」関数を用意しておくと、
後のコードがすごくスッキリします。

using System;

public static class CsvEscaping
{
    public static string Escape(string? value)
    {
        if (value is null)
        {
            return string.Empty;
        }

        bool containsSpecial =
            value.Contains(',') ||
            value.Contains('"') ||
            value.Contains('\r') ||
            value.Contains('\n');

        if (!containsSpecial)
        {
            return value;
        }

        string escaped = value.Replace("\"", "\"\"");

        return $"\"{escaped}\"";
    }
}
C#

ポイントを整理します。

値が null のときは空文字にする
CSV では「空セル」として扱われます。

カンマ・ダブルクォート・改行が含まれているかチェックする
これらが含まれていなければ、そのまま出して OK です。

ダブルクォートを "" に置き換える
"Hello"""Hello"" ではなく、
He said "Hello"He said ""Hello"" のように、
中の " だけを二重にします。

最後に全体を " で囲む
これで CSV として安全な 1 セル分の文字列になります。


エスケープを使った「ちゃんとした CSV 行」の書き込み

1 行分の string[] を安全に CSV にする

先ほどの Escape を使えば、
「1 行分の列配列」から「正しい CSV 行」を簡単に作れます。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

public static class CsvWriter
{
    public static void WriteLines(
        string path,
        IEnumerable<string[]> rows,
        Encoding encoding)
    {
        string? dir = Path.GetDirectoryName(path);
        if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }

        using var writer = new StreamWriter(path, false, encoding);

        foreach (var columns in rows)
        {
            string[] escaped = new string[columns.Length];

            for (int i = 0; i < columns.Length; i++)
            {
                escaped[i] = CsvEscaping.Escape(columns[i]);
            }

            string line = string.Join(",", escaped);
            writer.WriteLine(line);
        }
    }
}
C#

使い方の例です(カンマや改行を含む値をわざと入れてみます)。

var rows = new List<string[]>
{
    new[] { "Id", "Name", "Note" },
    new[] { "1", "Apple", "Simple fruit" },
    new[] { "2", "Orange, Big", "Has comma in name" },
    new[] { "3", "MultiLine", "Line1\nLine2" },
    new[] { "4", "Quote", "He said \"Hello\"" },
};

CsvWriter.WriteLines(
    @"C:\data\items_safe.csv",
    rows,
    Encoding.UTF8);
C#

この出力は、CSV として正しく読み込める形になります。
ここまで来ると、「CSV として外部に渡しても恥ずかしくない」レベルです。


型付きオブジェクトから CSV を書く

「string[] を組み立てる」部分を隠蔽する

実務では、string[] を直接扱うより、
「業務クラス → CSV 行」の変換をどこかに閉じ込めておいたほうが、
呼び出し側のコードがきれいになります。

例えば、こんなレコードクラスがあるとします。

public sealed class ItemRecord
{
    public int Id { get; }
    public string Name { get; }
    public int Price { get; }
    public string? Note { get; }

    public ItemRecord(int id, string name, int price, string? note)
    {
        Id = id;
        Name = name;
        Price = price;
        Note = note;
    }
}
C#

これを CSV に書き出すユーティリティを用意します。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

public static class ItemCsvWriter
{
    public static void WriteItems(
        string path,
        IEnumerable<ItemRecord> items,
        Encoding encoding)
    {
        string? dir = Path.GetDirectoryName(path);
        if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }

        using var writer = new StreamWriter(path, false, encoding);

        writer.WriteLine("Id,Name,Price,Note");

        foreach (var item in items)
        {
            string[] columns =
            {
                item.Id.ToString(),
                item.Name,
                item.Price.ToString(),
                item.Note ?? string.Empty
            };

            string[] escaped = new string[columns.Length];
            for (int i = 0; i < columns.Length; i++)
            {
                escaped[i] = CsvEscaping.Escape(columns[i]);
            }

            string line = string.Join(",", escaped);
            writer.WriteLine(line);
        }
    }
}
C#

使い方の例です。

var items = new List<ItemRecord>
{
    new ItemRecord(1, "Apple", 120, null),
    new ItemRecord(2, "Orange, Big", 200, "Has comma"),
    new ItemRecord(3, "MultiLine", 300, "Line1\nLine2"),
};

ItemCsvWriter.WriteItems(
    @"C:\data\items_export.csv",
    items,
    Encoding.UTF8);
C#

呼び出し側は「ItemRecord の列を渡すだけ」で済み、
CSV の細かいルール(エスケープ、カンマ、改行)はユーティリティ側に閉じ込められます。


ストリーミングで書く:大量データでも耐えられる形にする

「全部 List にためてから書く」より「流しながら書く」

データ件数が少ないうちは、
List<string[]> に全部ためてから一気に書く、でも問題ありません。

ただ、数十万件〜数百万件のデータを CSV に出力する場合、
メモリに全部ためるのは無駄です。

さっきの ItemCsvWriter のように、
IEnumerable<T> を受け取り、foreach で回しながら 1 行ずつ書いていくスタイルにしておけば、
呼び出し側が yield return でデータを流してくるような設計にも対応できます。

例えば、DB から 1 件ずつ読みながら CSV に書き出す、
といった処理も、同じインターフェースで書けます。


エンコーディングと BOM をどうするか

「外部システムが何を期待しているか」を最優先する

CSV 書き込みで必ず出てくるのが、「エンコーディングどうする問題」です。

UTF-8(BOM 付き/なし)
Shift_JIS(CP932)

など、相手のシステムや Excel の挙動によって、
「これで出してほしい」という期待値が変わります。

C# 側では、StreamWriter に渡す Encoding を変えるだけで制御できます。

UTF-8(BOM 付き)で書きたい場合。

var utf8WithBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);

using var writer = new StreamWriter(path, false, utf8WithBom);
C#

Shift_JIS で書きたい場合。

var sjis = Encoding.GetEncoding(932);

using var writer = new StreamWriter(path, false, sjis);
C#

「何も考えずにデフォルトで書く」のではなく、
「この CSV はどこでどう使われるか」を意識して、
エンコーディングを明示する癖をつけておくと、後で助かります。


例題:売上データを CSV でエクスポートする

最後に、少し“業務っぽい”例をまとめてみます。

売上レコードクラス。

public sealed class SalesRecord
{
    public int Id { get; }
    public DateTime Date { get; }
    public string Customer { get; }
    public int Amount { get; }

    public SalesRecord(int id, DateTime date, string customer, int amount)
    {
        Id = id;
        Date = date;
        Customer = customer;
        Amount = amount;
    }
}
C#

CSV 書き出しユーティリティ。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

public static class SalesCsvWriter
{
    public static void WriteSales(
        string path,
        IEnumerable<SalesRecord> records,
        Encoding encoding)
    {
        string? dir = Path.GetDirectoryName(path);
        if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }

        using var writer = new StreamWriter(path, false, encoding);

        writer.WriteLine("Id,Date,Customer,Amount");

        foreach (var r in records)
        {
            string[] columns =
            {
                r.Id.ToString(),
                r.Date.ToString("yyyy-MM-dd"),
                r.Customer,
                r.Amount.ToString()
            };

            string[] escaped = new string[columns.Length];
            for (int i = 0; i < columns.Length; i++)
            {
                escaped[i] = CsvEscaping.Escape(columns[i]);
            }

            string line = string.Join(",", escaped);
            writer.WriteLine(line);
        }
    }
}
C#

呼び出し側。

var records = new List<SalesRecord>
{
    new SalesRecord(1, new DateTime(2025, 1, 1), "ABC", 1000),
    new SalesRecord(2, new DateTime(2025, 1, 1), "XYZ, Inc.", 2000),
};

SalesCsvWriter.WriteSales(
    @"C:\data\sales_export.csv",
    records,
    Encoding.UTF8);
C#

これで、「Excel で開いても崩れない」「外部システムにも渡せる」レベルの CSV 出力ができます。


まとめ 「CSV書き込みユーティリティ」を自分の味方にする

CSV 書き込みは、「とりあえず string.Join」から始められますが、
一歩踏み込んで「エスケープ」「エンコーディング」「型付きオブジェクトとの橋渡し」まで押さえておくと、
一気に“業務で戦える”感じになります。

大事なポイントをもう一度だけ整理すると、

値の中にカンマ・改行・ダブルクォートが入る前提なら、必ずエスケープ処理を書く。
1 セル分を安全にする Escape 関数を用意しておくと、行生成がシンプルになる。
IEnumerable<T> を受け取って 1 行ずつ書くスタイルにしておくと、大量データにも強い。
業務クラス → CSV 行の変換をユーティリティ側に閉じ込めると、呼び出しコードが読みやすくなる。
エンコーディング(UTF-8 / Shift_JIS、BOM の有無)は「相手が何を期待しているか」から逆算して決める。

このあたりを自分の中で腑に落としておくと、
「CSV で出しておいて」と言われたときに、
落ち着いて“ちゃんとした CSV”を書けるエンジニアになれます。

タイトルとURLをコピーしました