C#でCSVを読み書きする方法|文字化け・カンマ・改行の悩みを解決する実装ガイド

はじめに

C#でCSVを読み書きする処理は、業務システム、データ連携、ログ出力、Excel取り込み、マスタデータ管理などで頻繁に登場します。一見すると「カンマで区切られたテキストファイルを読み書きするだけ」に見えますが、実際には文字化け、カンマを含む値、ダブルクォート、改行、Excelで開いたときの表示崩れ、大容量ファイルのメモリ不足など、つまずきやすいポイントが多くあります。

C#でCSVを扱う方法には、大きく分けて次の2つがあります。

C#
// 標準クラスで処理する方法
StreamReader
StreamWriter

// ライブラリを使う方法
CsvHelper

単純なCSVであれば、StreamReaderStreamWriterだけでも十分に対応できます。一方、値の中にカンマや改行が含まれるCSV、列のマッピングが必要なCSV、保守性を重視したい実務コードでは、CsvHelperのようなCSVライブラリを使うのが安全です。

この記事では、C#でCSVを読み書きする基本から、文字化け対策、カンマ・改行・ダブルクォートへの対応、CsvHelperの使い方、DataTableやListとの変換、実務向けのサンプルコードまで順番に解説します。

1. C#でCSVを扱う前に知っておきたい基本

1-1. CSVとは何か|C#で扱う場面とよくある用途

CSVは「Comma-Separated Values」の略で、値をカンマで区切って表現するテキスト形式のデータです。

たとえば、次のようなファイルがCSVです。

csv
Id,Name,Age
1,山田太郎,30
2,佐藤花子,25
3,田中一郎,40

C#でCSVを扱う場面は多く、たとえば次のような用途があります。

  • データベースの内容をCSVとしてエクスポートする

  • Excelで作成されたCSVをシステムに取り込む

  • 外部システムとのデータ連携に使う

  • バッチ処理で大量データを読み込む

  • ログや集計結果をCSVとして保存する

  • マスタデータをCSVで管理する

CSVはシンプルで扱いやすい一方、仕様を正しく理解せずに実装すると、列ずれや文字化けが発生しやすい形式でもあります。

1-2. C#でCSVを読み書きする主な方法

C#でCSVを読み書きする方法は、主に次の2つです。

1つ目は、StreamReaderStreamWriterを使って自分で読み書きする方法です。

C#
using var reader = new StreamReader("sample.csv");
string? line = reader.ReadLine();
C#
using var writer = new StreamWriter("sample.csv");
writer.WriteLine("Id,Name,Age");

2つ目は、CsvHelperなどのCSVライブラリを使う方法です。

C#
using var reader = new StreamReader("sample.csv");
using var csv = new CsvHelper.CsvReader(reader, System.Globalization.CultureInfo.InvariantCulture);

単純なCSVなら標準クラスで十分ですが、実務ではカンマ、ダブルクォート、改行、文字コード、ヘッダー有無などを考慮する必要があるため、ライブラリを使ったほうが安全な場面も多いです。

1-3. StreamReader・StreamWriterを使う場合のメリットと注意点

StreamReaderStreamWriterを使うメリットは、C#標準のクラスだけでCSVを読み書きできることです。追加のライブラリをインストールする必要がなく、処理内容も理解しやすいです。

たとえば、単純なCSVの読み込みは次のように書けます。

C#
using var reader = new StreamReader("users.csv");

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (line == null)
{
continue;
}

string[] values = line.Split(',');

Console.WriteLine(values[0]);
}

ただし、Split(',')には大きな注意点があります。値の中にカンマが含まれる場合、正しく分割できません。

csv
Id,Name,Address
1,山田太郎,"東京都渋谷区, 1-2-3"

このようなCSVをSplit(',')で分割すると、住所の中のカンマまで区切りとして扱われ、列がずれてしまいます。

そのため、StreamReaderStreamWriterで処理する場合は、対象のCSVが本当に単純な形式かどうかを確認することが重要です。

1-4. CsvHelperなどのライブラリを使うべきケース

次のようなCSVを扱う場合は、CsvHelperなどのライブラリを使うのがおすすめです。

  • 値の中にカンマが含まれる

  • 値の中にダブルクォートが含まれる

  • 値の中に改行が含まれる

  • ヘッダー名とC#のクラス名をマッピングしたい

  • 列の順番が変わる可能性がある

  • 大量データを安全に処理したい

  • 実務アプリとして保守性を高めたい

  • CSVの仕様に沿った読み書きをしたい

CsvHelperを使うと、次のようにCSVをクラスへ直接変換できます。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
}
C#
using var reader = new StreamReader("users.csv");
using var csv = new CsvHelper.CsvReader(reader, System.Globalization.CultureInfo.InvariantCulture);

List<User> users = csv.GetRecords<User>().ToList();

CSVの列とC#のクラスを対応させられるため、実務コードでは可読性と保守性が高くなります。

1-5. CSV処理で初心者がつまずきやすいポイント

C#のCSV処理で初心者がよくつまずくポイントは、次のとおりです。

  • 日本語が文字化けする

  • Shift_JISのCSVをUTF-8として読んでしまう

  • Excelで開くと文字化けする

  • 値の中にカンマがあり、列がずれる

  • 値の中に改行があり、行が分割される

  • ダブルクォートのエスケープを忘れる

  • ヘッダー行をデータとして処理してしまう

  • ファイルが存在しないと例外になる

  • ファイルがExcelで開かれていて書き込みできない

  • 大容量CSVを一度に読み込み、メモリ不足になる

特に「文字コード」と「CSV仕様」は重要です。C#でCSVを扱うときは、単に文字列をカンマで分割するだけではなく、実際のデータ内容に合わせて処理方法を選ぶ必要があります。

2. C#でCSVファイルを読み込む基本実装

2-1. StreamReaderでCSVファイルを1行ずつ読み込む方法

C#でCSVファイルを読み込む基本は、StreamReaderを使って1行ずつ処理する方法です。

C#
using System.Text;

string path = "users.csv";

using var reader = new StreamReader(path, Encoding.UTF8);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (line == null)
{
continue;
}

Console.WriteLine(line);
}

ReadLine()は1行ずつ読み込むため、大容量CSVでも比較的メモリを抑えて処理できます。

次のようにFile.ReadAllLines()を使う方法もあります。

C#
string[] lines = File.ReadAllLines("users.csv", Encoding.UTF8);

ただし、File.ReadAllLines()はファイル全体を一度にメモリへ読み込みます。小さなCSVなら便利ですが、大容量CSVではメモリ不足の原因になるため注意が必要です。

実務では、基本的にStreamReaderで1行ずつ処理する方法をおすすめします。

2-2. Splitでカンマ区切りの値を配列に変換する方法

単純なCSVであれば、Split(',')でカンマ区切りの値を配列に変換できます。

C#
string line = "1,山田太郎,30";

string[] values = line.Split(',');

Console.WriteLine(values[0]); // 1
Console.WriteLine(values[1]); // 山田太郎
Console.WriteLine(values[2]); // 30

CSVファイルを読み込みながら分割する例は次のとおりです。

C#
using System.Text;

string path = "users.csv";

using var reader = new StreamReader(path, Encoding.UTF8);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (string.IsNullOrWhiteSpace(line))
{
continue;
}

string[] values = line.Split(',');

string id = values[0];
string name = values[1];
string age = values[2];

Console.WriteLine($"{id}: {name} ({age})");
}

ただし、この方法は「値の中にカンマが含まれない」ことが前提です。

csv
1,山田太郎,"東京都渋谷区, 1-2-3"

