CSVは「ただのテキスト」ではない ── C#業務アプリのCSV実務(文字コード・Excel互換・インジェクション対策)

· · CSharp, .NET, CSV, Excel, 文字コード, ファイル連携, Windows開発, 技術相談

CSVによるデータ連携は、業務アプリの世界では今も現役の主役です。そして「テキストをカンマでつなぐだけ」に見えるせいで、自作パーサーがフィールド内のカンマで壊れる、Excelで開いたら文字化けする、電話番号の先頭ゼロが消える、といった事故が繰り返し起きる領域でもあります。

この記事では、C#でCSVを読み書きする業務アプリを想定して、CSVの仕様(RFC 4180)と現実の方言、文字コードの決め方、Excelとの相性問題、CSVインジェクション対策、運用の落とし穴までを実務で判断に迷う順に整理します。

1. まず結論(判断表)

CSVの設計判断は「誰が(何が)そのファイルを開くのか」から逆算します。

用途 文字コードの第一候補 設計上の要点
人がExcelでダブルクリックして開く UTF-8(BOM付き) BOMがないとExcelが文字コードを誤認して文字化けする環境がある。先頭ゼロ・日付化の問題は文字コードでは防げない(第5章)
新規のシステム間連携 UTF-8(BOMなし) 文字コード・引用符・改行コード・ヘッダー有無を仕様書に明記する。BOMの有無も明記する
レガシーシステムとの連携 先方仕様に従う(Shift_JISが多い) .NET(Core)ではShift_JISにCodePagesEncodingProviderの登録が必要(第4章)。機種依存文字の扱いを先に取り決める
大量データのバッチ連携 仕様に従う 全件をメモリに載せず1行ずつストリーミングで処理する(第7章)。排他制御と書きかけファイルの扱いも設計する
人がExcelで見る・印刷するのが主目的 ── そもそもCSVではなくxlsx出力を検討する(FAQ参照)

結論を先に書きます。

  • String.Split(',') によるCSVパースは、動いているように見えても仕様上壊れます。 フィールド内のカンマ・改行・引用符を処理できないためです。.NET標準の範囲なら TextFieldParser を使ってください(第3章)。
  • 文字コードの事故は「Excelがどう解釈するか」で起きます。 人がExcelで開くCSVはBOM付きUTF-8にするのが現在の無難な選択です。また、.NETでBOMを付けるかどうかは Encoding の指定方法で変わるという罠があります(第4章)。
  • .NET(Core)ではShift_JISは追加パッケージなしには使えません。 System.Text.Encoding.CodePages パッケージの CodePagesEncodingProvider を登録して初めて Encoding.GetEncoding("shift_jis") が成功します1。.NET Frameworkから移行したCSV処理が本番で初めて例外を出す典型ポイントです。
  • 先頭ゼロ落ち・日付化はCSV側では防げません。 ダブルクォートで囲んでも防げない、というのが重要な事実です(第5章)。
  • ユーザー入力をCSVに出力するアプリは、CSVインジェクション対策が必要です。 =+ で始まるフィールドはExcelで数式として解釈されます(第6章)。

2. CSVの「仕様」 ── RFC 4180と現実の方言

CSVには事実上の標準としてRFC 4180があります2。実務で押さえるべきルールは次の4つだけです。

  1. レコードの区切りは改行(CRLF)、フィールドの区切りはカンマ。
  2. フィールドにカンマ・改行・ダブルクォートを含む場合、そのフィールド全体をダブルクォートで囲む。
  3. フィールド内のダブルクォートは "" と2つ重ねてエスケープする。
  4. 囲む必要のないフィールドは囲んでも囲まなくてもよい。

つまり、次の1行は3フィールドのレコードです。

1001,"株式会社サンプル 営業部","住所は""東京都○○区1-2-3""です,と入力された備考"

そしてRFC 4180は「事実上の標準」であって、現実のCSVには方言が山ほどあります。改行がLFのみ、引用符処理をしない、区切りがタブやセミコロン、ヘッダー行があったりなかったり──だからこそ、連携仕様書に「RFC 4180準拠」の一言と、文字コード・BOM・改行コード・ヘッダー有無を明記することが、後々のトラブルを防ぐ最も費用対効果の高い作業になります。

