はじめに 「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 に書き出すときは、
- 値の中にカンマ・改行・ダブルクォートがあるかチェックする
- あればダブルクォートで囲む
- 中のダブルクォートは
""に置き換える
という処理が必要になります。
これをやらないと、「読み込んだ側が正しくパースできない 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”を書けるエンジニアになれます。
