Java Tips | 基本ユーティリティ:CSV一行生成

Java Java
スポンサーリンク
スポンサーリンク

CSV一行生成は「地味だけど壊れやすいところを固める」技

業務システムで CSV を出力するとき、「とりあえずカンマでつなげばいいでしょ」と思って書き始めると、すぐにハマります。
名前にカンマが入っていたり、改行が入っていたり、ダブルクォートが入っていたりすると、一気に壊れた CSV になります。

だからこそ、「CSV の一行を正しく組み立てる」ユーティリティを一つ持っておくと、
どのバッチでも、どの画面でも、同じルールで安全に CSV を出力できるようになります。


CSV の基本ルールをざっくり押さえる

ただの「カンマ区切り」ではない

CSV は「Comma Separated Values」の略ですが、単に「カンマで区切る」だけではありません。
ざっくり、次のようなルールがあります。

カンマを含むフィールドは、ダブルクォートで囲む。
改行を含むフィールドも、ダブルクォートで囲む。
ダブルクォート自体を含む場合は、" を "" と二重にしてエスケープする。

例えば、次のような値を CSV 一行にしたいとします。

id = 1
name = 山田,太郎
note = 彼は"特別"なメンバーです

正しい CSV はこうなります。

1,"山田,太郎","彼は""特別""なメンバーです"

この「囲む」「二重にする」というルールを、毎回手書きするのは危険なので、ユーティリティに閉じ込めてしまうのが狙いです。


まずは「単一フィールド」をCSV用にエスケープする

1つの値を「CSVセル」に変換するメソッド

一行を組み立てる前に、「1つの値を CSV 用に安全な文字列にする」メソッドを作ります。

public final class CsvEscaper {

    private CsvEscaper() {}

    public static String escape(String value) {
        if (value == null) {
            return "";
        }
        boolean needQuote = false;

        if (value.contains(",") || value.contains("\"") || value.contains("\n") || value.contains("\r")) {
            needQuote = true;
        }

        String escaped = value.replace("\"", "\"\"");

        if (needQuote) {
            return "\"" + escaped + "\"";
        } else {
            return escaped;
        }
    }
}
Java

ここでの重要ポイントをかみ砕きます。

まず、「null は空文字にする」と決めています。
CSV では「空セル」として扱いたいことが多いので、null をそのまま "null" と書かないようにしています。

次に、「カンマ」「ダブルクォート」「改行(LF, CR)」のいずれかを含んでいたら、ダブルクォートで囲むフラグを立てています。
これらが入っていると、そのままでは CSV として壊れるので、必ず囲む必要があります。

最後に、「ダブルクォートを "" に二重化」しています。
これは CSV のお約束で、「中に " があるなら、"" と書く」というルールです。
そのうえで、必要なら全体を " で囲みます。


複数フィールドから「一行のCSV」を生成する

可変長引数で「好きなだけフィールドを渡せる」ようにする

先ほどの escape を使って、一行分の CSV を組み立てるユーティリティを作ります。

import java.util.StringJoiner;

public final class CsvLineBuilder {

    private CsvLineBuilder() {}

    public static String buildLine(String... values) {
        StringJoiner joiner = new StringJoiner(",");
        for (String v : values) {
            joiner.add(CsvEscaper.escape(v));
        }
        return joiner.toString();
    }
}
Java

使い方はとてもシンプルです。

String line = CsvLineBuilder.buildLine(
        "1",
        "山田,太郎",
        "彼は\"特別\"なメンバーです",
        null
);
System.out.println(line);
Java

出力イメージはこうなります。

1,"山田,太郎","彼は""特別""なメンバーです",

ここで深掘りしたいのは、「呼び出し側は“CSV のルール”を一切意識しなくてよくなっている」ことです。
buildLine に「そのままの値」を渡すだけで、カンマや改行やダブルクォートを含んでいても、正しい CSV 一行が返ってきます。


例題:エンティティからCSV一行を生成する

ドメインオブジェクトを「CSV行」にマッピングする

例えば、次のようなユーザー情報クラスがあるとします。

public class User {
    private final String id;
    private final String name;
    private final String email;

    public User(String id, String name, String email) {
        this.id    = id;
        this.name  = name;
        this.email = email;
    }

    public String getId()     { return id; }
    public String getName()   { return name; }
    public String getEmail()  { return email; }
}
Java

この User を CSV 一行に変換するメソッドを作ります。

public final class UserCsvMapper {

    private UserCsvMapper() {}

    public static String toCsvLine(User user) {
        return CsvLineBuilder.buildLine(
                user.getId(),
                user.getName(),
                user.getEmail()
        );
    }

    public static String header() {
        return CsvLineBuilder.buildLine("id", "name", "email");
    }
}
Java

使い方はこうです。

User user = new User("1", "山田,太郎", "[email protected]");

System.out.println(UserCsvMapper.header());
System.out.println(UserCsvMapper.toCsvLine(user));
Java

ここでの重要ポイントは、「CSV の列順や項目名を、このクラスに閉じ込めている」ことです。
呼び出し側は「User を CSV にしたい」とだけ考えればよく、
「何列目に何を出すか」「カンマや改行をどうエスケープするか」は、UserCsvMapper と CsvLineBuilder に任せられます。


例題:CSVファイルへの書き出しと組み合わせる

try-with-resources と一緒に「一行ずつ安全に書く」

CSV 一行生成ユーティリティは、ファイル書き出しと組み合わせてこそ真価を発揮します。

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

public class UserCsvWriter {

    public void write(Path path, List<User> users) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(path)) {
            writer.write(UserCsvMapper.header());
            writer.newLine();

            for (User u : users) {
                writer.write(UserCsvMapper.toCsvLine(u));
                writer.newLine();
            }
        }
    }
}
Java

ここでのポイントは、「一行の生成と、ファイルへの書き込みをきれいに分離している」ことです。
UserCsvWriter は「どの順番で何行書くか」だけを担当し、
「一行の中身をどう組み立てるか」は UserCsvMapper と CsvLineBuilder に任せています。

こうしておくと、「列を追加したい」「順番を変えたい」といった変更も、
UserCsvMapper だけを直せばよくなり、他のコードへの影響を最小限にできます。


CSV一行生成の「やりがちな落とし穴」

文字列連結でゴリゴリ書いてしまう

よくある失敗パターンは、こんなコードです。

// よくない例
String line = id + "," + name + "," + email;
Java

これだと、name にカンマや改行が入った瞬間に壊れます。
また、ダブルクォートが入っていても何も対処していないので、CSV として解釈できなくなります。

「とりあえず動く」ように見えても、現場のデータは想像以上に“汚い”ので、
最初からユーティリティを通す前提で設計しておくほうが安全です。

null をそのまま "null" と書いてしまう

String.valueOf(value) をそのまま使うと、null が "null" という文字列になってしまいます。
CSV では、「空セル」と「文字列 "null"」は意味が違うことが多いので、
ユーティリティ側で「null は空文字にする」と決めておくのが無難です。


まとめ:CSV一行生成ユーティリティで身につけたい感覚

CSV 一行生成は、「地味だけど壊れやすいところ」をユーティリティに閉じ込めて、
どこからでも同じルールで安全に使えるようにするための技です。

大事なポイントは、まず「単一フィールドを CSV 用にエスケープする」メソッドを用意すること。
次に、それを使って「可変長引数から一行を組み立てる」ビルダーを作ること。
そして、ドメインオブジェクトごとに「CSV 行へのマッピングクラス」を用意して、
業務ロジックからは「CSV の細かいルール」を追い出してしまうことです。

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