概要

CSV はシステム間のデータ連携、マスタ一括登録、帳票用データの受け渡しなど、業務システムでもっとも頻繁に扱うファイル形式のひとつです。一見単純なフォーマットですが、フィールド内にカンマを含む場合のダブルクォート対応、ヘッダー行の扱い、大容量ファイルのメモリ効率など、実務では意外と考慮点が多くなります。この記事では、標準 API だけで CSV の読み書きと簡易パースを実装し、大容量ファイルにも対応できる Files.lines() によるストリーム処理まで整理します。

使いどころ

経理部門が用意した商品マスタの CSV を読み込み、DB に一括登録する

月次売上データを CSV で出力し、他システムや Excel との連携に使う

取込処理でカンマ入りの住所フィールドを含む CSV を安全にパースする

コード例

CSV の読み書きとダブルクォート対応パーサー
import java.io.IOException;

public class CsvReadWrite {

    /** CSV ファイルを読み込む */
    public static List<String> readCsv(Path path) throws IOException {
        return Files.readAllLines(path, StandardCharsets.UTF_8);
    }

    /** CSV ファイルに書き込む */
    public static void writeCsv(Path path, List<String> lines) throws IOException {
        Files.write(path, lines, StandardCharsets.UTF_8);
    }

    /** ダブルクォート対応の簡易 CSV パーサー */
    public static String[] parseCsvLine(String line) {
        List<String> fields = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        boolean inQuotes = false;
        for (int i = 0; i < line.length(); i++) {
            char c = line.charAt(i);
            if (c == '"') {
                inQuotes = !inQuotes;
            } else if (c == ',' && !inQuotes) {
                fields.add(sb.toString());
                sb = new StringBuilder();
            } else {
                sb.append(c);
            }
        }
        fields.add(sb.toString());
        return fields.toArray(String[]::new);
    }

    public static void main(String[] args) throws IOException {
        var csvLines = List.of(
            "name,price,category",
            "apple,100,fruit",
            "\"milk, low-fat\",200,dairy"
        );

        var tempFile = Files.createTempFile("sample", ".csv");
        tempFile.toFile().deleteOnExit();
        writeCsv(tempFile, new ArrayList<>(csvLines));

        readCsv(tempFile).stream().skip(1).forEach(line -> {
            var fields = parseCsvLine(line);
            System.out.println("name=" + fields[0] + ", price=" + fields[1]);
        });

        try (var stream = Files.lines(tempFile, StandardCharsets.UTF_8)) {
            stream.skip(1)
                .map(line -> parseCsvLine(line)[0])
                .forEach(System.out::println);
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

Files.readAllLines() / Files.write() で簡潔に読み書き可能。var とメソッド参照の組み合わせで記述量が減る。

Java 17
// Java 17: Files.readAllLines() で一括読み込み
var lines = Files.readAllLines(path, StandardCharsets.UTF_8);
lines.stream().skip(1).forEach(line -> {
    var fields = parseCsvLine(line);

});

Library Comparison

Pure Java(自前パーサー)フィールド内改行がない単純な CSV で、外部依存を増やしたくない場合。ダブルクォート内の改行やエスケープを完全に扱うには実装が複雑になる。
Apache Commons CSVRFC 4180 準拠のパースが必要な場合。フィールド内改行、エスケープ、ヘッダー自動マッピングが必要なとき。外部依存が増える。小規模な CSV 処理には過剰なケースもある。
OpenCSVBean マッピング(CSV 行を POJO にバインド)が欲しい場合。ライブラリの更新頻度がやや低い。依存サイズも小さくない。

注意点

標準 API には CSV パーサーが含まれないため、ダブルクォート内の改行を扱うには自前の実装が必要。複雑な CSV は Apache Commons CSV の利用を検討すること。

Files.readAllLines() は全行をメモリに読み込むため、数十万行を超える CSV には Files.lines() のストリーム処理を使うこと。

CSV の改行コードが CRLF・LF 混在していると、行末に \r が残ってパースが壊れることがある。trim() での除去を忘れないこと。

BOM(Byte Order Mark)付き UTF-8 ファイルを読むと先頭フィールドに \uFEFF が混入する。必要に応じて除去処理を入れること。

FAQ

ヘッダー行をフィールド名として使いたい場合はどうしますか。

1行目を parseCsvLine() で分割してフィールド名配列を作り、2行目以降のインデックスと対応づけるのが標準的な方法です。

数十万行の CSV を処理するにはどうすべきですか。

Files.lines() で Stream を取得し、1行ずつ処理すればメモリ消費を抑えられます。try-with-resources で確実に close してください。

TSV(タブ区切り)にも同じパーサーは使えますか。

区切り文字をカンマからタブに変えるだけで対応できます。ダブルクォートのルールも CSV と同様に適用されます。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます