C# Tips | ファイル・ディレクトリ操作:CSV読み込み

C sharp C#
スポンサーリンク

はじめに 「CSV読み込み」がちゃんと書けると一気に“業務っぽく”なる

業務システムで一番よく出てくるファイル形式、それが CSV です。
売上データ、マスタデータ、ログのエクスポート、外部システムとの連携――とりあえず CSV、という世界。

だからこそ、「CSV をちゃんと読めるユーティリティ」を持っているかどうかで、
あなたの C# 生活の快適さがかなり変わります。

ここでは、プログラミング初心者向けに、

  • まずは「超シンプルな読み込み」
  • 次に「CSV ならではの罠(カンマ・ダブルクォート・改行)」
  • そして「実務で使えるレベルの CSV 読み込みユーティリティ」

という順番で、例題を交えながら丁寧に解説していきます。


CSV の基本をざっくり整理する

CSV は「カンマ区切りのテキスト」だけど、それだけじゃない

CSV は「Comma Separated Values」の略で、
「1 行が 1 レコード」「カンマで項目を区切る」というのが基本ルールです。

例えば、こんな感じです。

Id,Name,Price
1,Apple,120
2,Banana,80

ここまでは簡単です。
でも、現実の CSV はもう少しややこしいです。

  • 項目の中にカンマが入ることがある(例:「東京都,千代田区」)
  • 項目の中に改行が入ることがある(長い説明文など)
  • 項目の中にダブルクォートが入ることがある

これらを表現するために、「ダブルクォートで囲む」「ダブルクォートを二重に書く」といったルールがあります。

例えば、こんな行は正しい CSV です。

3,"Orange, Big",200
4,"Multi
Line
Text",300
5,"He said ""Hello""",400

このあたりを「全部自前でパースしよう」とすると、
正直、初心者にはかなりしんどいです。

なので方針としては、

  • 「簡易 CSV(カンマも改行も含まない)」なら自前実装でもよい
  • 「ちゃんとした CSV」を扱うなら、ライブラリを使う

という二段構えで考えるのが現実的です。


まずは超シンプル版:Split で読む「簡易 CSV」

カンマも改行も含まない前提なら、これで十分

「社内ツール用の簡単な CSV」「カンマを含まないことが仕様で保証されている」
といった前提なら、string.Split を使ったシンプルな読み込みで十分です。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

public sealed class SimpleCsvRow
{
    public string[] Columns { get; }

    public SimpleCsvRow(string[] columns)
    {
        Columns = columns;
    }
}

public static class SimpleCsvReader
{
    public static IEnumerable<SimpleCsvRow> Read(string path, Encoding encoding)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("CSV ファイルが存在しません。", path);
        }

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

        string? line;
        while ((line = reader.ReadLine()) is not null)
        {
            if (string.IsNullOrWhiteSpace(line))
            {
                continue;
            }

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

            yield return new SimpleCsvRow(columns);
        }
    }
}
C#

使い方の例です。

string path = @"C:\data\items.csv";

foreach (var row in SimpleCsvReader.Read(path, Encoding.UTF8))
{
    Console.WriteLine(string.Join(" | ", row.Columns));
}
C#

ここでの重要ポイントは、「前提条件をはっきりさせる」ことです。

  • カンマは区切り専用で、値の中には出てこない
  • ダブルクォートで囲まれた値は出てこない
  • 改行を含む値は出てこない

この前提が崩れると、この実装は簡単に壊れます。
だからこそ、「簡易 CSV 用」と割り切って使うのが大事です。


実務で避けて通れない「ちゃんとした CSV」の罠

ダブルクォートとカンマと改行の三角関係

CSV の正式な仕様(RFC 4180 など)では、
値の中にカンマや改行を含めたい場合、その値をダブルクォートで囲みます。

"Tokyo, Japan"
"Line1
Line2"

さらに、値の中にダブルクォートを含めたい場合は、
ダブルクォートを二重に書きます。

"He said ""Hello"""

これを自前でパースしようとすると、
「今見ているカンマは区切りなのか、値の一部なのか」
「今見ている改行はレコードの区切りなのか、値の一部なのか」
を、ダブルクォートの内側か外側かで判断しなければなりません。

つまり、単純な Split(',') では絶対に正しく処理できません。