このようなCSVではSplit(',')だけでは正しく処理できません。カンマや改行を含む可能性があるCSVでは、後述するCsvHelperや専用のパース処理を使うべきです。

2-3. ヘッダー行を除外してデータ行だけ処理する方法

CSVには、1行目に列名が入っていることがよくあります。

csv
Id,Name,Age
1,山田太郎,30
2,佐藤花子,25

この場合、1行目のヘッダーを除外してデータ行だけ処理します。

C#
using System.Text;

string path = "users.csv";

using var reader = new StreamReader(path, Encoding.UTF8);

// 1行目のヘッダーを読み飛ばす
string? header = reader.ReadLine();

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (string.IsNullOrWhiteSpace(line))
{
continue;
}

string[] values = line.Split(',');

Console.WriteLine($"ID: {values[0]}, Name: {values[1]}, Age: {values[2]}");
}

行番号を使ってヘッダーを判定する方法もあります。

C#
using System.Text;

string path = "users.csv";
int lineNumber = 0;

using var reader = new StreamReader(path, Encoding.UTF8);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();
lineNumber++;

if (lineNumber == 1)
{
continue;
}

if (string.IsNullOrWhiteSpace(line))
{
continue;
}

string[] values = line.Split(',');
Console.WriteLine(values[1]);
}

ヘッダー行があるかどうかはCSVによって異なるため、処理前に仕様を確認しておきましょう。

2-4. 読み込んだCSVデータをListやクラスに格納する方法

CSVを読み込んだ後は、配列のまま扱うよりも、クラスに変換してList<T>へ格納すると扱いやすくなります。

まず、CSVの1行に対応するクラスを作成します。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
}

CSVを読み込み、List<User>に格納します。

C#
using System.Text;

string path = "users.csv";
List<User> users = new();

using var reader = new StreamReader(path, Encoding.UTF8);

// ヘッダー行を読み飛ばす
reader.ReadLine();

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (string.IsNullOrWhiteSpace(line))
{
continue;
}

string[] values = line.Split(',');

User user = new User
{
Id = int.Parse(values[0]),
Name = values[1],
Age = int.Parse(values[2])
};

users.Add(user);
}

foreach (User user in users)
{
Console.WriteLine($"{user.Id}: {user.Name} - {user.Age}");
}

実務では、int.Parse()で変換に失敗すると例外が発生するため、int.TryParse()を使うとより安全です。

C#
if (!int.TryParse(values[0], out int id))
{
Console.WriteLine("IDの形式が不正です。");
continue;
}

if (!int.TryParse(values[2], out int age))
{
Console.WriteLine("Ageの形式が不正です。");
continue;
}

CSVの内容は外部データであることが多いため、常に不正な値が入る可能性を考えて実装することが大切です。

2-5. ファイルが存在しない場合の例外処理

CSVファイルを読み込むとき、指定したファイルが存在しないとFileNotFoundExceptionが発生します。

事前にFile.Exists()で確認すると安全です。

C#
using System.Text;

string path = "users.csv";

if (!File.Exists(path))
{
Console.WriteLine("CSVファイルが存在しません。");
return;
}

using var reader = new StreamReader(path, Encoding.UTF8);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();
Console.WriteLine(line);
}

try-catchで例外処理を行う方法もあります。

C#
using System.Text;

string path = "users.csv";

try
{
using var reader = new StreamReader(path, Encoding.UTF8);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();
Console.WriteLine(line);
}
}
catch (FileNotFoundException)
{
Console.WriteLine("CSVファイルが見つかりません。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("CSVファイルへのアクセス権限がありません。");
}
catch (IOException ex)
{
Console.WriteLine($"CSVファイルの読み込み中にエラーが発生しました: {ex.Message}");
}

実務では、ファイルの存在確認だけでなく、アクセス権限、ファイルロック、文字コード、データ形式の不正も考慮しましょう。

3. C#でCSVファイルを書き込む基本実装

3-1. StreamWriterでCSVファイルを作成する方法

C#でCSVファイルを書き込む基本は、StreamWriterを使います。

C#
using System.Text;

string path = "users.csv";

using var writer = new StreamWriter(path, false, Encoding.UTF8);

writer.WriteLine("Id,Name,Age");
writer.WriteLine("1,山田太郎,30");
writer.WriteLine("2,佐藤花子,25");

StreamWriterの第2引数は追記するかどうかを指定します。

C#
new StreamWriter(path, false, Encoding.UTF8); // 上書き
new StreamWriter(path, true, Encoding.UTF8); // 追記

CSVを新規作成したい場合はfalse、既存ファイルに追記したい場合はtrueを指定します。

3-2. Listや配列のデータをCSV形式で出力する方法

配列やListのデータをCSV形式で出力する場合、string.Join()を使うと簡単です。

C#
using System.Text;

string path = "users.csv";

List<string[]> rows = new()
{
new[] { "1", "山田太郎", "30" },
new[] { "2", "佐藤花子", "25" },
new[] { "3", "田中一郎", "40" }
};

using var writer = new StreamWriter(path, false, Encoding.UTF8);

foreach (string[] row in rows)
{
string line = string.Join(",", row);
writer.WriteLine(line);
}

クラスのListをCSVとして出力する例です。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
}
C#
using System.Text;

string path = "users.csv";

List<User> users = new()
{
new User { Id = 1, Name = "山田太郎", Age = 30 },
new User { Id = 2, Name = "佐藤花子", Age = 25 }
};

using var writer = new StreamWriter(path, false, Encoding.UTF8);

foreach (User user in users)
{
writer.WriteLine($"{user.Id},{user.Name},{user.Age}");
}

ただし、この方法も値の中にカンマや改行が含まれないことが前提です。安全なCSV出力を行う場合は、後述するエスケープ処理を入れる必要があります。

3-3. ヘッダー行付きのCSVを書き出す方法

CSVをExcelや別システムで扱う場合、1行目にヘッダーを付けることがよくあります。

C#
using System.Text;

string path = "users.csv";

List<User> users = new()
{
new User { Id = 1, Name = "山田太郎", Age = 30 },
new User { Id = 2, Name = "佐藤花子", Age = 25 }
};

using var writer = new StreamWriter(path, false, Encoding.UTF8);

// ヘッダー行
writer.WriteLine("Id,Name,Age");

// データ行
foreach (User user in users)
{
writer.WriteLine($"{user.Id},{user.Name},{user.Age}");
}

ヘッダー名は、読み込む側の仕様に合わせる必要があります。

たとえば、C#のプロパティ名はNameでも、CSVの列名は日本語で氏名にしたい場合があります。

C#
writer.WriteLine("ID,氏名,年齢");

業務システムでは、CSVの列名が外部システムとの連携仕様になっていることも多いため、勝手に変更しないよう注意しましょう。

3-4. 既存CSVに追記する方法

既存のCSVにデータを追記する場合は、StreamWriterの第2引数にtrueを指定します。

C#
using System.Text;

string path = "users.csv";

using var writer = new StreamWriter(path, true, Encoding.UTF8);

writer.WriteLine("3,田中一郎,40");

ただし、追記時に毎回ヘッダーを書いてしまうと、ファイルの途中にヘッダー行が混ざってしまいます。

csv
Id,Name,Age
1,山田太郎,30
2,佐藤花子,25
Id,Name,Age
3,田中一郎,40

そのため、ファイルが存在しない場合だけヘッダーを書き込むようにすると便利です。

C#
using System.Text;

string path = "users.csv";
bool exists = File.Exists(path);

using var writer = new StreamWriter(path, true, Encoding.UTF8);

if (!exists)
{
writer.WriteLine("Id,Name,Age");
}

writer.WriteLine("3,田中一郎,40");

