CSV一行生成は「地味だけど壊れやすいところを固める」技
業務システムで CSV を出力するとき、「とりあえずカンマでつなげばいいでしょ」と思って書き始めると、すぐにハマります。
名前にカンマが入っていたり、改行が入っていたり、ダブルクォートが入っていたりすると、一気に壊れた CSV になります。
だからこそ、「CSV の一行を正しく組み立てる」ユーティリティを一つ持っておくと、
どのバッチでも、どの画面でも、同じルールで安全に CSV を出力できるようになります。
CSV の基本ルールをざっくり押さえる
ただの「カンマ区切り」ではない
CSV は「Comma Separated Values」の略ですが、単に「カンマで区切る」だけではありません。
ざっくり、次のようなルールがあります。
カンマを含むフィールドは、ダブルクォートで囲む。
改行を含むフィールドも、ダブルクォートで囲む。
ダブルクォート自体を含む場合は、" を "" と二重にしてエスケープする。
例えば、次のような値を CSV 一行にしたいとします。
id = 1name = 山田,太郎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 にしたい」とだけ考えればよく、
「何列目に何を出すか」「カンマや改行をどうエスケープするか」は、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 の細かいルール」を追い出してしまうことです。
