Java Tips | 基本ユーティリティ:CSV分解

Java Java
スポンサーリンク

CSV分解は「カンマの意味をちゃんと理解する」技

CSV を読むときに、line.split(",") と書きたくなる気持ちはよく分かります。
でもそれをやると、名前にカンマが入っていたり、ダブルクォートで囲まれた値があった瞬間に、きれいに壊れます。

CSV分解で大事なのは、「見た目のカンマ全部で分割する」のではなく、「CSV のルールに従って“区切りとしてのカンマ”だけを認識する」ことです。
そのために、最低限のルールを自前で実装した小さなユーティリティを持っておくと、業務で扱う CSV にかなり強くなれます。


まずは「CSV一行のルール」を頭に入れる

ダブルクォートの内側と外側でカンマの意味が変わる

CSV の一行は、ざっくり言うと「フィールドがカンマで区切られている」だけですが、ダブルクォートが絡むと話が変わります。

1,山田太郎,営業部
これは素直に ["1", "山田太郎", "営業部"] です。

1,"山田,太郎","彼は""特別""なメンバーです"
これは ["1", "山田,太郎", "彼は"特別"なメンバーです"] です。

ここで重要なのは、ダブルクォートで囲まれた部分のカンマは「区切りではない」ということです。
さらに、ダブルクォート自体を表現するために "" と二重に書かれているので、
分解するときはこれを元の " に戻してあげる必要があります。

つまり、CSV分解は「ダブルクォートの内側か外側かを意識しながら、一文字ずつ読んでいく」処理になります。


最小限の「CSV一行分解ユーティリティ」を作る

状態を持ちながら一文字ずつ読む

split(",") を封印して、自前で一文字ずつ読んでいく実装を作ってみます。
ここでは「1行の CSV を List<String> に分解する」ユーティリティを用意します。

import java.util.ArrayList;
import java.util.List;

public final class CsvLineParser {

    private CsvLineParser() {}

    public static List<String> parseLine(String line) {
        List<String> result = new ArrayList<>();
        if (line == null || line.isEmpty()) {
            result.add("");
            return result;
        }

        StringBuilder current = new StringBuilder();
        boolean inQuotes = false;
        int len = line.length();

        for (int i = 0; i < len; i++) {
            char c = line.charAt(i);

            if (c == '"') {
                if (inQuotes && i + 1 < len && line.charAt(i + 1) == '"') {
                    current.append('"');
                    i++;
                } else {
                    inQuotes = !inQuotes;
                }
            } else if (c == ',' && !inQuotes) {
                result.add(current.toString());
                current.setLength(0);
            } else {
                current.append(c);
            }
        }
        result.add(current.toString());
        return result;
    }
}
Java

ここで深掘りしたい重要ポイントは三つです。

一つ目は、「inQuotes というフラグで“今ダブルクォートの内側かどうか”を管理している」ことです。
これによって、「内側のカンマは区切りではない」「外側のカンマだけ区切り」と判断できます。

二つ目は、「"" を一つの " に戻している」ことです。
inQuotes 中に " を見つけたとき、次の文字も " なら、それはエスケープされたダブルクォートなので、
" を一つだけ追加し、インデックスを一つ進めています。

三つ目は、「ループが終わったあとに最後のフィールドを必ず追加している」ことです。
行末にはカンマがないので、ループ中では追加されません。
ここを忘れると、最後の列が消えるという地味に痛いバグになります。


例題:さっきの「CSV一行生成」と組み合わせて往復させる

生成→分解で「ルールが対になっている」ことを確認する

以前作った CsvLineBuilder と今回の CsvLineParser を組み合わせて、
「一度 CSV にしてから分解しても元に戻る」ことを確認してみます。

public class CsvRoundTripExample {

    public static void main(String[] args) {
        String original1 = "1";
        String original2 = "山田,太郎";
        String original3 = "彼は\"特別\"なメンバーです";

        String line = CsvLineBuilder.buildLine(original1, original2, original3);
        System.out.println("line = " + line);

        var fields = CsvLineParser.parseLine(line);
        System.out.println("parsed1 = " + fields.get(0));
        System.out.println("parsed2 = " + fields.get(1));
        System.out.println("parsed3 = " + fields.get(2));
    }
}
Java

ここでのポイントは、「生成側と分解側のルールが“ペア”になっている」ことです。
生成側は「必要ならダブルクォートで囲み、""" にする」。
分解側は「""" に戻し、ダブルクォートの内外でカンマの扱いを変える」。

このペアが揃っていると、「どんな値でも安全に CSV に出して、安全に元に戻せる」という安心感が生まれます。


例題:CSVファイルを一行ずつ読みながら分解する

Files.lines と組み合わせてストリーム処理する

一行分解ユーティリティができたので、実際に CSV ファイルを読むコードに組み込んでみます。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class CsvReader {

    public void read(Path path) throws IOException {
        try (var lines = Files.lines(path)) {
            lines.forEach(line -> {
                List<String> fields = CsvLineParser.parseLine(line);
                System.out.println("cols=" + fields.size() + " : " + fields);
            });
        }
    }
}
Java

ここでの重要ポイントは、「一行の分解ロジックを完全に CsvLineParser に閉じ込めている」ことです。
CsvReader は「ファイルから行を読む」ことだけに集中し、
「カンマやダブルクォートをどう扱うか」は一切気にしていません。

こうしておくと、「TSV にも対応したい」「セミコロン区切りにも対応したい」といったときに、
分解ロジック側だけを差し替えればよくなります。


CSV分解の「やりがちな失敗」とどう避けるか

split(“,”) で済ませてしまう

一番多い失敗は、やはりこれです。

String[] cols = line.split(",");
Java

これだと、ダブルクォートで囲まれたカンマも区切りとして扱ってしまうので、
「列数が合わない」「値が途中で切れる」といった事故が頻発します。

「テストデータでは動いていたのに、本番データを流したら壊れた」という典型パターンなので、
「CSV を読むときに split は使わない」というマイルールを持ってしまっていいくらいです。

改行を含むフィールドを考慮していない

CSV 仕様上、ダブルクォートで囲まれていれば、フィールドの中に改行を含めることもできます。
今回の parseLine は「一行の文字列」を前提にしているので、
「ファイルから読むときに、改行を含むフィールドをどう扱うか」は別途考える必要があります。

実務でそこまで複雑な CSV を扱わないなら、「1レコード=1行」という前提で割り切るのも現実的です。
もし改行を含む可能性があるなら、既存ライブラリ(OpenCSV など)を使うほうが安全です。


既存ライブラリを使うか、自前でいくかの判断

OpenCSV などを知ったうえで、あえて「最小限だけ自前」もアリ

Java には OpenCSV や Jackson CSV モジュールなど、CSV を安全に扱うライブラリがいくつもあります。
本格的に CSV を読み書きするなら、それらを使うのが王道です。

一方で、「業務でよくある“そこまで複雑じゃない CSV”をちょっと読むだけ」という場面では、
ここまでのような「一行分解ユーティリティ」を自前で持っておくのも十分現実的です。

大事なのは、「CSV は split で済むほど単純ではない」「ダブルクォートのルールを守る必要がある」という感覚を持ったうえで、
「どこまで自前でやるか」「どこからライブラリに任せるか」をチームで決めることです。


まとめ:CSV分解ユーティリティで身につけたい感覚

CSV分解は、「カンマを見たら即 split」ではなく、「ダブルクォートの内外を意識しながら読む」ための技です。

押さえておきたい感覚は、まず「一文字ずつ読みながら inQuotes フラグで状態を管理する」こと。
次に、「""" に戻す」「行末の最後のフィールドを忘れずに追加する」といった細かいルールをユーティリティに閉じ込めること。
そして、「生成側と分解側のルールをペアにしておくと、往復しても壊れない」という設計の気持ちよさを知ることです。

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