既存ファイルが空の場合もヘッダーを書きたい場合は、ファイルサイズも確認します。

C#
bool shouldWriteHeader = !File.Exists(path) || new FileInfo(path).Length == 0;

using var writer = new StreamWriter(path, true, Encoding.UTF8);

if (shouldWriteHeader)
{
writer.WriteLine("Id,Name,Age");
}

writer.WriteLine("3,田中一郎,40");

3-5. 書き込み時の例外処理とファイルロック対策

CSVを書き込むときは、ファイルがExcelなどで開かれているとIOExceptionが発生することがあります。

C#
using System.Text;

string path = "users.csv";

try
{
using var writer = new StreamWriter(path, false, Encoding.UTF8);

writer.WriteLine("Id,Name,Age");
writer.WriteLine("1,山田太郎,30");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("CSVファイルへの書き込み権限がありません。");
}
catch (IOException ex)
{
Console.WriteLine($"CSVファイルに書き込めません。ファイルが開かれている可能性があります: {ex.Message}");
}

ファイルロックを避けるためには、次の点に注意します。

  • usingを使ってファイルを確実に閉じる

  • ExcelでCSVを開いたまま書き込まない

  • 同じファイルに複数プロセスから同時に書き込まない

  • 一時ファイルに書き出してから置き換える

  • 書き込み中に例外が起きた場合のログを残す

実務では、CSV出力中にアプリが停止すると中途半端なファイルが残ることがあります。その対策として、一度.tmpファイルに出力し、成功後に正式ファイルへリネームする設計も有効です。

C#
using System.Text;

string path = "users.csv";
string tempPath = path + ".tmp";

try
{
using (var writer = new StreamWriter(tempPath, false, Encoding.UTF8))
{
writer.WriteLine("Id,Name,Age");
writer.WriteLine("1,山田太郎,30");
}

File.Copy(tempPath, path, true);
File.Delete(tempPath);
}
catch (IOException ex)
{
Console.WriteLine($"CSV出力に失敗しました: {ex.Message}");
}

4. CSVの文字化けを防ぐエンコーディング設定

4-1. C#でCSVが文字化けする原因

C#でCSVが文字化けする主な原因は、読み込み時または書き込み時の文字コードが実際のCSVファイルと一致していないことです。

たとえば、Shift_JISで保存されたCSVをUTF-8として読み込むと、日本語が文字化けすることがあります。

C#
// Shift_JISのCSVをUTF-8として読んでしまう例
using var reader = new StreamReader("users.csv", Encoding.UTF8);

また、C#でUTF-8のCSVを出力しても、Excel側がShift_JISとして開こうとして文字化けするケースがあります。

CSVでよく使われる文字コードには、次のようなものがあります。

文字コード特徴
UTF-8現在よく使われる文字コード。Webや.NETで扱いやすい
UTF-8 BOM付きExcelで開いたときに文字化けを防ぎやすい
Shift_JIS日本語Windows環境や古い業務システムで使われることが多い
UTF-16Excelとの相性がよい場合もあるが、CSVとしては扱いに注意が必要

C#でCSVを読み書きするときは、文字コードを明示するのが基本です。

4-2. UTF-8でCSVを読み書きする方法

UTF-8でCSVを読み込む場合は、StreamReaderEncoding.UTF8を指定します。

C#
using System.Text;

string path = "users.csv";

using var reader = new StreamReader(path, Encoding.UTF8);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();
Console.WriteLine(line);
}

UTF-8でCSVを書き込む場合は、StreamWriterEncoding.UTF8を指定します。

C#
using System.Text;

string path = "users.csv";

using var writer = new StreamWriter(path, false, Encoding.UTF8);

writer.WriteLine("Id,Name,Age");
writer.WriteLine("1,山田太郎,30");

BOMなしUTF-8を明示したい場合は、次のようにします。

C#
using System.Text;

var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

using var writer = new StreamWriter("users.csv", false, utf8NoBom);

writer.WriteLine("Id,Name,Age");
writer.WriteLine("1,山田太郎,30");

Excelで直接開くことを重視する場合は、BOM付きUTF-8を使うこともあります。

C#
using System.Text;

var utf8WithBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);

using var writer = new StreamWriter("users.csv", false, utf8WithBom);

writer.WriteLine("Id,Name,Age");
writer.WriteLine("1,山田太郎,30");

4-3. Shift_JISのCSVを読み込む方法

日本語の業務システムでは、Shift_JISのCSVを扱うことがあります。

.NET Frameworkでは次のように読み込めます。

C#
using System.Text;

Encoding shiftJis = Encoding.GetEncoding("shift_jis");

using var reader = new StreamReader("users.csv", shiftJis);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();
Console.WriteLine(line);
}

.NET Coreや.NET 5以降でShift_JISを扱う場合、環境によってはEncoding.RegisterProviderが必要です。

C#
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

Encoding shiftJis = Encoding.GetEncoding("shift_jis");

using var reader = new StreamReader("users.csv", shiftJis);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();
Console.WriteLine(line);
}

書き込み時も同様です。

C#
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

Encoding shiftJis = Encoding.GetEncoding("shift_jis");

using var writer = new StreamWriter("users.csv", false, shiftJis);

writer.WriteLine("Id,Name,Age");
writer.WriteLine("1,山田太郎,30");

ただし、Shift_JISでは扱えない文字もあります。絵文字、特殊記号、機種依存文字などが含まれる可能性がある場合は、UTF-8を検討しましょう。

4-4. Excelで開いたときに文字化けしないCSVを出力する方法

ExcelでCSVを直接開いたときに文字化けする場合、BOM付きUTF-8で出力すると改善することがあります。

C#
using System.Text;

string path = "users.csv";
var utf8WithBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);

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

writer.WriteLine("Id,Name,Age");
writer.WriteLine("1,山田太郎,30");
writer.WriteLine("2,佐藤花子,25");

古いExcelや特定の業務環境では、Shift_JISのほうが期待どおりに開けることもあります。

C#
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

Encoding shiftJis = Encoding.GetEncoding("shift_jis");

using var writer = new StreamWriter("users.csv", false, shiftJis);

writer.WriteLine("Id,Name,Age");
writer.WriteLine("1,山田太郎,30");

どちらを選ぶべきかは、CSVを利用する相手によって変わります。

  • Webシステムや新しいアプリとの連携ならUTF-8

  • Excelで直接開くことが多いならBOM付きUTF-8

  • 古い業務システムとの連携ならShift_JIS

  • 仕様書で指定されている場合は仕様書を優先

CSV出力では、文字コードを暗黙にせず、必ず明示することが重要です。

4-5. Encoding.RegisterProviderが必要になるケース

.NET Coreや.NET 5以降でEncoding.GetEncoding("shift_jis")を使う場合、次のような例外が発生することがあります。

System.ArgumentException: 'shift_jis' is not a supported encoding name.

この場合は、System.Text.Encoding.CodePagesパッケージを追加し、次のコードを実行します。

C#
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

その後、Shift_JISを取得できます。

C#
Encoding shiftJis = Encoding.GetEncoding("shift_jis");

コンソールアプリであれば、Mainメソッドの最初に書いておくとよいです。

C#
using System.Text;

class Program
{
static void Main()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

Encoding shiftJis = Encoding.GetEncoding("shift_jis");

using var reader = new StreamReader("users.csv", shiftJis);

while (!reader.EndOfStream)
{
Console.WriteLine(reader.ReadLine());
}
}
}

Shift_JISのCSVを扱う可能性があるプロジェクトでは、早い段階で文字コード対応を決めておきましょう。

5. カンマ・ダブルクォート・改行を含むCSVの正しい扱い方