3. Split(‘,’)で自作してはいけない ── 読み込みの実務

前章のルールを踏まえると、String.Split(',') が壊れる理由は明らかです。フィールド内のカンマで余計に分割され、引用符内の改行で行が途中で切れます。「今来ているファイルにはカンマ入りデータがないから動いている」だけで、データ次第でいつでも壊れます。

.NET標準の範囲で正しく読むなら、Microsoft.VisualBasic.FileIO.TextFieldParser が使えます。名前空間はVisual Basicですが、C#からも普通に使えます3

using Microsoft.VisualBasic.FileIO; // Microsoft.VisualBasic.Core(Windowsデスクトップ向けSDKに同梱)

using var parser = new TextFieldParser(path, System.Text.Encoding.UTF8)
{
    TextFieldType = FieldType.Delimited,
    HasFieldsEnclosedInQuotes = true, // 引用符で囲まれたフィールドを正しく処理する
    TrimWhiteSpace = false, // 既定はtrue(前後の空白が黙って除去される)。空白が意味を持つデータでは無効化する
};
parser.SetDelimiters(",");

while (!parser.EndOfData)
{
    try
    {
        string[]? fields = parser.ReadFields();
        if (fields is null) break;
        // fields[0], fields[1], ... を処理する
    }
    catch (MalformedLineException ex)
    {
        // 壊れた行: ErrorLine / ErrorLineNumber で内容と行番号を記録できる
        logger.LogWarning("CSVの{Line}行目が不正な形式です: {Content}",
            ex.LineNumber, parser.ErrorLine);
    }
}

TextFieldParser の実務上の利点は、引用符処理(HasFieldsEnclosedInQuotes)に加えて、壊れた行を MalformedLineException として行番号付きで検出できることです3。業務のCSV取り込みでは「1行壊れていたらファイルごと拒否するのか、その行だけスキップして報告するのか」が仕様の分かれ目になるので、この例外処理の設計をそのまま仕様に落とし込めます。

列とオブジェクトのマッピング・型変換・数百万行規模の性能まで必要なら、実績のあるCSVライブラリ(NuGetで広く使われているもの)の導入を検討してください。どのライブラリを選ぶ場合も、確認すべき観点は「RFC 4180の引用符処理」「文字コード指定」「壊れた行の扱い」「ストリーミング読み込みの可否」の4つです。

書き出し側は仕様が単純なので自作も現実的ですが、エスケープ処理を必ず1つの関数に集約してください。

// 書き出し側のエスケープはこの1関数に集約する
static string ToCsvField(string? value)
{
    value ??= string.Empty;
    bool needsQuoting = value.Contains(',') || value.Contains('"')
        || value.Contains('\r') || value.Contains('\n');
    return needsQuoting ? $"\"{value.Replace("\"", "\"\"")}\"" : value;
}

「金額のカラムだけ手書きで string.Join している箇所」が1か所でも残っていると、そこが将来の事故現場になります。

4. 文字コード ── Excelの挙動と.NETの罠

4.1 Excelで開くならBOM付きUTF-8

CSVの文字化けの大半は、Excelがファイルの文字コードを誤認することで起きます。BOM(ファイル先頭に付く数バイトの目印)のないUTF-8のCSVをダブルクリックで開くと、環境によってはANSI(日本語環境ではShift_JIS)として解釈され、日本語が全滅します。BOM付きUTF-8であればUTF-8として認識されるため、人がExcelでダブルクリックして開くCSVはBOM付きUTF-8にするのが現在の無難な選択です。

ここに.NET側の罠が重なります。BOMが付くかどうかは Encoding の渡し方で変わります。

// BOMなしUTF-8: エンコーディング指定を省略した場合の既定
File.WriteAllText(path, csv);

// BOM付きUTF-8: Encoding.UTF8(静的プロパティ)はBOMを出力する設定になっている
File.WriteAllText(path, csv, Encoding.UTF8);

