C#でCSVを出力すると、Excelで開いたときに文字化けする
業務システムでは、一覧をCSVで出力して、Excelで開いて確認・加工する、という使い方がよくあります。ところが、C#でふつうに書き出したCSVをダブルクリックでExcelで開くと、次のようになることがあります。
日本語の部分が「譌・イサ莠、騾夊イサ」のような読めない文字になってしまった
原因は文字コードです。C#(.NET)は文字列をUTF-8で書き出すのが基本ですが、日本語のWindowsのExcelは、CSVの先頭にBOM(バイト・オーダー・マーク)が無いと、Shift_JIS(CP932)のファイルとして読み込みます。このPCのExcel(Microsoft 365)で確かめた結果で、Excelのバージョンや設定によっては動きが違うこともあります。
本記事では、C#でCSVを「Excelで開いても文字化けしない形」で出力する方法を紹介します。自作の立替精算システムで、仕訳データや集計のCSV出力に実際に使っている部品です。
立替精算システムで使ったところ
私が自作した立替精算システムでは、経理が会計ソフトに取り込む仕訳データ、部門別・プロジェクト別の集計、伝票照会の一覧、操作ログなどをCSVで出力しています。

仕訳データ出力の画面です。「費用仕訳 6件」のボタンを押し、確認の画面で「仕訳出力」を押すと、CSVがダウンロードされます。
経理の方が、出力したCSVを一度Excelで開いて中身を確かめてから会計ソフトに取り込むこともあるため、「Excelで開いて文字化けしない」ことが欠かせません。
Excelで文字化けしないCSVの条件
| 項目 | どうするか | 理由 |
|---|---|---|
| 文字コード | BOM付きUTF-8 | 先頭にBOMがあると、ExcelがUTF-8として読み込む |
| 改行 | CRLF(\r\n) | CSVの決まり(RFC 4180)の改行。Excel自体はLFでも読めるが、取込先がCRLFを前提にしていることがある |
| カンマ・ダブルクォート・改行を含む値 | “” で囲み、中の ” は “” にする | 列がずれないように |
| =・+・-・@ などで始まる値 | 先頭に ‘ を付ける | Excelが数式として実行しないように(CSVインジェクション対策) |
Shift_JISで書き出す方法もありますが、Shift_JIS(CP932)に無い文字(「𠮷」などの一部の漢字や絵文字)は「?」に置き換わってしまいます(「𠮷」や絵文字は「??」になります)。社員の名前や支払先の名前が化けると困るので、立替精算システムではCSVをBOM付きUTF-8に統一しています。
C#でExcelで文字化けしないCSVを出力するサンプルコード
動作確認は Windows 11・.NET 10 です(.NET 8 以降で動きます)。Visual Studio でコンソールアプリを作り、Program.cs に貼り付けて実行してください。.NET 10 なら、ファイルに保存して dotnet run ファイル名.cs でも実行できます。
using System.Text;
// 出力するデータ(仕訳データのイメージ)
var gyouList = new List<string[]>
{
new[] { "伝票番号", "日付", "勘定科目", "金額", "摘要" },
new[] { "10001", "2026/09/05", "旅費交通費", "1340", "大阪出張 新大阪→本町" },
new[] { "10002", "2026/09/12", "会議費", "4400", "打合せ(喫茶 \"ルーム\", 2名)" },
new[] { "10003", "2026/09/18", "会議費", "3300", "𠮷野家で昼食会議" },
new[] { "10004", "2026/09/20", "消耗品費", "880", "=1+2" },
};
// CSV の 1 行ずつの文字列にする
var lines = gyouList.Select(CsvGyou).ToList();
// BOM 付き UTF-8・改行 CRLF のバイト列にして保存する
var bytes = ToBytes(lines);
File.WriteAllBytes("shiwake.csv", bytes);
Console.WriteLine($"shiwake.csv を出力しました({bytes.Length} バイト)");
Console.WriteLine($"先頭 3 バイト(BOM):{Convert.ToHexString(bytes, 0, 3)}");
Console.WriteLine();
Console.WriteLine(File.ReadAllText("shiwake.csv"));
// ↓ 比較:BOM なし UTF-8 を、Excel と同じように Shift_JIS(CP932)として読んだ場合
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var bomNashi = new UTF8Encoding(false).GetBytes(lines[1]);
Console.WriteLine("BOM なしを CP932 として読むと:" + Encoding.GetEncoding(932).GetString(bomNashi));
// ↓ 比較:Shift_JIS(CP932)で書くと、CP932 に無い文字は「?」になる
var sjis = Encoding.GetEncoding(932).GetBytes(lines[3]);
Console.WriteLine("Shift_JIS で書くと:" + Encoding.GetEncoding(932).GetString(sjis));
/// <summary>値を 1 つ、CSV 用に整える。</summary>
static string Escape(string? value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
// Excel が数式として動かしてしまう先頭文字は、先頭に ' を付けて文字列にする
char[] suushikiKaishi = ['=', '+', '-', '@', '\t', '\r'];
var moji = suushikiKaishi.Contains(value[0]) ? "'" + value : value;
// カンマ・ダブルクォート・改行を含むときは "" で囲む(中の " は "" にする)
return moji.AsSpan().IndexOfAny(",\"\r\n") >= 0
? $"\"{moji.Replace("\"", "\"\"")}\""
: moji;
}
/// <summary>1 行分を組み立てる。</summary>
static string CsvGyou(string?[] values) =>
string.Join(",", values.Select(Escape));
/// <summary>BOM 付き UTF-8 のバイト列にする。改行は Excel に合わせて CRLF。</summary>
static byte[] ToBytes(IEnumerable<string> gyouList)
{
var sb = new StringBuilder();
foreach (var gyou in gyouList)
{
sb.Append(gyou).Append("\r\n");
}
var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
return [.. encoding.GetPreamble(), .. encoding.GetBytes(sb.ToString())];
}実行結果
shiwake.csv を出力しました(299 バイト)
先頭 3 バイト(BOM):EFBBBF
伝票番号,日付,勘定科目,金額,摘要
10001,2026/09/05,旅費交通費,1340,大阪出張 新大阪→本町
10002,2026/09/12,会議費,4400,"打合せ(喫茶 ""ルーム"", 2名)"
10003,2026/09/18,会議費,3300,𠮷野家で昼食会議
10004,2026/09/20,消耗品費,880,'=1+2
BOM なしを CP932 として読むと:10001,2026/09/05,譌・イサ莠、騾夊イサ,1340,螟ァ髦ェ蜃コ蠑オ 譁ー螟ァ髦ェ竊呈悽逕コ
Shift_JIS で書くと:10003,2026/09/18,会議費,3300,??野家で昼食会議先頭の3バイト EF BB BF がBOMです。出力した shiwake.csv をダブルクリックすると、Excelで日本語がそのまま表示されます。shiwake.csv は、実行したときの作業フォルダ(カレントディレクトリ)にできます。場所が分からないときは、Console.WriteLine(Path.GetFullPath("shiwake.csv")); を足すと表示できます。
日本語のWindowsのコンソールは文字コードがCP932のことが多く、その場合、画面では3行目の「𠮷」が「??」と表示されます。画面に表示するときだけの話で、shiwake.csv の中身は「𠮷」のままです。
下の2行は比較です。BOMの無いUTF-8をShift_JISとして読むと、Excelで見えるのと同じ文字化けになります(BOMを外したファイルをこのPCのExcelで開くと、同じ文字が表示されました)。文字化けしたときは、ダブルクォートが前の文字と一緒に化けて消え、行や列がずれることもあります。また、Shift_JISで書き出すと「𠮷」が「??」になってしまいます。
コードの説明
new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)で「BOMを付けるUTF-8」を用意し、GetPreamble()でBOMの3バイトを取り出して、データの先頭に付けています。File.WriteAllText(path, text)はBOMなし、File.WriteAllText(path, text, Encoding.UTF8)はBOMあり、と書き方によって変わります。どちらになるかが読み取りにくいので、立替精算システムではBOMを自分で付ける形にしています。- 改行は
"\r\n"を自分で付けています。AppendLineやEnvironment.NewLineは、動かすOSによって改行が変わるためです。 Escapeでは、カンマ・ダブルクォート・改行を含む値を"で囲み、値の中の"を""にしています。また、=・+・-・@・タブ・CR(\r)で始まる値の先頭に'を付けて、Excelが数式として実行しないようにしています。支払先や摘要のように、社員が自由に入力できる欄がそのままCSVに出るためです。Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)は、.NETでShift_JIS(CP932)を使うときに1回だけ必要な準備です。サンプルでは比較のためだけに使っています。
たくさんの行を出力するとき(Webアプリの場合)
立替精算システムはWebアプリなので、CSVはブラウザへのダウンロードとして返しています。件数に上限を設けていないため、行数の多くなる伝票照会と操作ログのCSVは、全部をメモリにためてから返すのではなく、データベースから読んだ行から順に書き出しています(仕訳データや集計のCSVは、サンプルと同じ ToBytes でバイト列にしてから返しています)。
response.ContentType = "text/csv";
// BOM は自分で 1 回だけ書く
await response.Body.WriteAsync(new UTF8Encoding(true).GetPreamble(), cancellationToken);
// StreamWriter には BOM なしの UTF-8 を渡す
await using var writer = new StreamWriter(response.Body, new UTF8Encoding(false), bufferSize: 64 * 1024, leaveOpen: true);
writer.NewLine = "\r\n";
await foreach (var gyou in gyouList.WithCancellation(cancellationToken))
{
await writer.WriteLineAsync(gyou.AsMemory(), cancellationToken);
}
await writer.FlushAsync(cancellationToken);BOMは最初に1回だけ自分で書き、StreamWriter にはBOMなしのUTF-8を渡しています。自分でBOMを書いたうえで StreamWriter にもBOM付きのエンコーディングを渡すと、ブラウザへの応答のように位置を戻せない(シークできない)ストリームでは StreamWriter も先頭にBOMを書くため、BOMが二重になります。1行ずつ書き出すので、CSVの全部の行をサーバのメモリにためることはありません。
注意点
- 取り込む側(会計ソフトなど)がShift_JISしか読めない場合は、相手の仕様に合わせてください。その場合は、Shift_JISに無い文字を事前に置き換える処理が必要になります。
- 先頭に
'を付けた値は、CSVのデータとしても'が付いた状態になり、Excelで開いても'=1+2のように'が見えたまま表示されます。-1340のようなマイナスの数値も'-1340になり、数値として扱われなくなります(立替精算システムは金額を1円以上に限っているので、この問題は起きません)。取込先で困る列では付けないなど、取込先に合わせて調整してください。 - 全角の
=・+・-・@で始まる値も、このPCのExcel(Microsoft 365)では数式として実行されました。サンプルのEscapeは半角だけを見ているので、全角も防ぎたいときは、配列に全角の文字も足してください。 - Excelで開いたCSVを上書き保存すると、日付が
2026/09/05から2026/9/5になるなど、値の形が変わることがあります。中身を確かめるだけなら、保存せずに閉じてください。 - Excelの「データ」→「テキストまたはCSVから」で開けば、BOMが無くても文字コードを選んで読み込めます。ただ、使う人に毎回その手順を案内するより、BOM付きで出力しておく方が使う人は楽です。
最後に
以上が、C#でCSVをExcelで文字化けせずに出力する方法です。
ポイントは「BOM付きUTF-8」「CRLF」「””で囲む」「数式に見える値の対策」の4つです。一度共通部品にしておけば、CSVを出力するたびに文字コードを気にする必要がなくなります。
この部品を使っている立替精算システムは、こちらで紹介しています。
PythonでCSVを扱う方法は、こちらで紹介しています。




コメント