5-1. Splitだけでは正しく処理できないCSVの例

CSV処理でよくある失敗が、すべてのCSVをSplit(',')で処理してしまうことです。

次のCSVを考えてみます。

csv
Id,Name,Address
1,山田太郎,"東京都渋谷区, 1-2-3"

Split(',')で分割すると、住所の中のカンマまで区切り文字として扱われます。

C#
string line = "1,山田太郎,\"東京都渋谷区, 1-2-3\"";

string[] values = line.Split(',');

Console.WriteLine(values.Length); // 4になってしまう

本来は3列ですが、4列として扱われてしまいます。

また、次のように改行を含む値もあります。

csv
Id,Name,Memo
1,山田太郎,"1行目
2行目"

この場合、ReadLine()で1行ずつ読み込むだけでは、1レコードが途中で分割されてしまいます。

このようなCSVを扱う場合は、CSV仕様に沿ったパーサーを使う必要があります。実務ではCsvHelperの利用をおすすめします。

5-2. カンマを含む値をダブルクォートで囲む方法

CSVでは、値の中にカンマが含まれる場合、その値をダブルクォートで囲みます。

csv
Id,Name,Address
1,山田太郎,"東京都渋谷区, 1-2-3"

C#で出力する場合は、値にカンマが含まれているかを判定し、必要に応じてダブルクォートで囲みます。

C#
static string EscapeCsvValue(string? value)
{
if (value == null)
{
return "";
}

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

if (value.Contains('"'))
{
value = value.Replace("\"", "\"\"");
}

return mustQuote ? $"\"{value}\"" : value;
}

使い方は次のとおりです。

C#
string address = "東京都渋谷区, 1-2-3";

string line = string.Join(",", new[]
{
EscapeCsvValue("1"),
EscapeCsvValue("山田太郎"),
EscapeCsvValue(address)
});

Console.WriteLine(line);

出力結果は次のようになります。

csv
1,山田太郎,"東京都渋谷区, 1-2-3"

CSVを書き出すときは、すべての値に対してエスケープ関数を通すと安全です。

5-3. ダブルクォートを含む値のエスケープ方法

CSVでは、値の中にダブルクォートが含まれる場合、ダブルクォートを2つにしてエスケープします。

たとえば、次の値があるとします。

彼は"OK"と言った

CSVでは次のように表現します。

csv
"彼は""OK""と言った"

C#ではReplace("\"", "\"\"")でエスケープできます。

C#
string text = "彼は\"OK\"と言った";

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

Console.WriteLine($"\"{escaped}\"");

CSV出力用の関数としてまとめると、次のようになります。

C#
static string EscapeCsvValue(string? value)
{
if (value == null)
{
return "";
}

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

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

return mustQuote ? $"\"{value}\"" : value;
}

この関数を使えば、カンマ、ダブルクォート、改行を含む値を安全にCSV出力できます。

5-4. 改行を含むフィールドを扱うときの注意点

CSVでは、値の中に改行を含めることもできます。その場合、フィールド全体をダブルクォートで囲みます。

csv
Id,Name,Memo
1,山田太郎,"1行目
2行目
3行目"

C#でこのような値を出力する場合も、改行を含む値をダブルクォートで囲む必要があります。

C#
string memo = "1行目\n2行目\n3行目";

string line = string.Join(",", new[]
{
EscapeCsvValue("1"),
EscapeCsvValue("山田太郎"),
EscapeCsvValue(memo)
});

Console.WriteLine(line);

ただし、読み込み側では注意が必要です。

StreamReader.ReadLine()は物理的な1行を読み込むため、改行を含むフィールドがある場合、CSVの1レコードを正しく読み取れません。

つまり、次のようなデータでは、1つのレコードが複数行にまたがります。

csv
1,山田太郎,"1行目
2行目"

このようなCSVを正しく読み込むには、自前でクォート状態を管理するか、CsvHelperのようなCSVパーサーを使う必要があります。

実務では、改行を含むフィールドがある可能性が少しでもあるなら、SplitReadLineだけで処理しないほうが安全です。

5-5. RFC 4180に沿ったCSV処理の考え方

CSVには、一般的にRFC 4180に沿った考え方があります。すべてのCSVが厳密にRFC 4180準拠とは限りませんが、実装時の基準として理解しておくと便利です。

主なポイントは次のとおりです。

  • 1レコードは通常1行で表す

  • フィールドはカンマで区切る

  • フィールドにカンマ、ダブルクォート、改行が含まれる場合はダブルクォートで囲む

  • フィールド内のダブルクォートは2つのダブルクォートで表す

  • ヘッダー行は任意

  • 改行コードはCRLFが使われることが多い

たとえば、次のCSVはカンマ、ダブルクォート、改行を含む値を表しています。

csv
Id,Name,Memo
1,山田太郎,"東京都渋谷区, 1-2-3"
2,佐藤花子,"彼は""OK""と言った"
3,田中一郎,"1行目
2行目"

このようなCSVを安全に扱うには、CSV仕様に沿った処理が必要です。

単純なデータならSplitでも対応できますが、実務では「将来的に値の中にカンマや改行が入るかもしれない」と考えて設計することが大切です。

6. CsvHelperを使ってCSVを安全に読み書きする方法

6-1. CsvHelperを導入する方法

CsvHelperは、C#でCSVを扱うための代表的なライブラリです。カンマ、ダブルクォート、改行、ヘッダー、クラスマッピングなどを安全に扱えます。

NuGetでインストールします。

Bash
dotnet add package CsvHelper

Visual Studioを使っている場合は、NuGetパッケージマネージャーからCsvHelperを検索して追加できます。

CsvHelperを使うときは、主に次の名前空間を使用します。

C#
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
using System.Text;

基本的には、CsvReaderで読み込み、CsvWriterで書き込みます。

6-2. CsvHelperでCSVを読み込む基本コード

まず、CSVに対応するクラスを用意します。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
}

CSVファイルは次のような形式とします。

csv
Id,Name,Age
1,山田太郎,30
2,佐藤花子,25

CsvHelperで読み込むコードは次のとおりです。

C#
using CsvHelper;
using System.Globalization;
using System.Text;

string path = "users.csv";

using var reader = new StreamReader(path, Encoding.UTF8);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

List<User> users = csv.GetRecords<User>().ToList();

foreach (User user in users)
{
Console.WriteLine($"{user.Id}: {user.Name} ({user.Age})");
}

GetRecords<User>()を使うと、CSVの各行をUserクラスのインスタンスとして取得できます。

ヘッダー名とプロパティ名が一致していれば、自動的にマッピングされます。

6-3. CsvHelperでCSVを書き出す基本コード

CsvHelperでCSVを書き出す場合は、CsvWriterを使います。

C#
using CsvHelper;
using System.Globalization;
using System.Text;

string path = "users.csv";

List<User> users = new()
{
new User { Id = 1, Name = "山田太郎", Age = 30 },
new User { Id = 2, Name = "佐藤花子", Age = 25 }
};

using var writer = new StreamWriter(path, false, Encoding.UTF8);
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);

csv.WriteRecords(users);

このコードを実行すると、次のようなCSVが出力されます。

csv
Id,Name,Age
1,山田太郎,30
2,佐藤花子,25

CsvHelperは、値にカンマやダブルクォートが含まれている場合でも、必要に応じて正しくエスケープしてくれます。

たとえば、名前やメモにカンマが含まれていても、安全にCSVとして出力できます。

6-4. クラスとCSV列をマッピングする方法

CSVのヘッダー名とC#のプロパティ名が一致しない場合は、マッピングクラスを使います。

たとえば、CSVのヘッダーが日本語の場合を考えます。

