TSV対応は「CSVの兄弟をちゃんと扱う」技
業務システムでは、CSV だけでなく TSV(Tab Separated Values)もよく出てきます。
「Excel からタブ区切りで出したデータ」「ログをタブ区切りで吐くミドルウェア」など、現場には普通に混在しています。
TSV は「カンマの代わりにタブで区切るだけでしょ?」と思いがちですが、
CSV 用に作ったユーティリティをそのまま流用しようとすると、地味に設計が崩れます。
だからこそ、「区切り文字を差し替えられる設計」にしておくことが、実務での TSV 対応の肝になります。
発想の転換:「CSV専用」ではなく「区切り文字付きテキスト」として扱う
「カンマ固定」のコードはすぐに限界が来る
前回までに作ったような CSV ユーティリティは、だいたいこんな前提で書いていました。
- 区切り文字は
,固定 - ダブルクォートで囲む/囲まないのルールは CSV 準拠
このままだと、「TSV も扱いたい」となった瞬間に、"," を "\t" に書き換えた別バージョンをコピペで作る、という悲しい未来が見えます。
そこで発想を変えて、「“区切り文字付きテキスト”を扱う汎用ユーティリティ」として設計し直すと、
CSV も TSV も「同じ仕組みの上で、区切り文字だけ変える」だけで対応できるようになります。
区切り文字をパラメータ化した「行生成ユーティリティ」
DelimitedLineBuilder という“土台”を作る
まずは、「区切り文字を外から渡せる行生成ユーティリティ」を作ります。
CSV のときは ,、TSV のときは \t を渡すイメージです。
import java.util.StringJoiner;
public final class DelimitedLineBuilder {
private DelimitedLineBuilder() {}
public static String buildLine(char delimiter, String... values) {
StringJoiner joiner = new StringJoiner(String.valueOf(delimiter));
for (String v : values) {
joiner.add(escapeForCsvLike(v, delimiter));
}
return joiner.toString();
}
private static String escapeForCsvLike(String value, char delimiter) {
if (value == null) {
return "";
}
boolean needQuote = false;
if (value.indexOf(delimiter) >= 0 ||
value.contains("\"") ||
value.contains("\n") ||
value.contains("\r")) {
needQuote = true;
}
String escaped = value.replace("\"", "\"\"");
if (needQuote) {
return "\"" + escaped + "\"";
} else {
return escaped;
}
}
}
Javaここで深掘りしたい重要ポイントは、「“CSV 的なルール”はそのままに、区切り文字だけを変えられるようにしている」ことです。
TSV でも、「タブを含む値」「改行を含む値」「ダブルクォートを含む値」が出てきたら、
CSV と同じようにダブルクォートで囲んであげたほうが安全です。
つまり、「区切り文字は変わるけれど、“囲み方とエスケープのルール”は CSV と同じでよい」という割り切りが、実務ではかなり使えます。
CSV専用・TSV専用の薄いラッパーを用意する
呼び出し側から「区切り文字」を隠す
毎回 buildLine('\t', ...) と書くのは少しノイズなので、CSV/TSV 用の薄いラッパーを用意します。
public final class CsvLines {
private CsvLines() {}
public static String build(String... values) {
return DelimitedLineBuilder.buildLine(',', values);
}
}
Javapublic final class TsvLines {
private TsvLines() {}
public static String build(String... values) {
return DelimitedLineBuilder.buildLine('\t', values);
}
}
Java使い方はこうです。
String csv = CsvLines.build("1", "山田,太郎", "営業部");
String tsv = TsvLines.build("1", "山田\t太郎", "営業部");
System.out.println(csv); // 1,"山田,太郎",営業部
System.out.println(tsv); // 1,"山田\t太郎",営業部 (タブを含む場合はダブルクォートで囲まれる)
Javaここでのポイントは、「呼び出し側は“CSV か TSV か”だけを意識すればよく、区切り文字そのものは見なくていい」ことです。
土台の DelimitedLineBuilder は「区切り文字付きテキストのルール」を知っていて、CsvLines と TsvLines は「どの区切り文字を使うか」だけを決めています。
区切り文字をパラメータ化した「行分解ユーティリティ」
CsvLineParser を「DelimitedLineParser」に進化させる
次は、CSV 用に作った CsvLineParser を、区切り文字を変えられる形に一般化します。
import java.util.ArrayList;
import java.util.List;
public final class DelimitedLineParser {
private DelimitedLineParser() {}
public static List<String> parseLine(char delimiter, 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 == delimiter && !inQuotes) {
result.add(current.toString());
current.setLength(0);
} else {
current.append(c);
}
}
result.add(current.toString());
return result;
}
}
Javaそして、CSV/TSV 用の薄いラッパーを用意します。
import java.util.List;
public final class CsvParser {
private CsvParser() {}
public static List<String> parse(String line) {
return DelimitedLineParser.parseLine(',', line);
}
}
Javaimport java.util.List;
public final class TsvParser {
private TsvParser() {}
public static List<String> parse(String line) {
return DelimitedLineParser.parseLine('\t', line);
}
}
Javaここで深掘りしたいのは、「“ダブルクォートの扱い”は CSV と TSV で共通にしている」ことです。
TSV でも、値の中にタブや改行が入ることは普通にあります。
そのとき、「ダブルクォートで囲んで、"" でエスケープする」という CSV 的ルールをそのまま使えば、
CSV と TSV の両方を同じ感覚で扱えるようになります。
例題:同じエンティティを CSV と TSV の両方で出力する
出力フォーマットだけ差し替えられる設計にする
例えば、ユーザー一覧を CSV で出すバッチと、TSV で出すバッチが両方必要になったとします。
エンティティは共通で、フォーマットだけ変えたいイメージです。
public class User {
private final String id;
private final String name;
private final String email;
// コンストラクタや getter は省略
}
JavaCSV 用と TSV 用のマッパーを、それぞれ薄く用意します。
public final class UserCsvMapper {
private UserCsvMapper() {}
public static String header() {
return CsvLines.build("id", "name", "email");
}
public static String toLine(User user) {
return CsvLines.build(
user.getId(),
user.getName(),
user.getEmail()
);
}
}
Javapublic final class UserTsvMapper {
private UserTsvMapper() {}
public static String header() {
return TsvLines.build("id", "name", "email");
}
public static String toLine(User user) {
return TsvLines.build(
user.getId(),
user.getName(),
user.getEmail()
);
}
}
Javaここでの重要ポイントは、「“列の並びや項目名”は CSV/TSV で共通だが、“区切り文字とエスケープの仕方”だけを差し替えている」ことです。
エンティティから「どの順番で何を出すか」は共通の設計で、
出力フォーマットだけを変えたいときに、マッパーの呼び分けだけで対応できます。
TSV対応で意識したい「CSVとの違い」と「共通点」
違い:人間が見るときの読みやすさ
TSV は、タブ区切りなので、テキストエディタで開いたときに「カラムが揃って見える」ことが多いです。
ログや一時的なデバッグ出力では、「TSV のほうが目で追いやすい」という理由で選ばれることもあります。
一方で、カンマよりも目に見えにくいので、「どこが区切りか分かりづらい」という側面もあります。
その意味でも、「CSV と TSV を同じユーティリティで扱えるようにしておく」と、
「人間が見たいときは TSV」「他システム連携では CSV」といった使い分けがしやすくなります。
共通点:ダブルクォートのルールはそのまま使える
実務で扱う TSV の多くは、「区切り文字がタブなだけで、ダブルクォートのルールは CSV と同じ」で問題ありません。
つまり、「タブや改行を含む値はダブルクォートで囲む」「" は "" にする」というルールを共通化できます。
ここを共通化しておくと、「CSV も TSV も“同じ世界のフォーマット”」として扱えるようになり、
ユーティリティの再利用性が一気に上がります。
まとめ:TSV対応ユーティリティで身につけたい感覚
TSV対応は、「CSV 用に書いたコードをコピペで改造する」ことではなく、
「最初から“区切り文字付きテキスト”として設計し、区切りだけ差し替えられるようにする」ための考え方です。
押さえておきたい感覚は、まず「行生成・行分解の土台を DelimitedLineBuilder/DelimitedLineParser のように一般化する」こと。
次に、「CSV 用・TSV 用の薄いラッパーを用意して、呼び出し側から区切り文字の存在を隠す」こと。
そして、「ダブルクォートのルールは CSV/TSV で共通にして、生成と分解をペアで設計する」ことです。