// 意図を明示したいなら、コンストラクタで指定するのが誤解がない
File.WriteAllText(path, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));

Encoding.UTF8 を渡したときと省略したときで結果のファイルが変わる、というのは知らないと気づけない仕様です4。「システム間連携はBOMなし」「Excel向けはBOM付き」を仕様として決めたら、コード上も new UTF8Encoding(...) で意図を明示することをおすすめします。

4.2 .NET(Core)でShift_JISを使うには登録が必要

レガシーシステムとの連携では今もShift_JIS(コードページ932)のCSVが現役です。ここで重要なのは、.NET(Core)/.NET 5以降は、既定ではUnicode系とASCIIなどごく一部のエンコーディングしかサポートしないことです。Encoding.GetEncoding("shift_jis") は、System.Text.Encoding.CodePages パッケージを追加して CodePagesEncodingProvider を登録するまで例外になります1

using System.Text;

// アプリ起動時に1回だけ登録する(Program.cs等)
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

// これでShift_JIS(コードページ932)が取得できるようになる
Encoding sjis = Encoding.GetEncoding("shift_jis");

.NET Framework時代のCSV処理を.NETへ移行したアプリが、本番で初めてShift_JISのファイルを受け取って例外を出す──という事故の定番ポイントです。移行時のチェック観点は「.NET Framework→.NET移行前チェックリスト」にまとめています。

また、Shift_JISを使う以上、機種依存文字(①や㈱など)・波ダッシュ問題・Unicodeにしかない文字の欠落は避けて通れません。このあたりの文字コードの基礎は「Windowsの文字コードと文字化けの仕組み」と「Windowsの文字コードと改行コードの実務」で詳しく説明しています。

5. Excelで開くと壊れるデータ ── 先頭ゼロ・日付化・指数表記

文字コードを正しくしても、Excelでの閲覧にはもう1つの問題群があります。ExcelはCSVを開くときに値を「解釈」することです。

元のデータ Excelでの表示 何が起きたか
090123456xx(電話番号) 90123456xx 数値と解釈され先頭ゼロが消える
007(商品コード) 7 同上
1-1(枝番) 1月1日 日付と解釈される
1234567890123456(16桁の管理番号) 1.23457E+15 指数表記になり下位桁が失われる

そして重要なのは、これらはフィールドをダブルクォートで囲んでも防げないことです。引用符はCSVの構文上の区切りを示すだけで、Excelは中身を再び解釈します。

実務上の対処は次の3択です。

  1. Excel側の正しい手順で開いてもらう。 ダブルクリックではなく、Excelの「データの取り込み(テキスト/CSVから)」で列の型を「文字列」に指定して読み込めば壊れません。運用手順書に書く価値があります。
  2. 閲覧が主目的ならxlsxで出力する。 セル書式を出力側で制御できるため、この問題群が根本的に消えます。Excelファイル出力の作り方は「Excel帳票出力の作り方」を参照してください。なお、サーバーやサービスでのExcel COM自動化は非推奨であり、「EXCEL.EXEが残る問題」で書いたとおりプロセス残留の温床になるため、ライブラリベースの出力を選んでください。
  3. ="007" 形式で出力するのは最後の手段。 Excelで開くと文字列として表示される裏技ですが、CSVとしては「数式入りの非標準ファイル」になり、Excel以外の取り込み先を壊します。他システムとの兼用ファイルでは使わないでください。

6. CSVインジェクション ── ユーザー入力を数式にしない

ユーザーが入力した値(名前・備考・住所など)をCSVに出力してExcelで開かせるアプリには、CSVインジェクションという攻撃経路があります。=+-@ などで始まるフィールドをExcelが数式として解釈するため、悪意ある入力(例: =cmd|'/C calc'!A0 のような外部プロセス起動を狙う数式)が、CSVを開いた別のユーザーの環境で実行されうるという問題です5

対策の基本は、出力時に危険な先頭文字を無害化することです。