csv
ID,氏名,年齢
1,山田太郎,30
2,佐藤花子,25

C#のクラスは次のように英語のプロパティ名にしておきます。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
}

マッピングクラスを作成します。

C#
using CsvHelper.Configuration;

public sealed class UserMap : ClassMap<User>
{
public UserMap()
{
Map(m => m.Id).Name("ID");
Map(m => m.Name).Name("氏名");
Map(m => m.Age).Name("年齢");
}
}

読み込み時にマッピングを登録します。

C#
using CsvHelper;
using System.Globalization;
using System.Text;

using var reader = new StreamReader("users.csv", Encoding.UTF8);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

csv.Context.RegisterClassMap<UserMap>();

List<User> users = csv.GetRecords<User>().ToList();

書き込み時にも同じマッピングを利用できます。

C#
using CsvHelper;
using System.Globalization;
using System.Text;

List<User> users = new()
{
new User { Id = 1, Name = "山田太郎", Age = 30 }
};

using var writer = new StreamWriter("users.csv", false, Encoding.UTF8);
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);

csv.Context.RegisterClassMap<UserMap>();
csv.WriteRecords(users);

マッピングを使うと、CSV仕様とC#のクラス設計を分離できるため、保守しやすくなります。

6-5. 文字コードやヘッダー有無を設定する方法

CsvHelperでも、文字コードはStreamReaderStreamWriter側で指定します。

UTF-8で読み込む場合です。

C#
using var reader = new StreamReader("users.csv", Encoding.UTF8);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

Shift_JISで読み込む場合です。

C#
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

Encoding shiftJis = Encoding.GetEncoding("shift_jis");

using var reader = new StreamReader("users.csv", shiftJis);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

ヘッダーがないCSVを読み込む場合は、CsvConfigurationHasHeaderRecordfalseにします。

C#
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
using System.Text;

var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = false
};

using var reader = new StreamReader("users.csv", Encoding.UTF8);
using var csv = new CsvReader(reader, config);

List<User> users = csv.GetRecords<User>().ToList();

ヘッダーなしCSVの場合、列の順番でマッピングする必要があります。

C#
using CsvHelper.Configuration;

public sealed class UserNoHeaderMap : ClassMap<User>
{
public UserNoHeaderMap()
{
Map(m => m.Id).Index(0);
Map(m => m.Name).Index(1);
Map(m => m.Age).Index(2);
}
}
C#
csv.Context.RegisterClassMap<UserNoHeaderMap>();
List<User> users = csv.GetRecords<User>().ToList();

6-6. カンマ・改行・ダブルクォートを含むCSVへの対応

CsvHelperを使う大きなメリットは、カンマ、改行、ダブルクォートを含むCSVを安全に扱えることです。

たとえば、次のようなクラスを用意します。

C#
public class Note
{
public int Id { get; set; }
public string Text { get; set; } = "";
}

次のようなデータを書き出します。

C#
using CsvHelper;
using System.Globalization;
using System.Text;

List<Note> notes = new()
{
new Note { Id = 1, Text = "東京都渋谷区, 1-2-3" },
new Note { Id = 2, Text = "彼は\"OK\"と言った" },
new Note { Id = 3, Text = "1行目\n2行目" }
};

using var writer = new StreamWriter("notes.csv", false, Encoding.UTF8);
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);

csv.WriteRecords(notes);

出力されるCSVでは、必要な値が自動的にダブルクォートで囲まれ、ダブルクォートも正しくエスケープされます。

読み込みも同様に、CsvHelperがCSV仕様に沿って処理してくれます。

C#
using CsvHelper;
using System.Globalization;
using System.Text;

using var reader = new StreamReader("notes.csv", Encoding.UTF8);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

List<Note> notes = csv.GetRecords<Note>().ToList();

foreach (Note note in notes)
{
Console.WriteLine(note.Text);
}

Splitでは壊れやすいCSVでも、CsvHelperを使えば安全に読み書きできます。

7. DataTableやListとCSVを相互変換する方法

7-1. CSVをDataTableに読み込む方法

DataTableは、列と行を持つ表形式データとしてCSVを扱いたい場合に便利です。

単純なCSVをDataTableに読み込む例です。

C#
using System.Data;
using System.Text;

static DataTable ReadCsvToDataTable(string path, Encoding encoding)
{
DataTable table = new();

using var reader = new StreamReader(path, encoding);

string? headerLine = reader.ReadLine();

if (headerLine == null)
{
return table;
}

string[] headers = headerLine.Split(',');

foreach (string header in headers)
{
table.Columns.Add(header);
}

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (string.IsNullOrWhiteSpace(line))
{
continue;
}

string[] values = line.Split(',');
table.Rows.Add(values);
}

return table;
}

使い方は次のとおりです。

C#
DataTable table = ReadCsvToDataTable("users.csv", Encoding.UTF8);

foreach (DataRow row in table.Rows)
{
Console.WriteLine(row["Name"]);
}

ただし、この実装はSplit(',')を使っているため、カンマや改行を含むCSVには向いていません。複雑なCSVをDataTableに読み込む場合は、CsvHelperで読み込んでからDataTableに変換するほうが安全です。

7-2. DataTableをCSVとして出力する方法

DataTableをCSVとして出力する場合は、列名をヘッダーとして出力し、各行を順番に書き込みます。

C#
using System.Data;
using System.Text;

static void WriteDataTableToCsv(DataTable table, string path, Encoding encoding)
{
using var writer = new StreamWriter(path, false, encoding);

IEnumerable<string> headers = table.Columns
.Cast<DataColumn>()
.Select(column => EscapeCsvValue(column.ColumnName));

writer.WriteLine(string.Join(",", headers));

foreach (DataRow row in table.Rows)
{
IEnumerable<string> values = table.Columns
.Cast<DataColumn>()
.Select(column => EscapeCsvValue(row[column]?.ToString()));

writer.WriteLine(string.Join(",", values));
}
}

static string EscapeCsvValue(string? value)
{
if (value == null)
{
return "";
}

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

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

return mustQuote ? $"\"{value}\"" : value;
}

使い方は次のとおりです。

C#
DataTable table = new();
table.Columns.Add("Id");
table.Columns.Add("Name");
table.Columns.Add("Memo");

table.Rows.Add("1", "山田太郎", "東京都渋谷区, 1-2-3");
table.Rows.Add("2", "佐藤花子", "1行目\n2行目");

WriteDataTableToCsv(table, "users.csv", Encoding.UTF8);

このように、CSV出力時には必ずエスケープ処理を入れると安全です。

7-3. CSVをList<T>に変換する方法

C#でCSVを扱う場合、DataTableよりもList<T>に変換したほうが、型安全で扱いやすいことが多いです。

たとえば、ユーザー情報を表すクラスを用意します。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
}

単純なCSVをList<User>に変換する例です。

C#
using System.Text;

static List<User> ReadUsers(string path, Encoding encoding)
{
List<User> users = new();

using var reader = new StreamReader(path, encoding);

reader.ReadLine(); // ヘッダーを読み飛ばす

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (string.IsNullOrWhiteSpace(line))
{
continue;
}

string[] values = line.Split(',');

if (values.Length < 3)
{
continue;
}

if (!int.TryParse(values[0], out int id))
{
continue;
}

if (!int.TryParse(values[2], out int age))
{
continue;
}

users.Add(new User
{
Id = id,
Name = values[1],
Age = age
});
}

return users;
}

使い方です。

C#
List<User> users = ReadUsers("users.csv", Encoding.UTF8);

実務では、単純なCSVならこの方法でもよいですが、複雑なCSVではCsvHelperを使ったList<T>変換のほうが安全です。

