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", "山田,太郎", "yamada@example.com");

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

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


例題: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 は「どの順番で何行書くか」だけを担当し、
「一行の中身をどう組み立てるか」は UserCsvMapperCsvLineBuilder に任せています。

こうしておくと、「列を追加したい」「順番を変えたい」といった変更も、
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をコピーしました