C#でCSVをExcelで文字化けせずに出力する|BOM付きUTF-8とCRLF

C#・.NET

C#でCSVを出力すると、Excelで開いたときに文字化けする

業務システムでは、一覧をCSVで出力して、Excelで開いて確認・加工する、という使い方がよくあります。ところが、C#でふつうに書き出したCSVをダブルクリックでExcelで開くと、次のようになることがあります。

日本語の部分が「譌・イサ莠、騾夊イサ」のような読めない文字になってしまった

原因は文字コードです。C#(.NET)は文字列をUTF-8で書き出すのが基本ですが、日本語版のExcelは、CSVの先頭にBOM(バイト・オーダー・マーク)が無いと、Shift_JIS(CP932)のファイルだと思って開きます。

本記事では、C#でCSVを「Excelで開いても文字化けしない形」で出力する方法を紹介します。自作の立替精算システムで、仕訳データや集計のCSV出力に実際に使っている部品です。

立替精算システムで使ったところ

私が自作した立替精算システムでは、経理が会計ソフトに取り込む仕訳データ、部門別・プロジェクト別の集計、伝票照会の一覧、操作ログなどをCSVで出力しています。

立替精算システムの仕訳データ出力の画面。対象年月ごとに費用仕訳・支払仕訳のCSVを出力するボタンが並んでいる

仕訳データ出力の画面です。「費用仕訳 6件」のボタンを押すと、CSVをダウンロードします。

経理の方は、出力したCSVを一度Excelで開いて中身を確かめてから会計ソフトに取り込むことが多いため、「Excelで開いて文字化けしない」ことが欠かせません。

Excelで文字化けしないCSVの条件

項目どうするか理由
文字コードBOM付きUTF-8先頭にBOMがあると、ExcelがUTF-8と判断して開く
改行CRLF(\r\n)Windows・Excelの標準の改行
カンマ・ダブルクォート・改行を含む値“” で囲み、中の ” は “” にする列がずれないように
=・+・-・@ で始まる値先頭に ‘ を付けるExcelが数式として実行しないように(CSVインジェクション対策)

Shift_JISで書き出す方法もありますが、Shift_JIS(CP932)に無い文字(「𠮷」などの一部の漢字や絵文字)は「?」に置き換わってしまいます。社員の名前や支払先の名前が化けると困るので、立替精算システムでは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で日本語がそのまま表示されます。

下の2行は比較です。BOMの無いUTF-8をShift_JISとして読むと、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 では、カンマ・ダブルクォート・改行を含む値を " で囲み、値の中の " を "" にしています。また、=・+・-・@ などで始まる値の先頭に ' を付けて、Excelが数式として実行しないようにしています。支払先や摘要のように、社員が自由に入力できる欄がそのままCSVに出るためです。
  • Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) は、.NETでShift_JIS(CP932)を使うときに1回だけ必要な準備です。サンプルでは比較のためだけに使っています。

たくさんの行を出力するとき(Webアプリの場合)

立替精算システムはWebアプリなので、CSVはブラウザへのダウンロードとして返しています。件数に上限を設けていないため、全部をメモリにためてから返すのではなく、データベースから読んだ行から順に書き出しています。

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を渡しています。StreamWriter にBOM付きのエンコーディングを渡すと、書き込み先のストリームによってはBOMが二重に付くことがあるためです。何十万行あっても、サーバのメモリは行数に比例して増えません。

注意点

  • 取り込む側(会計ソフトなど)がShift_JISしか読めない場合は、相手の仕様に合わせてください。その場合は、Shift_JISに無い文字を事前に置き換える処理が必要になります。
  • 先頭に ' を付けた値は、CSVのデータとしても ' が付いた状態になります。取込先で困る列では付けないなど、取込先に合わせて調整してください。
  • Excelの「データ」→「テキストまたはCSVから」で開けば、BOMが無くても文字コードを選んで読み込めます。ただ、使う人に毎回その手順を案内するより、BOM付きで出力しておく方が使う人は楽です。

最後に

以上が、C#でCSVをExcelで文字化けせずに出力する方法です。

ポイントは「BOM付きUTF-8」「CRLF」「””で囲む」「数式に見える値の対策」の4つです。一度共通部品にしておけば、CSVを出力するたびに文字コードを気にする必要がなくなります。

この部品を使っている立替精算システムは、こちらで紹介しています。

PythonでCSVを扱う方法は、こちらで紹介しています。

コメント

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