C#
using CsvHelper;
using System.Globalization;
using System.Text;

using var reader = new StreamReader("users.csv", Encoding.UTF8);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

List<User> users = csv.GetRecords<User>().ToList();

7-4. List<T>をCSVとして書き出す方法

List<T>をCSVとして書き出す場合は、StreamWriterで1件ずつ出力します。

C#
using System.Text;

static void WriteUsers(string path, List<User> users, Encoding encoding)
{
using var writer = new StreamWriter(path, false, encoding);

writer.WriteLine("Id,Name,Age");

foreach (User user in users)
{
string line = string.Join(",", new[]
{
EscapeCsvValue(user.Id.ToString()),
EscapeCsvValue(user.Name),
EscapeCsvValue(user.Age.ToString())
});

writer.WriteLine(line);
}
}

static string EscapeCsvValue(string? value)
{
if (value == null)
{
return "";
}

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

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

return mustQuote ? $"\"{value}\"" : value;
}

使い方は次のとおりです。

C#
List<User> users = new()
{
new User { Id = 1, Name = "山田太郎", Age = 30 },
new User { Id = 2, Name = "佐藤花子", Age = 25 }
};

WriteUsers("users.csv", users, Encoding.UTF8);

CsvHelperを使う場合はさらに簡潔です。

C#
using CsvHelper;
using System.Globalization;
using System.Text;

using var writer = new StreamWriter("users.csv", false, Encoding.UTF8);
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);

csv.WriteRecords(users);

CSVの安全性と保守性を重視するなら、CsvHelperを使う方法がおすすめです。

7-5. 業務アプリで使いやすいデータ変換設計

業務アプリでCSVを扱う場合、画面処理やビジネスロジックの中にCSV読み書きコードを直接書くと、保守が難しくなります。

おすすめは、CSV処理を専用クラスに分離する設計です。

C#
public class UserCsvService
{
public List<User> Read(string path)
{
// CSV読み込み処理
throw new NotImplementedException();
}

public void Write(string path, List<User> users)
{
// CSV書き込み処理
throw new NotImplementedException();
}
}

実務では、次のような責務を分けると保守しやすくなります。

  • ファイル選択や画面表示はUI層

  • CSV読み書きはCSVサービスクラス

  • データ検証はバリデーションクラス

  • データ登録はリポジトリやアプリケーションサービス

  • エラーログ出力はログクラス

たとえば、CSV読み込み時にエラー行を収集する設計も便利です。

C#
public class CsvImportResult<T>
{
public List<T> Records { get; set; } = new();
public List<string> Errors { get; set; } = new();

public bool HasError => Errors.Count > 0;
}

このように結果クラスを作っておくと、成功データとエラー情報を分けて扱えます。

C#
CsvImportResult<User> result = new();

if (result.HasError)
{
foreach (string error in result.Errors)
{
Console.WriteLine(error);
}
}

CSV処理は一度書いて終わりではなく、仕様変更や列追加が発生しやすい部分です。最初から保守しやすい構成にしておくと、後から修正しやすくなります。

8. 実務で使えるCSV読み書きのサンプルコード

8-1. 文字化けしないCSV読み込みサンプル

UTF-8とShift_JISの両方を想定したCSV読み込みサンプルです。

C#
using System.Text;

public static class CsvFileReader
{
public static IEnumerable<string[]> ReadSimpleCsv(string path, Encoding encoding, bool hasHeader)
{
if (!File.Exists(path))
{
throw new FileNotFoundException("CSVファイルが見つかりません。", path);
}

using var reader = new StreamReader(path, encoding);

if (hasHeader)
{
reader.ReadLine();
}

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (string.IsNullOrWhiteSpace(line))
{
continue;
}

yield return line.Split(',');
}
}
}

UTF-8で読む場合です。

C#
foreach (string[] row in CsvFileReader.ReadSimpleCsv("users.csv", Encoding.UTF8, true))
{
Console.WriteLine(row[0]);
}

Shift_JISで読む場合です。

C#
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

Encoding shiftJis = Encoding.GetEncoding("shift_jis");

foreach (string[] row in CsvFileReader.ReadSimpleCsv("users.csv", shiftJis, true))
{
Console.WriteLine(row[0]);
}

このサンプルは単純なCSV向けです。カンマや改行を含むCSVにはCsvHelperを使ってください。

8-2. Excelで開けるCSV書き込みサンプル

Excelで開いたときの文字化けを防ぎやすくするため、BOM付きUTF-8でCSVを書き出す例です。

C#
using System.Text;

public static class ExcelCsvWriter
{
public static void WriteUsers(string path, List<User> users)
{
var utf8WithBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);

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

writer.WriteLine("ID,氏名,年齢");

foreach (User user in users)
{
string line = string.Join(",", new[]
{
EscapeCsvValue(user.Id.ToString()),
EscapeCsvValue(user.Name),
EscapeCsvValue(user.Age.ToString())
});

writer.WriteLine(line);
}
}

private static string EscapeCsvValue(string? value)
{
if (value == null)
{
return "";
}

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

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

return mustQuote ? $"\"{value}\"" : value;
}
}

使い方です。

C#
List<User> users = new()
{
new User { Id = 1, Name = "山田太郎", Age = 30 },
new User { Id = 2, Name = "佐藤花子", Age = 25 }
};

ExcelCsvWriter.WriteUsers("users.csv", users);

Excelで直接開くことが前提なら、BOM付きUTF-8またはShift_JISを検討しましょう。

8-3. カンマ・改行・ダブルクォート対応のCSV出力サンプル

カンマ、改行、ダブルクォートを含む値に対応したCSV出力サンプルです。

C#
using System.Text;

public static class SafeCsvWriter
{
public static void Write(string path, IEnumerable<string[]> rows, Encoding encoding)
{
using var writer = new StreamWriter(path, false, encoding);

foreach (string[] row in rows)
{
string line = string.Join(",", row.Select(EscapeCsvValue));
writer.WriteLine(line);
}
}

private static string EscapeCsvValue(string? value)
{
if (value == null)
{
return "";
}

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

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

return mustQuote ? $"\"{value}\"" : value;
}
}

使い方です。

C#
List<string[]> rows = new()
{
new[] { "Id", "Name", "Memo" },
new[] { "1", "山田太郎", "東京都渋谷区, 1-2-3" },
new[] { "2", "佐藤花子", "彼は\"OK\"と言った" },
new[] { "3", "田中一郎", "1行目\n2行目" }
};

SafeCsvWriter.Write("notes.csv", rows, Encoding.UTF8);

出力結果は、CSV仕様に沿った形になります。

csv
Id,Name,Memo
1,山田太郎,"東京都渋谷区, 1-2-3"
2,佐藤花子,"彼は""OK""と言った"
3,田中一郎,"1行目
2行目"

読み込み側でも同じ仕様に対応する必要があるため、複雑なCSVの読み込みにはCsvHelperを使いましょう。

8-4. CsvHelperを使った実装サンプル

CsvHelperを使った実務向けの読み書きサンプルです。

C#
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
using System.Text;

public class Product
{
public string Code { get; set; } = "";
public string Name { get; set; } = "";
public decimal Price { get; set; }
public string Note { get; set; } = "";
}

public sealed class ProductMap : ClassMap<Product>
{
public ProductMap()
{
Map(m => m.Code).Name("商品コード");
Map(m => m.Name).Name("商品名");
Map(m => m.Price).Name("価格");
Map(m => m.Note).Name("備考");
}
}

