引用符で囲まれたカンマ入りセルがあるCSV表の図。単純なsplitでは区切れないことを示す

実装・エラー解決

JavaでCSVを読み込む方法|カンマ・改行・ダブルクォートを含むデータに対応する

3列のCSVを line.split(",") で読んでいたら、名前にカンマが入った行から列がずれた。1セル内に改行があると、行を読む時点でも崩れます。

CSV取込で私が取る方針は、区切り位置を自作しないことです。ファイルの形式と文字コードを決め、対応するパーサーへ任せます。そのうえで、ヘッダー・列数・必須値という業務上の条件を自分たちのコードで検証します。以下は Java 21、Apache Commons CSV 1.14.1 の例です。

split が壊れる入力を見る

id,name,note
1,"山田, 太郎","1行目
2行目"
2,佐藤,"""要確認"""

このCSVは3列です。1行目の名前に含まれるカンマは区切りではなく、メモ内の改行も次のレコードの開始ではありません。2行目の """要確認""" は、セル内に引用符を含む表現です。

split(",") は引用符を解釈しないので、1件目を4列以上に分けます。Files.readAllLines で物理行を先に切る方法も、メモ中の改行で1件を二つに分けます。単純なCSVだけ動いた実装を、一般のCSV入力に広げないことが大切です。

パーサーに形式を任せ、列契約を自分で確認する

Mavenなら org.apache.commons:commons-csv:1.14.1 を追加します。以下の取込では、1行目をヘッダーとして読み、期待する列名・順番と比較します。

CSVで壊れやすい3つの入力。カンマ:引用符の内側は区切らない。改行:セル内の改行を保持。二重引用符:エスケープして解釈。列数:不正な行は通知する
CSVで壊れやすい3つの入力。引用符と改行を含む行を扱う
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.csv.DuplicateHeaderMode;

public final class CustomerCsvReader {
    public record Customer(String id, String name, String note) {}

    public static List<Customer> read(Path path) throws IOException {
        var format = CSVFormat.RFC4180.builder()
                .setHeader() // 先頭レコードからヘッダー名を読む
                .setSkipHeaderRecord(true)
                .setDuplicateHeaderMode(DuplicateHeaderMode.DISALLOW)
                .get();
        List<Customer> customers = new ArrayList<>();

        try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8);
             CSVParser parser = format.parse(reader)) {
            if (!parser.getHeaderNames().equals(List.of("id", "name", "note"))) {
                throw new IllegalArgumentException("CSVの列名または順番が違います");
            }
            for (CSVRecord row : parser) {
                if (row.size() != 3) {
                    throw new IllegalArgumentException(
                            "レコード" + row.getRecordNumber() + "の列数が違います");
                }
                String id = row.get("id");
                String name = row.get("name");
                if (id.isBlank() || name.isBlank()) {
                    throw new IllegalArgumentException(
                            "レコード" + row.getRecordNumber() + "の必須項目が空です");
                }
                customers.add(new Customer(id, name, row.get("note")));
            }
        }
        return customers;
    }
}

getRecordNumber() はCSVの論理レコード番号です。セル内改行を含む場合、テキストエディタで見た物理行番号とは一致しません。エラー画面に「ファイルの3行目」と断定して表示しないでください。

また、setHeader() はファイルの1行目からヘッダーを読み取ります。先頭行にヘッダーがないCSVなら、列名を明示する構成へ変えます。列順を許す要件なら、ヘッダーの集合を確認して名前で取得する方針にもできます。この例は外部連携の列順が契約で固定されている想定です。

文字コードと取込失敗時の扱いを決める

上のコードはUTF-8を前提にします。Excelで出力したファイルが別の文字コードなら、文字化けはCSVの引用符処理とは別問題です。連携元と文字コードを確認し、決まった値を指定してください。UTF-8 BOMが付くファイルでは、先頭ヘッダーが \uFEFFid になり得るため、BOMを取り除くReaderを使うか、連携仕様でBOMなしに固定します。

業務取込では、途中で1行失敗したときに「全件取り消し」「正常行だけ登録」のどちらにするかも決めます。上の例は読み取り結果をメモリに集め、異常なら例外で止めます。大量ファイルを扱う場合は逐次処理とトランザクションを設計し直してください。

確認する入力は、通常値、カンマ入り、セル内改行、引用符、空欄、列不足、列過多、重複ヘッダーです。パーサーがCSV構文を読めることと、業務に取り込んでよいことは別の判定です。

参考資料

-実装・エラー解決
-