// 数式として解釈されうるフィールドの先頭にシングルクォートを付ける。
// 日本語環境では全角の=+-@も数式の開始として解釈されうるため対象に含める
static string SanitizeForExcel(string value)
{
    if (value.Length > 0 && value[0]
        is '=' or '+' or '-' or '@' or '\t' or '\r' or '\n'
        or '=' or '+' or '-' or '@')
    {
        return "'" + value;
    }
    return value;
}

先頭のシングルクォートはExcelで「文字列として扱う」印になります。なお、危険な先頭文字の一覧は表計算ソフトの実装・バージョンによって変わりうるため、このような無害化は「先頭がこの許可リスト(英数字など)に一致する場合だけそのまま通す」方向で設計すると、想定漏れに強くなります。ただし、他システムがそのCSVを取り込む場合はシングルクォートがデータとして残る点に注意が必要で、「Excelで開く用」と「システム取り込み用」でファイルを分ける判断もあり得ます。入力値の検証(そもそも先頭 = を許可しない等)と組み合わせるかどうかも含め、アプリ全体のセキュリティ設計は「Windowsアプリのセキュリティ最小チェックリスト」を参照してください。

7. 運用の落とし穴 ── フォーマット以外で事故る場所

最後に、CSVそのものの構文以外で実務の事故が起きるポイントをまとめます。

  • 数値・日付のフォーマットをOSのカルチャに依存させない。 value.ToString() はOSの地域設定の影響を受けます。小数点がカンマの地域設定では 1,5 と出力されてCSVの区切りと衝突します。ファイル連携用の変換は CultureInfo.InvariantCulture を明示し、日付は yyyy-MM-dd のような固定書式にしてください。日付の設計は「業務アプリの日時・タイムゾーン設計」で詳しく扱っています。
  • 大量データはストリーミングで処理する ── ただし「1行ずつ」でよいかは仕様次第。 File.ReadAllLines は全行を配列としてメモリに載せるため、大きなファイルには向きません6。ここで File.ReadLines(遅延列挙)や StreamReader の行単位読み込みに置き換えてよいのは、フィールド内の改行を許さないと仕様で明記されているファイルだけです。引用符内の改行を許すRFC 4180準拠のCSVでは、行単位の分割が1レコードを途中で切断します。その場合は、ファイルパスから直接読み込む TextFieldParser(全体をメモリに載せずに処理できる)や、CSVライブラリのストリーミング読み込みを使ってください。
  • 「書きかけのファイル」を読ませない・読まない。 出力側は一時ファイル名で書いてからリネームする、取り込み側は排他を確認してから読む──このファイル連携の基本形は「ファイル連携の排他制御の基礎知識」に、フォルダー監視で取り込む場合の注意は「FileSystemWatcher実務ガイド」にまとめています。
  • 列の増減に備える。 「ヘッダー行の列名で対応付ける」か「列順固定・列数チェックで不一致なら拒否する」かを仕様として明記します。暗黙のfields[7]のようなマジックナンバー参照は、連携先の些細な列追加で静かに壊れます。
  • 末尾改行と空行の扱いを決めておく。 最終行の後に改行があるか、空行をスキップするか拒否するかは、パーサー実装によって挙動が割れる部分です。TextFieldParser は空行をスキップします3

まとめ

CSVは「カンマで区切るだけ」の顔をした、方言だらけの連携フォーマットです。実務の要点は4つ──読み込みは引用符処理のできる部品を使う(Splitで自作しない)、文字コードは開く相手(特にExcel)から逆算して決めてBOMの有無まで仕様に書く、Excelの値解釈(ゼロ落ち・日付化)はCSV側では防げないと知る、ユーザー入力を出力するならインジェクション対策を入れる──これだけで、CSV連携の事故の大半は設計段階で潰せます。

既存のCSV連携の文字化けやデータ破損の調査、レガシーシステムとの連携仕様の整理、CSV取り込み・出力機能の新規実装は、実際のファイルと連携先の仕様を見ながらの判断が必要になることが多いので、迷ったらご相談ください。

関連記事

関連する相談領域