public static class ProductCsvService
{
public static List<Product> Read(string path, Encoding encoding)
{
using var reader = new StreamReader(path, encoding);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

csv.Context.RegisterClassMap<ProductMap>();

return csv.GetRecords<Product>().ToList();
}

public static void Write(string path, List<Product> products, Encoding encoding)
{
using var writer = new StreamWriter(path, false, encoding);
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);

csv.Context.RegisterClassMap<ProductMap>();
csv.WriteRecords(products);
}
}

使い方です。

C#
List<Product> products = new()
{
new Product
{
Code = "001",
Name = "ノートPC",
Price = 120000,
Note = "在庫あり"
},
new Product
{
Code = "002",
Name = "モニター",
Price = 30000,
Note = "27インチ, HDMI対応"
}
};

ProductCsvService.Write("products.csv", products, Encoding.UTF8);

List<Product> loaded = ProductCsvService.Read("products.csv", Encoding.UTF8);

CsvHelperを使うと、カンマやダブルクォートを含む値も自動的に処理されるため、実務で安心して使えます。

8-5. エラー処理を含めた実務向けサンプル

CSV読み込みでは、ファイルが存在しない、文字コードが違う、数値変換に失敗する、列が足りないなど、さまざまなエラーが発生します。

エラー処理を含めたサンプルです。

C#
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
using System.Text;

public class CsvImportResult<T>
{
public List<T> Records { get; } = new();
public List<string> Errors { get; } = new();

public bool Success => Errors.Count == 0;
}

public static class SafeProductCsvImporter
{
public static CsvImportResult<Product> Import(string path, Encoding encoding)
{
CsvImportResult<Product> result = new();

if (!File.Exists(path))
{
result.Errors.Add("CSVファイルが存在しません。");
return result;
}

try
{
using var reader = new StreamReader(path, encoding);

var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
BadDataFound = context =>
{
result.Errors.Add($"不正なCSVデータがあります: {context.RawRecord}");
},
MissingFieldFound = null,
HeaderValidated = null
};

using var csv = new CsvReader(reader, config);
csv.Context.RegisterClassMap<ProductMap>();

foreach (Product product in csv.GetRecords<Product>())
{
if (string.IsNullOrWhiteSpace(product.Code))
{
result.Errors.Add("商品コードが空です。");
continue;
}

result.Records.Add(product);
}
}
catch (UnauthorizedAccessException)
{
result.Errors.Add("CSVファイルへのアクセス権限がありません。");
}
catch (IOException ex)
{
result.Errors.Add($"CSVファイルの読み込みに失敗しました: {ex.Message}");
}
catch (Exception ex)
{
result.Errors.Add($"予期しないエラーが発生しました: {ex.Message}");
}

return result;
}
}

使い方です。

C#
CsvImportResult<Product> result =
SafeProductCsvImporter.Import("products.csv", Encoding.UTF8);

if (!result.Success)
{
foreach (string error in result.Errors)
{
Console.WriteLine(error);
}
}
else
{
foreach (Product product in result.Records)
{
Console.WriteLine(product.Name);
}
}

実務では、CSV取り込み処理でエラーが発生した場合に、どの行で何が問題だったのかをユーザーに伝えることが重要です。

9. C#のCSV処理でよくあるエラーと解決策

9-1. 日本語が文字化けする

日本語が文字化けする原因の多くは、文字コードの不一致です。

Shift_JISのCSVをUTF-8として読み込むと、文字化けすることがあります。

C#
// 文字化けの原因になる可能性
using var reader = new StreamReader("users.csv", Encoding.UTF8);

Shift_JISのCSVなら、Shift_JISを明示して読み込みます。

C#
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

Encoding shiftJis = Encoding.GetEncoding("shift_jis");

using var reader = new StreamReader("users.csv", shiftJis);

Excelで開いたときに文字化けする場合は、BOM付きUTF-8で出力する方法もあります。

C#
var utf8WithBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);

using var writer = new StreamWriter("users.csv", false, utf8WithBom);

CSVを扱うときは、入力元と出力先で文字コードを合わせることが大切です。

9-2. カンマ入りのデータで列がずれる

値の中にカンマが含まれる場合、Split(',')で分割すると列がずれます。

csv
1,山田太郎,"東京都渋谷区, 1-2-3"

このCSVを単純に分割すると、住所の中のカンマまで区切りとして扱われます。

解決策は、値をダブルクォートで囲み、CSV仕様に沿って読み込むことです。

csv
1,山田太郎,"東京都渋谷区, 1-2-3"

出力時にはエスケープ処理を行います。

C#
static string EscapeCsvValue(string? value)
{
if (value == null)
{
return "";
}

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

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

return mustQuote ? $"\"{value}\"" : value;
}

読み込み時はCsvHelperを使うのが安全です。

C#
using var reader = new StreamReader("users.csv", Encoding.UTF8);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

List<User> users = csv.GetRecords<User>().ToList();

9-3. 改行入りのデータで行が分割される

CSVのフィールドには改行が含まれることがあります。

csv
Id,Name,Memo
1,山田太郎,"1行目
2行目"

このようなCSVをReadLine()で1行ずつ処理すると、1レコードが途中で分割されます。

解決策は、改行を含むフィールドに対応したCSVパーサーを使うことです。

C#
using CsvHelper;
using System.Globalization;
using System.Text;

using var reader = new StreamReader("notes.csv", Encoding.UTF8);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

List<Note> notes = csv.GetRecords<Note>().ToList();

改行を含む可能性があるCSVでは、自前のReadLine()Split()に頼らないほうが安全です。

9-4. ファイルが別プロセスで使用中になる

CSVファイルをExcelで開いたままC#から書き込もうとすると、ファイルロックによりIOExceptionが発生することがあります。

C#
try
{
using var writer = new StreamWriter("users.csv", false, Encoding.UTF8);
writer.WriteLine("Id,Name,Age");
}
catch (IOException ex)
{
Console.WriteLine($"ファイルが使用中の可能性があります: {ex.Message}");
}

対策は次のとおりです。

  • ExcelでCSVを閉じてから書き込む

  • usingでファイルを確実に閉じる

  • 同時書き込みを避ける

  • 一時ファイルへ出力してから置き換える

  • ファイルロック時はユーザーに再試行を促す

ファイル入出力では、例外が発生する前提で実装しておくことが重要です。

9-5. 大容量CSVの読み込みでメモリ不足になる

大容量CSVをFile.ReadAllLines()で読み込むと、ファイル全体がメモリに展開されます。

C#
// 大容量CSVでは注意
string[] lines = File.ReadAllLines("large.csv", Encoding.UTF8);

大容量CSVでは、StreamReaderで1行ずつ処理しましょう。

C#
using var reader = new StreamReader("large.csv", Encoding.UTF8);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (line == null)
{
continue;
}

// 1行ずつ処理する
}

CsvHelperでも、ToList()で全件を一度に読み込むのではなく、foreachで逐次処理するとメモリ使用量を抑えられます。

C#
using var reader = new StreamReader("large.csv", Encoding.UTF8);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

foreach (User user in csv.GetRecords<User>())
{
// 1件ずつ処理する
}

大量データでは、「一度に全部読み込まない」ことが重要です。

9-6. Excelで開くと先頭の0が消える

CSVをExcelで開くと、商品コードや郵便番号の先頭の0が消えることがあります。

csv
Code,Name
00123,商品A

Excelが00123を数値として自動判定し、123として表示してしまうためです。

CSV側でダブルクォートを付けても、Excelでは数値として解釈される場合があります。

csv
"00123","商品A"

対策としては、次の方法があります。

  • Excelのインポート機能で列の型を「文字列」に指定する

  • CSVではなくExcelファイル形式で出力する

  • 先頭にタブを付けて文字列として扱わせる

  • Excel向けに="00123"のような形式で出力する