ここが「CSV は意外と難しい」と言われる理由です。


ちゃんとした CSV を読むなら、ライブラリを使うのが正解

自前実装より「CSV ライブラリ」を使うべき理由

業務で「外部システムから渡される CSV」「人間が Excel で編集する CSV」を扱うなら、
ほぼ確実に「ダブルクォート付き」「カンマ入り」「改行入り」の CSV に遭遇します。

これを毎回自前でパースするのは、正直コスパが悪いです。
バグりやすいし、テストも大変です。

なので、実務では素直に「CSV パーサライブラリ」を使うのが王道です。
C# だと、例えば CsvHelper などが有名です。

ここでは、「ライブラリを使う前提」で、
CSV 読み込みユーティリティの設計の仕方をイメージしてもらいます。


行を「string[]」として読む汎用 CSV リーダーのイメージ

「1 行=1 配列」として扱う

まずは、「1 行を string[] として扱う」レベルの汎用 CSV リーダーをイメージしてみましょう。
ここでは、パース部分は「ライブラリに任せる」として、
周辺の設計に集中します。

インターフェースとしては、こんな感じが分かりやすいです。

public interface ICsvRow
{
    string this[int index] { get; }
    int ColumnCount { get; }
}
C#

実装例のイメージです(パース部分は仮とします)。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

public sealed class CsvRow : ICsvRow
{
    private readonly string[] _columns;

    public CsvRow(string[] columns)
    {
        _columns = columns;
    }

    public string this[int index] => _columns[index];

    public int ColumnCount => _columns.Length;
}

public static class CsvReaderUtil
{
    public static IEnumerable<ICsvRow> ReadRows(
        string path,
        Encoding encoding,
        bool hasHeader)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("CSV ファイルが存在しません。", path);
        }

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

        string? line;

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

        while ((line = reader.ReadLine()) is not null)
        {
            if (string.IsNullOrWhiteSpace(line))
            {
                continue;
            }

            string[] columns = ParseCsvLineNaive(line);

            yield return new CsvRow(columns);
        }
    }

    private static string[] ParseCsvLineNaive(string line)
    {
        return line.Split(',');
    }
}
C#

ここではあえて「パースは雑(Split)」にしてありますが、
実務ではこの ParseCsvLineNaive の中身を、
CSV ライブラリの呼び出しに差し替えるイメージです。

重要なのは、「読み込みの枠組み(ファイルを開く・ヘッダーを飛ばす・1 行ずつ列挙する)」を
きれいに分離しておくことです。


CSV を「型付きオブジェクト」にマッピングする

「string[] のまま」だと業務コードがつらくなる

row[0] が Id、row[1] が Name、row[2] が Price――
というコードは、最初は簡単に見えますが、
業務コードが増えてくると一気に読みにくくなります。

なので、実務では「CSV の 1 行をクラスにマッピングする」スタイルがよく使われます。

例えば、こんなクラスを用意します。

public sealed class ItemRecord
{
    public int Id { get; }
    public string Name { get; }
    public int Price { get; }

    public ItemRecord(int id, string name, int price)
    {
        Id = id;
        Name = name;
        Price = price;
    }
}
C#

そして、「1 行(string[])→ ItemRecord」に変換するメソッドを用意します。

public static class CsvMapping
{
    public static ItemRecord ToItemRecord(ICsvRow row)
    {
        int id = int.Parse(row[0]);
        string name = row[1];
        int price = int.Parse(row[2]);

        return new ItemRecord(id, name, price);
    }
}
C#

使い方のイメージです。

string path = @"C:\data\items.csv";

foreach (var row in CsvReaderUtil.ReadRows(path, Encoding.UTF8, hasHeader: true))
{
    ItemRecord item = CsvMapping.ToItemRecord(row);

    Console.WriteLine($"{item.Id}: {item.Name} = {item.Price}");
}
C#

ここでの重要ポイントは、「パース」と「業務ロジック」を分けることです。

  • CSV をどう読むか(エンコーディング、ヘッダー、クォート処理)は CSV ユーティリティ側
  • 読み込んだデータをどう解釈するか(どの列が何の意味か)はマッピング側

この分離ができていると、仕様変更(列が増えた・順番が変わった)にも対応しやすくなります。