合同会社小村ソフトでは、CSV・ファイル連携を含む業務アプリの開発、既存連携の文字化け・データ破損の原因調査、レガシーシステムとの連携仕様の整理に関する技術相談を扱っています。

参考リンク

  1. Microsoft Learn, CodePagesEncodingProvider Class. .NET Coreが既定でサポートするのはASCII・ISO-8859-1・Unicode系のみであり、Shift_JIS(コードページ932)等のコードページエンコーディングにはSystem.Text.Encoding.CodePagesCodePagesEncodingProviderEncoding.RegisterProviderで登録する必要があることについて。  2

  2. IETF, RFC 4180 - Common Format and MIME Type for Comma-Separated Values (CSV) Files. CSVの事実上の標準仕様。フィールド内のカンマ・改行・ダブルクォートの引用符処理、""によるエスケープについて。 

  3. Microsoft Learn, TextFieldParser Class. 区切り形式・固定長形式のテキストパース、HasFieldsEnclosedInQuotesによる引用符処理、解析できない行でのMalformedLineExceptionErrorLine/ErrorLineNumberReadFieldsが空行をスキップすることについて。  2 3

  4. Microsoft Learn, UTF8Encoding Class. encoderShouldEmitUTF8IdentifierパラメーターによるBOM(U+FEFF)出力の制御と、Encoding.UTF8プロパティが返すインスタンスはBOMを出力する設定であることについて。 

  5. OWASP, CSV Injection. =+-@・タブ・復帰文字で始まるフィールドが表計算ソフトで数式として解釈される攻撃(Formula Injection)と、その対策について。 

  6. Microsoft Learn, File.ReadLines Method. ReadAllLinesが全行の配列を返すのに対し、ReadLinesは列挙可能なコレクションを返すため、大きなファイルでも全体を待たずに列挙を開始できることについて。 

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

CSVの文字コードは何にすべきですか?
受け取る側が決まっているなら受け取る側の仕様に合わせる、が大原則です。新規に決められる場合、人がExcelで開くファイルはBOM付きUTF-8、システム間連携はBOMなしUTF-8を仕様書に明記する、が無難な出発点です。レガシーシステムとの連携でShift_JISを求められた場合は、機種依存文字や外字の扱いを先に取り決めておかないと、後から文字化けの責任問題になります。
CSVライブラリは導入すべきですか?それとも自作でよいですか?
読み込みに関しては、自作のString.Splitでは仕様上正しく処理できないケース(フィールド内のカンマ・改行・引用符)があるため、何らかの部品を使うべきです。.NET標準の範囲ならTextFieldParserがRFC 4180相当の引用符処理に対応しています。列とオブジェクトのマッピング、型変換、大量データの性能まで求めるなら、実績のあるCSVライブラリの導入を検討してください。書き出しは仕様が単純なぶん自作も現実的ですが、引用符エスケープの共通関数を1つ用意して全部そこを通す形にしてください。
カンマ区切りではなくタブ区切り(TSV)にすべきですか?
データにカンマが頻出する(住所・氏名・金額表記など)場合、TSVにするとクォート処理の頻度が下がり、目視確認もしやすくなります。ただしタブや改行がデータに入り得る点は同じで、引用符処理が不要になるわけではありません。「TSVだから区切り文字の考慮は不要」とはならないことに注意してください。連携先との合意が取れるなら、区切り文字よりも「文字コード・引用符・改行コード・ヘッダー有無を仕様書に明記すること」の方が事故防止には効きます。
Excelで開くことが目的なら、CSVではなくExcelファイルを出力すべきですか?
「人がExcelで見る・印刷する」ことが主目的なら、xlsx形式での出力を検討する価値があります。CSVはExcelで開いた瞬間に先頭ゼロ落ちや日付化が起きる形式であり、これらをCSV側で完全に防ぐ方法はありません。xlsxならセルの書式を出力側で制御できます。一方、他システムへの取り込みが主目的で、Excelでの閲覧はおまけなら、CSVのまま「取り込み手順(データのインポート機能を使う)」を運用手順として案内する方が現実的です。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る