ただし、="00123"は純粋なCSVデータではなく、Excel向けの表現です。また、CSVインジェクション対策が必要な場面では注意してください。

業務システムで商品コードや郵便番号を扱う場合は、CSVを開く側の運用も含めて設計する必要があります。

10. C#でCSVを扱うときのベストプラクティス

10-1. 単純なCSVならStreamReader・StreamWriterで十分なケース

次のような単純なCSVであれば、StreamReaderStreamWriterで十分対応できます。

  • 値にカンマが含まれない

  • 値に改行が含まれない

  • 値にダブルクォートが含まれない

  • ヘッダーや列順が固定されている

  • ファイルサイズが比較的小さい

  • 社内ツールなどで仕様を完全に管理できる

たとえば、次のようなCSVです。

csv
Id,Name,Age
1,山田太郎,30
2,佐藤花子,25

このような形式なら、次のようにシンプルに読み込めます。

C#
using var reader = new StreamReader("users.csv", Encoding.UTF8);

reader.ReadLine();

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (line == null)
{
continue;
}

string[] values = line.Split(',');
}

ただし、最初は単純なCSVでも、後から住所、備考、説明文などが追加されると、カンマや改行を含む可能性があります。将来的な拡張も考えて選択しましょう。

10-2. 複雑なCSVはライブラリを使うべき理由

複雑なCSVでは、CsvHelperのようなライブラリを使うべきです。

理由は、CSVには見た目以上に多くのルールがあるためです。

  • カンマを含む値はダブルクォートで囲む

  • ダブルクォートは2つにしてエスケープする

  • 改行を含むフィールドに対応する

  • ヘッダーの有無を処理する

  • 列名とプロパティをマッピングする

  • 型変換を行う

  • 不正データを検出する

これらをすべて自前で実装すると、バグが入りやすくなります。

CsvHelperを使えば、次のように短いコードで安全に読み込めます。

C#
using var reader = new StreamReader("users.csv", Encoding.UTF8);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

List<User> users = csv.GetRecords<User>().ToList();

CSVが外部システムやユーザー入力に由来する場合は、想定外のデータが入る可能性があります。複雑なCSVや実務で重要なCSV処理には、ライブラリを使うのが安全です。

10-3. 文字コードを明示して読み書きする

C#でCSVを扱うときは、文字コードを必ず明示しましょう。

読み込み時です。

C#
using var reader = new StreamReader("users.csv", Encoding.UTF8);

書き込み時です。

C#
using var writer = new StreamWriter("users.csv", false, Encoding.UTF8);

Shift_JISを使う場合です。

C#
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

Encoding shiftJis = Encoding.GetEncoding("shift_jis");

using var reader = new StreamReader("users.csv", shiftJis);

文字コードを明示しないと、環境や実行時の設定によって期待しない文字コードで処理され、文字化けの原因になります。

特に日本語CSVでは、UTF-8、BOM付きUTF-8、Shift_JISのどれを使うのかを仕様として決めておくことが大切です。

10-4. Splitだけに頼らずCSV仕様を考慮する

Split(',')は便利ですが、CSVの完全なパースには向いていません。

次のようなCSVでは正しく処理できません。

csv
1,山田太郎,"東京都渋谷区, 1-2-3"
2,佐藤花子,"彼は""OK""と言った"
3,田中一郎,"1行目
2行目"

Splitで処理してよいのは、値の中にカンマ、ダブルクォート、改行が絶対に含まれない場合です。

CSV仕様を考慮するなら、次のどちらかを選びましょう。

  • 出力時に正しいエスケープ処理を実装する

  • 読み書きにCsvHelperなどのライブラリを使う

実務では、CSVの仕様変更に備えて、早い段階でライブラリを使う判断をすることも重要です。

10-5. 大容量ファイルは1行ずつ処理する

大容量CSVを扱う場合は、ファイル全体を一度に読み込まないようにしましょう。

避けたい例です。

C#
string[] lines = File.ReadAllLines("large.csv", Encoding.UTF8);

推奨例です。

C#
using var reader = new StreamReader("large.csv", Encoding.UTF8);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();

if (line == null)
{
continue;
}

// 1行ずつ処理
}

CsvHelperでも、ToList()ではなくforeachで1件ずつ処理できます。

C#
using var reader = new StreamReader("large.csv", Encoding.UTF8);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

foreach (User user in csv.GetRecords<User>())
{
// 1件ずつDB登録などを行う
}

大容量CSVでは、次の点も意識しましょう。

  • 進捗ログを出す

  • エラー行を記録する

  • 一定件数ごとにDBへ保存する

  • 全件をListに保持しない

  • 処理時間とメモリ使用量を測定する

CSV処理は小さなファイルでは問題が見えにくいため、本番に近いサイズのファイルでテストすることが大切です。

10-6. 保守しやすいCSV処理クラスを作る

CSV処理は、専用クラスにまとめると保守しやすくなります。

たとえば、ユーザーCSV用のサービスクラスを作ります。

C#
using CsvHelper;
using System.Globalization;
using System.Text;

public class UserCsvService
{
private readonly Encoding _encoding;

public UserCsvService(Encoding encoding)
{
_encoding = encoding;
}

public List<User> Read(string path)
{
using var reader = new StreamReader(path, _encoding);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

return csv.GetRecords<User>().ToList();
}

public void Write(string path, List<User> users)
{
using var writer = new StreamWriter(path, false, _encoding);
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);

csv.WriteRecords(users);
}
}

使い方です。

C#
UserCsvService service = new UserCsvService(Encoding.UTF8);

List<User> users = service.Read("users.csv");

service.Write("output.csv", users);

このようにCSV処理をクラス化しておくと、文字コード変更、列追加、エラー処理追加、CsvHelper設定変更などに対応しやすくなります。

さらに実務では、次のように設計するとよいです。

  • CSVごとに専用のDTOクラスを作る

  • CsvHelperのClassMapを使う

  • バリデーション処理を分離する

  • エラー行を記録する

  • 読み込み結果をResultクラスで返す

  • テストコードでCSV入出力を確認する

CSV処理は地味ですが、業務データの品質に直結します。保守しやすく、安全な設計を心がけましょう。

まとめ

C#でCSVを読み書きする方法は、単純なCSVであればStreamReaderStreamWriterを使うだけで実装できます。

基本的な読み込みは次のように書けます。

C#
using var reader = new StreamReader("users.csv", Encoding.UTF8);

while (!reader.EndOfStream)
{
string? line = reader.ReadLine();
}

基本的な書き込みは次のように書けます。

C#
using var writer = new StreamWriter("users.csv", false, Encoding.UTF8);

writer.WriteLine("Id,Name,Age");

ただし、実務のCSV処理では次の点に注意が必要です。

  • 文字化けを防ぐために文字コードを明示する

  • Excelで開くCSVはBOM付きUTF-8やShift_JISを検討する

  • Shift_JISを扱う場合はEncoding.RegisterProviderが必要になることがある

  • 値にカンマが含まれる場合はダブルクォートで囲む

  • ダブルクォートは2つにしてエスケープする

  • 改行を含むフィールドはSplitReadLineだけでは扱いにくい

  • 大容量CSVは一度に読み込まず、1行ずつ処理する

  • 複雑なCSVはCsvHelperを使う

  • CSV処理は専用クラスに分離すると保守しやすい

特に、カンマ、ダブルクォート、改行を含むCSVを扱う場合は、Split(',')だけでは不十分です。C#でCSVを安全に処理するなら、CSV仕様を理解したうえで、必要に応じてCsvHelperなどのライブラリを活用しましょう。