ストリーミングで読む:巨大 CSV にも耐えられる形にする

ReadAllLines は危険、1 行ずつ読むのが基本

File.ReadAllLines で全部読み込んでから処理する、という書き方もできますが、
数十万行〜数百万行の CSV を相手にすると、メモリをかなり食います。

業務では、「とりあえず全部メモリに載せる」は避けたほうが安全です。

StreamReaderReadLine で 1 行ずつ読み、
yield return で列挙可能にするスタイルは、
巨大ファイルでも安定して動く「ストリーミング処理」になります。

さっきの ReadRows がまさにそれで、
foreach で回している間だけ、順番に行を読み進めていきます。

このスタイルを一度体に染み込ませておくと、
「大きなファイルでも怖くない」感覚が身につきます。


エンコーディングと BOM をちゃんと意識する

「読み込み時の Encoding をどう決めるか」は設計ポイント

CSV 読み込みで地味にハマるのが、エンコーディングです。

  • 外部システムから来る CSV が Shift_JIS のこともある
  • 自分たちのシステムは UTF-8 で統一したい
  • Excel で開く前提だと、環境によっては Shift_JIS が前提になっている

StreamReader を作るときに、
Encoding.UTF8 なのか Encoding.GetEncoding(932)(いわゆる Shift_JIS)なのかを、
ちゃんと決めて渡す必要があります。

using var reader = new StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
C#

detectEncodingFromByteOrderMarks: true にしておくと、
UTF-8 BOM 付きファイルなどは自動で判定してくれます。

ただし、「Shift_JIS か UTF-8 か分からない」ようなファイルを完全自動で判定するのは難しいので、
前に話した「エンコーディング判定ユーティリティ」と組み合わせるのも一つの手です。


例題:売上 CSV を読み込んで合計金額を出す

最後に、ここまでの話を組み合わせた、
「業務っぽい」例を一つ書いてみます。

CSV のイメージはこんな感じです。

Id,Date,Customer,Amount
1,2025-01-01,ABC,1000
2,2025-01-01,XYZ,2000
3,2025-01-02,ABC,1500

まずはレコードクラスです。

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 行から SalesRecord に変換するマッピングです。

public static class SalesCsvMapping
{
    public static SalesRecord ToSalesRecord(ICsvRow row)
    {
        int id = int.Parse(row[0]);
        DateTime date = DateTime.Parse(row[1]);
        string customer = row[2];
        int amount = int.Parse(row[3]);

        return new SalesRecord(id, date, customer, amount);
    }
}
C#

そして、読み込み+集計のコードです。

string path = @"C:\data\sales.csv";

int totalAmount = 0;

foreach (var row in CsvReaderUtil.ReadRows(path, Encoding.UTF8, hasHeader: true))
{
    SalesRecord record = SalesCsvMapping.ToSalesRecord(row);
    totalAmount += record.Amount;
}

Console.WriteLine($"合計金額: {totalAmount}");
C#

ここまでくると、「CSV を読む」という処理が、
かなり業務コードに馴染んだ形になっているはずです。


まとめ 「CSV読み込みユーティリティ」を自分の武器にする

CSV 読み込みは、業務システムでは避けて通れないテーマです。
だからこそ、「なんとなく Split で書いた」状態から一歩進んでおくと、
後々の開発がかなり楽になります。

押さえておきたいポイントを整理すると、こうなります。

  • 簡易 CSV(カンマ・改行・クォートなし)なら Split でもよいが、前提条件を明示する。
  • ちゃんとした CSV(カンマ・改行・ダブルクォートあり)を扱うなら、自前実装ではなくライブラリを使うのが現実的。
  • 読み込みは StreamReaderReadLineyield return のストリーミングスタイルにしておくと、巨大ファイルにも強い。
  • 「1 行=string[]」から、さらに「型付きクラス」にマッピングすると、業務コードが読みやすくなる。
  • エンコーディング(UTF-8 / Shift_JIS など)を曖昧にせず、読み書きで揃える。

ここまでの考え方をベースに、
あなたの現場の CSV(項目数、ヘッダーの有無、エンコーディング、サイズ感)に合わせて、
「うち専用の CSV 読み込みユーティリティ」を育てていくと、
かなり“業務エンジニアっぽい”一歩になります。

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