Apache Commons CSV 配置参数详解

解析 Apache Commons CSV 1.14.1 的 CSVFormat 全部定制参数:12 个预设方言总览、分隔符、引号模式、转义符、nullString、表头枚举映射、注释与空行空白控制,每项附实测输出。

Apache Commons CSVApache Commons CSV2026/10/13

Apache Commons CSV 配置参数详解

目标:逐项讲清 Apache Commons CSV 1.14.1 中 CSVFormat 的定制参数(分隔符、引号、转义、换行、空值、表头、注释、空行与空白控制),每个参数配可直接运行的示例与真实输出。已在 Ubuntu 22.04 / CentOS 8(JDK 17 + Maven)实测通过。

一、环境信息

本文实验环境与 jar 获取方式如下,三行命令可在任意全新服务器复现。

cat /etc/os-release | head -2 && uname -m

回显:

NAME="Ubuntu"
PRETTY_NAME="Ubuntu 22.04.5 LTS"
x86_64
java -version | head -1

回显:

openjdk version "17.0.20.1" 2026-08-18

获取实验用 jar(主包加两个运行期依赖,缺一运行时报错):

mkdir -p /opt/csvlib && cd /opt/csvlib
curl -fsSLO https://downloads.apache.org/commons/csv/binaries/commons-csv-1.14.1-bin.tar.gz
curl -fsSLO https://downloads.apache.org/commons/csv/binaries/commons-csv-1.14.1-bin.tar.gz.sha512
echo "$(cat commons-csv-1.14.1-bin.tar.gz.sha512)  commons-csv-1.14.1-bin.tar.gz" | sha512sum -c -
tar xzf commons-csv-1.14.1-bin.tar.gz
curl -fsSLO https://repo1.maven.org/maven2/commons-io/commons-io/2.20.0/commons-io-2.20.0.jar
curl -fsSLO https://repo1.maven.org/maven2/commons-codec/commons-codec/1.19.0/commons-codec-1.19.0.jar

回显:

commons-csv-1.14.1-bin.tar.gz: OK
ls /opt/csvlib/

回显:

commons-codec-1.19.0.jar  commons-csv-1.14.1  commons-csv-1.14.1-bin.tar.gz  commons-csv-1.14.1-bin.tar.gz.sha512  commons-io-2.20.0.jar

本文示例统一用如下 classpath(后续章节记为 CP):

/opt/csvlib/commons-csv-1.14.1/commons-csv-1.14.1.jar:/opt/csvlib/commons-io-2.20.0.jar:/opt/csvlib/commons-codec-1.19.0.jar

二、CSVFormat 与预设总览

CSVFormat 是 Commons CSV 的"方言"对象:一次定义分隔符、引号、转义、换行等规则,解析与写出共用,且实例不可变,改动必须通过 Builder 生成新实例。

2.1 内置预设清单

jar 内 CSVFormat.Predefined 枚举完整列出了全部预设:

javap -cp /opt/csvlib/commons-csv-1.14.1/commons-csv-1.14.1.jar org.apache.commons.csv.CSVFormat\$Predefined | head -16

回显:

Compiled from "CSVFormat.java"
public final class org.apache.commons.csv.CSVFormat$Predefined extends java.lang.Enum<org.apache.commons.csv.CSVFormat$Predefined> {
  public static final org.apache.commons.csv.CSVFormat$Predefined Default;
  public static final org.apache.commons.csv.CSVFormat$Predefined Excel;
  public static final org.apache.commons.csv.CSVFormat$Predefined InformixUnload;
  public static final org.apache.commons.csv.CSVFormat$Predefined InformixUnloadCsv;
  public static final org.apache.commons.csv.CSVFormat$Predefined MongoDBCsv;
  public static final org.apache.commons.csv.CSVFormat$Predefined MongoDBTsv;
  public static final org.apache.commons.csv.CSVFormat$Predefined MySQL;
  public static final org.apache.commons.csv.CSVFormat$Predefined Oracle;
  public static final org.apache.commons.csv.CSVFormat$Predefined PostgreSQLCsv;
  public static final org.apache.commons.csv.CSVFormat$Predefined PostgreSQLText;
  public static final org.apache.commons.csv.CSVFormat$Predefined RFC4180;
  public static final org.apache.commons.csv.CSVFormat$Predefined TDF;

2.2 预设配置总表(反射实测)

用反射逐项读取每个预设的 getter,得到下表(实测值,tab 显示为 \t):

预设分隔符引号引号模式转义行分隔nullString空行去空白
DEFAULT,"未设(MINIMAL 语义)无\r\n无忽略保留
RFC4180,"未设无\r\n无保留保留
EXCEL,"未设无\r\n无保留保留
TDF\t"未设无\r\n无忽略忽略
MYSQL\t无ALL_NON_NULL\\n\N保留保留
ORACLE,"MINIMAL\\n\N保留去除(trim=true)
INFORMIX_UNLOAD|"未设\\n无忽略保留
INFORMIX_UNLOAD_CSV,"未设无\n无忽略保留
MONGODB_CSV,"MINIMAL"\r\n无忽略保留
MONGODB_TSV\t"MINIMAL"\r\n无忽略保留
POSTGRESQL_CSV,"ALL_NON_NULL无\n空串保留保留
POSTGRESQL_TEXT\t无ALL_NON_NULL\\n\N保留保留

逐项解说:DEFAULT 与 RFC4180 的唯一差别是空行策略(DEFAULT 忽略空行);EXCEL 相比 DEFAULT 保留空行并允许缺失列名;MYSQL/POSTGRESQL_TEXT 用 tab 分隔、无引号、反斜杠转义,行尾 \n;ORACLE 是唯一默认 trim=true 的预设。

💡 技术拓展:RFC 4180 对 CSV 的核心规定与各方言的差异点。

规则RFC 4180Excel 方言MySQL 方言
字段含分隔符双引号包裹同左不引(tab 分隔天然避让)
字段内引号两个连续引号 "" 转义同左反斜杠 "
字段内换行允许,须包裹在引号内同左反斜杠 \n
行结束符CRLFCRLFLF
注释无此概念无无(Commons CSV 可自定义 #)

点名本文所用:以 RFC 4180 为基准理解引号与换行,再按目标系统(Excel/MySQL 导入导出)切换对应预设。

2.3 调试视图:toString

CSVFormat 的 toString 直接可读:

cd /opt/csvlab && cat > FmtStr.java <<'EOF'
import org.apache.commons.csv.*;

public class FmtStr {
    public static void main(String[] args) {
        System.out.println(CSVFormat.DEFAULT.toString().replace("\r\n", "\\r\\n"));
        System.out.println(CSVFormat.MYSQL.toString().replace("\n", "\\n").replace("\t", "\\t"));
        System.out.println(CSVFormat.TDF.toString().replace("\r\n", "\\r\\n").replace("\t", "\\t"));
    }
}
EOF
javac -cp "$CP" FmtStr.java && java -cp ".:$CP" FmtStr

回显(CP 为第一章 classpath,下同):

Delimiter=<,> QuoteChar=<"> RecordSeparator=<\r\n> EmptyLines:ignored SkipHeaderRecord:false
Delimiter=<\t> Escape=<\> QuoteMode=<ALL_NON_NULL> NullString=<\N> RecordSeparator=<\n> SkipHeaderRecord:false
Delimiter=<\t> QuoteChar=<"> RecordSeparator=<\r\n> EmptyLines:ignored SurroundingSpaces:ignored SkipHeaderRecord:false

三、分隔符 delimiter

Builder 中由 setDelimiter 设置,1.14.1 既接受单字符也接受多字符序列;读取侧配套 getter 是 getDelimiterString()(老方法 getDelimiter() 返回 char,已标记废弃)。

cd /opt/csvlab && cat > DelimDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class DelimDemo {
    public static void main(String[] args) throws Exception {
        CSVFormat semi = CSVFormat.DEFAULT.builder().setDelimiter(';').get();
        try (CSVParser p = CSVParser.parse("a;b;c", semi)) {
            System.out.println("1) setDelimiter(';') -> " + String.join("|", p.iterator().next().values()));
        }
        CSVFormat multi = CSVFormat.DEFAULT.builder().setDelimiter("||").get();
        try (CSVParser p = CSVParser.parse("a||b||c", multi)) {
            System.out.println("2) setDelimiter(\"||\") -> " + String.join("|", p.iterator().next().values()));
        }
    }
}
EOF
javac -cp "$CP" DelimDemo.java && java -cp ".:$CP" DelimDemo

回显:

1) setDelimiter(';') -> a|b|c
2) setDelimiter("||") -> a|b|c

说明:分号分隔常见于欧洲 Excel 导出;多字符分隔符在旧版本库不支持,1.14.1 实测可用。

四、引号与引号模式 quote/quoteMode

引号字符由 setQuote 设置(默认双引号),是否包裹、何时包裹由 QuoteMode 决定。

💡 技术拓展:QuoteMode 是"多义名词"——同一个引号字符在不同模式下行为完全不同。

模式写出行为
MINIMAL仅在字段含分隔符/引号/换行时包裹(默认语义)
ALL所有非空字段都包裹,null 输出为空
ALL_NON_NULL非空字段都包裹,null 不处理
NON_NUMERIC非数字字段包裹,数字不包裹
NONE从不包裹,特殊字符靠转义符

逐项说明:MINIMAL 产出最短输出;ALL 兼容某些要求整行引号的下游;NONE 必须配合转义符否则写出后无法回读。

cd /opt/csvlab && cat > QuoteDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class QuoteDemo {
    public static void main(String[] args) throws Exception {
        StringBuilder sb = new StringBuilder();
        try (CSVPrinter printer = new CSVPrinter(sb, CSVFormat.DEFAULT.builder().setQuoteMode(QuoteMode.ALL).get())) {
            printer.printRecord("x", "y,z", null);
        }
        System.out.println("9a) QuoteMode.ALL -> " + sb);
        sb.setLength(0);
        try (CSVPrinter printer = new CSVPrinter(sb, CSVFormat.DEFAULT)) {
            printer.printRecord("x", "y,z", null);
        }
        System.out.println("9b) default(MINIMAL) -> " + sb);
        sb.setLength(0);
        try (CSVPrinter printer = new CSVPrinter(sb, CSVFormat.DEFAULT.builder().setQuoteMode(QuoteMode.NON_NUMERIC).get())) {
            printer.printRecord("x", 42, null);
        }
        System.out.println("9c) QuoteMode.NON_NUMERIC -> " + sb);
    }
}
EOF
javac -cp "$CP" QuoteDemo.java && java -cp ".:$CP" QuoteDemo

回显(记录行尾的空行来自记录分隔符 \r\n,为真实输出):

9a) QuoteMode.ALL -> "x","y,z",

9b) default(MINIMAL) -> x,"y,z",

9c) QuoteMode.NON_NUMERIC -> "x",42,

对照说明:同一组数据,ALL 把 x 也引起来;MINIMAL 只引含逗号的 y,z;NON_NUMERIC 放过数字 42;null 三种模式都输出为空。预设里的 MYSQL 采用 ALL_NON_NULL,POSTGRESQL_CSV 亦然。

五、转义符 escape

setEscape 定义转义字符:引号外另一层"还原特殊字符"的机制,写出时对特殊字符前加转义符,读回时还原。

cd /opt/csvlab && cat > EscDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class EscDemo {
    public static void main(String[] args) throws Exception {
        String v1 = "a\tb";
        String v2 = "a\\b";
        StringBuilder sb = new StringBuilder();
        try (CSVPrinter p = new CSVPrinter(sb, CSVFormat.MYSQL)) {
            p.printRecord(v1, v2);
        }
        System.out.println("MYSQL printed bytes: " + esc(sb.toString()));
        sb.setLength(0);
        try (CSVPrinter p = new CSVPrinter(sb, CSVFormat.DEFAULT)) {
            p.printRecord(v1, v2);
        }
        System.out.println("DEFAULT printed bytes: " + esc(sb.toString()));
    }
    static String esc(String s) { StringBuilder b = new StringBuilder(); for (char c : s.toCharArray()) b.append(String.format("\\u%04x", (int) c)); return b.toString(); }
}
EOF
javac -cp "$CP" EscDemo.java && java -cp ".:$CP" EscDemo

回显(字节级转储):

MYSQL printed bytes: \u0061\u005c\u0009\u0062\u0009\u0061\u005c\u005c\u0062\u000a
DEFAULT printed bytes: \u0061\u0009\u0062\u002c\u0061\u005c\u0062\u000d\u000a

逐字节解读:字段值 a+TAB+b 在 MYSQL(escape=\)下写出为 a、\、TAB、b(\u005c 是反斜杠,插在 TAB 前);值 a+反斜杠+b 写出为 a、\、\、b(反斜杠翻倍)。DEFAULT 无转义符,TAB 与反斜杠原样直写(\u0009、\u005c),行尾 \r\n。这也解释了为什么 MYSQL 预设不需要引号也能安全往返。

六、记录分隔符 recordSeparator

setRecordSeparator 控制写出与行边界识别。各预设取值见 2.2 表:DEFAULT/RFC4180/EXCEL/TDF 为 \r\n,MYSQL/ORACLE/INFORMIX 系为 \n。

cd /opt/csvlab && cat > SepDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class SepDemo {
    public static void main(String[] args) throws Exception {
        CSVFormat unix = CSVFormat.DEFAULT.builder().setRecordSeparator('\n').get();
        System.out.println("8) setRecordSeparator('\\n') -> " + esc(unix.getRecordSeparator()));
    }
    static String esc(String s) { StringBuilder b = new StringBuilder(); for (char c : s.toCharArray()) b.append(String.format("\\u%04x", (int) c)); return b.toString(); }
}
EOF
javac -cp "$CP" SepDemo.java && java -cp ".:$CP" SepDemo

回显:

8) setRecordSeparator('\n') -> \u000a

说明:读取侧 \r\n、\n、\r 都能按行识别;该参数主要影响写出。与 Linux 工具(column/awk)配合时建议显式设为 \n,避免 CRLF 干扰对齐(详见本专栏 CLI 篇的 column 实验)。

七、空值映射 nullString

setNullString 定义"哪个字符串读回来算 null"。注意方向:它是读侧映射,写 null 时 MYSQL 输出 \N、POSTGRESQL_CSV 输出空串。

cd /opt/csvlab && cat > NullDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class NullDemo {
    public static void main(String[] args) throws Exception {
        try (CSVParser p = CSVParser.parse("a,,c", CSVFormat.POSTGRESQL_CSV)) {
            CSVRecord r = p.iterator().next();
            System.out.println("6a) POSTGRESQL_CSV nullString=\"\" -> get(1)==null ? " + (r.get(1) == null));
        }
        String src = "a\t\\N\tc";
        try (CSVParser p = CSVParser.parse(src, CSVFormat.MYSQL)) {
            CSVRecord r = p.iterator().next();
            System.out.println("6b) MYSQL nullString=\\N -> size=" + r.size() + ", get(1)==null ? " + (r.get(1) == null));
        }
    }
}
EOF
javac -cp "$CP" NullDemo.java && java -cp ".:$CP" NullDemo

回显:

6a) POSTGRESQL_CSV nullString="" -> get(1)==null ? true
6b) MYSQL nullString=\N -> size=3, get(1)==null ? true

说明:POSTGRESQL_CSV 把空字段读为 null;MYSQL 把字面 \N 读为 null,与 LOAD DATA 的约定一致,做数据库导入导出时直接用对应预设即可对齐语义。

八、表头 setHeader 与枚举映射

Builder 的 setHeader 有四个重载:setHeader(String...)、setHeader(Class<? extends Enum<?>>)、setHeader(ResultSet)、setHeader(ResultSetMetaData)。

8.1 字符串表头与自动读首行

setHeader() 空参表示把数据文件第一行当表头,配合 setSkipHeaderRecord(true) 跳过首行:

cd /opt/csvlab && cat > HeaderDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class HeaderDemo {
    public static void main(String[] args) throws Exception {
        String data = "id,name,score\n101,Alice,92.5\n102,Bob,\n";
        CSVFormat fmt = CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).get();
        try (CSVParser parser = CSVParser.parse(data, fmt)) {
            System.out.println("getHeaderNames() = " + parser.getHeaderNames());
            for (CSVRecord r : parser) {
                System.out.println(r.getRecordNumber() + ": get(\"name\")=" + r.get("name") + ", toMap()=" + r.toMap());
            }
        }
    }
}
EOF
javac -cp "$CP" HeaderDemo.java && java -cp ".:$CP" HeaderDemo

回显:

getHeaderNames() = [id, name, score]
1: get("name")=Alice, toMap()={id=101, name=Alice, score=92.5}
2: get("name")=Bob, toMap()={id=102, name=Bob, score=}

8.2 枚举表头映射

枚举方式把"列名"固化为编译期常量,拼写错误在编译期暴露:

cd /opt/csvlab && cat > EnumDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class EnumDemo {
    enum User {
        NAME, EMAIL, AGE
    }
    public static void main(String[] args) throws Exception {
        String data = "Name,Email,Age\nAlice,alice@example.com,30\nBob,bob@example.com,25\n";
        CSVFormat fmt = CSVFormat.DEFAULT.builder()
                .setHeader(User.class)
                .setSkipHeaderRecord(true)
                .setIgnoreHeaderCase(true)
                .get();
        try (CSVParser parser = CSVParser.parse(data, fmt)) {
            System.out.println("headerNames=" + parser.getHeaderNames());
            for (CSVRecord r : parser) {
                System.out.println(r.get(User.NAME) + " <" + r.get(User.EMAIL) + "> age=" + r.get(User.AGE)
                        + " consistent=" + r.isConsistent());
            }
        }
    }
}
EOF
javac -cp "$CP" EnumDemo.java && java -cp ".:$CP" EnumDemo

回显:

headerNames=[NAME, EMAIL, AGE]
Alice <alice@example.com> age=30 consistent=true
Bob <bob@example.com> age=25 consistent=true

注意两点:parser.getHeaderNames() 返回的是枚举名(NAME/EMAIL/AGE),数据文件里的 Name/Email/Age 大小写差异由 setIgnoreHeaderCase(true) 吸收;实测中即使不开 ignoreHeaderCase,r.get(User.NAME) 的枚举查找对大小写也宽容,显式开启可同时约束 get(String) 行为。

8.3 重复表头与缺失表头名

cd /opt/csvlab && cat > RaggedDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class RaggedDemo {
    public static void main(String[] args) throws Exception {
        String dup = "id,id\n1,2\n";
        for (DuplicateHeaderMode mode : DuplicateHeaderMode.values()) {
            try (CSVParser p = CSVParser.parse(dup, CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).setDuplicateHeaderMode(mode).get())) {
                System.out.println(mode + " -> headerNames=" + p.getHeaderNames() + " toMap=" + p.iterator().next().toMap());
            } catch (Exception e) {
                System.out.println(mode + " -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
            }
        }
        String emptyName = "id,\n1,2\n";
        try (CSVParser p = CSVParser.parse(emptyName, CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).get())) {
            System.out.println("empty header name -> " + p.getHeaderNames());
        } catch (Exception e) {
            System.out.println("empty header name -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    }
}
EOF
javac -cp "$CP" RaggedDemo.java && java -cp ".:$CP" RaggedDemo

回显:

ALLOW_ALL -> headerNames=[id, id] toMap={id=2}
ALLOW_EMPTY -> IllegalArgumentException: The header contains a duplicate name: "id" in [id, id]. If this is valid then use CSVFormat.Builder.setDuplicateHeaderMode().
DISALLOW -> IllegalArgumentException: The header contains a duplicate name: "id" in [id, id]. If this is valid then use CSVFormat.Builder.setDuplicateHeaderMode().
empty header name -> IllegalArgumentException: A header name is missing in [id, ]

说明:DuplicateHeaderMode 三档中 ALLOW_ALL 允许重复(toMap 后者覆盖前者);默认档位对非空重复名直接抛 IllegalArgumentException,异常消息本身给出了修复建议;表头出现空列名同样默认报错。

九、注释 commentMarker 与 headerComments

setCommentMarker('#') 后,行首为 # 的行在解析时跳过;setHeaderComments 定义写出时置于表头前的注释。

cd /opt/csvlab && cat > CommentDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class CommentDemo {
    public static void main(String[] args) throws Exception {
        CSVFormat withComment = CSVFormat.DEFAULT.builder().setCommentMarker('#').get();
        String src = "a,b\n#comment line\n1,2";
        try (CSVParser p = CSVParser.parse(src, withComment)) {
            System.out.println("3) setCommentMarker('#') -> records=" + p.getRecords().size());
        }
        CSVFormat hc = CSVFormat.DEFAULT.builder().setCommentMarker('#').setHeaderComments("generated by demo", "2026").get();
        System.out.println("headerComments=" + java.util.Arrays.toString(hc.getHeaderComments()));
    }
}
EOF
javac -cp "$CP" CommentDemo.java && java -cp ".:$CP" CommentDemo

回显:

3) setCommentMarker('#') -> records=2
headerComments=[generated by demo, 2026]

说明:不设 commentMarker 的 DEFAULT 解析同一输入会把 # 行当数据记录(第一章预设表 commentMarker 全为无);设置后 # 行被跳过,记录数从 3 变 2。CSVPrinter 的 printComment 写注释同样依赖该配置。

十、空行与空白控制

四个参数覆盖"空行算不算记录""字段两侧空白去不去"两类需求。

10.1 ignoreEmptyLines

cd /opt/csvlab && cat > BlankDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class BlankDemo {
    public static void main(String[] args) throws Exception {
        String src = "1,2\n\n\n3,4";
        try (CSVParser p = CSVParser.parse(src, CSVFormat.DEFAULT)) {
            System.out.println("4a) DEFAULT ignoreEmptyLines=true -> records=" + p.getRecords().size());
        }
        try (CSVParser p = CSVParser.parse(src, CSVFormat.RFC4180)) {
            System.out.println("4b) RFC4180 ignoreEmptyLines=false -> records=" + p.getRecords().size());
        }
    }
}
EOF
javac -cp "$CP" BlankDemo.java && java -cp ".:$CP" BlankDemo

回显:

4a) DEFAULT ignoreEmptyLines=true -> records=2
4b) RFC4180 ignoreEmptyLines=false -> records=4

10.2 trim 与 ignoreSurroundingSpaces

cd /opt/csvlab && cat > TrimDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class TrimDemo {
    public static void main(String[] args) throws Exception {
        String src = "  aa  ,  bb  ";
        try (CSVParser p = CSVParser.parse(src, CSVFormat.DEFAULT.builder().setTrim(true).get())) {
            System.out.println("5a) setTrim(true) -> [" + String.join("] [", p.iterator().next().values()) + "]");
        }
        try (CSVParser p = CSVParser.parse(src, CSVFormat.DEFAULT.builder().setIgnoreSurroundingSpaces(true).get())) {
            System.out.println("5b) setIgnoreSurroundingSpaces(true) -> [" + String.join("] [", p.iterator().next().values()) + "]");
        }
    }
}
EOF
javac -cp "$CP" TrimDemo.java && java -cp ".:$CP" TrimDemo

回显:

5a) setTrim(true) -> [aa] [bb]
5b) setIgnoreSurroundingSpaces(true) -> [aa] [bb]

两者的差别:trim 连引号包裹字段内的空白也处理;ignoreSurroundingSpaces 只处理分隔符与引号外的空白。默认两者都关闭(除 ORACLE 预设 trim=true、TDF 预设 ignoreSurroundingSpaces=true)。

10.3 trailingDelimiter

cd /opt/csvlab && cat > TrailDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class TrailDemo {
    public static void main(String[] args) throws Exception {
        String src = "a,b,c,\n1,2,3,";
        try (CSVParser p = CSVParser.parse(src, CSVFormat.DEFAULT.builder().setTrailingDelimiter(true).get())) {
            System.out.println("7a) setTrailingDelimiter(true) -> size=" + p.iterator().next().size());
        }
        try (CSVParser p = CSVParser.parse(src, CSVFormat.DEFAULT)) {
            System.out.println("7b) default -> size=" + p.iterator().next().size());
        }
    }
}
EOF
javac -cp "$CP" TrailDemo.java && java -cp ".:$CP" TrailDemo

回显:

7a) setTrailingDelimiter(true) -> size=3
7b) default -> size=4

说明:某些系统导出每行末尾多一个分隔符;默认按"行尾空字段"处理得到 4 列,开启 trailingDelimiter 后行尾分隔符被忽略,回到 3 列。

十一、CSVRecord API 与一致性校验

CSVRecord 是一行数据的运行时载体,常用方法实测如下:

cd /opt/csvlab && cat > RecordDemo.java <<'EOF'
import org.apache.commons.csv.*;

public class RecordDemo {
    public static void main(String[] args) throws Exception {
        String data = "id,name,score\n101,Alice,92.5\n102,Bob,\n";
        CSVFormat fmt = CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).get();
        try (CSVParser parser = CSVParser.parse(data, fmt)) {
            for (CSVRecord r : parser) {
                System.out.println("--- record " + r.getRecordNumber() + " ---");
                System.out.println("get(\"name\")=" + r.get("name") + "  get(0)=" + r.get(0) + "  size()=" + r.size());
                System.out.println("isSet(\"score\")=" + r.isSet("score") + "  isMapped(\"score\")=" + r.isMapped("score") + "  isMapped(\"ghost\")=" + r.isMapped("ghost"));
                System.out.println("isConsistent()=" + r.isConsistent());
                System.out.println("values()=" + java.util.Arrays.toString(r.values()));
            }
        }
        String ragged = "id,name,score\n101,Alice\n";
        try (CSVParser parser = CSVParser.parse(ragged, CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).get())) {
            for (CSVRecord r : parser) {
                System.out.println("ragged: size=" + r.size() + " consistent=" + r.isConsistent() + " isSet(\"score\")=" + r.isSet("score"));
            }
        }
    }
}
EOF
javac -cp "$CP" RecordDemo.java && java -cp ".:$CP" RecordDemo

回显:

--- record 1 ---
get("name")=Alice  get(0)=101  size()=3
isSet("score")=true  isMapped("score")=true  isMapped("ghost")=false
isConsistent()=true
values()=[101, Alice, 92.5]
--- record 2 ---
get("name")=Bob  get(0)=102  size()=3
isSet("score")=true  isMapped("score")=true  isMapped("ghost")=false
isConsistent()=true
values()=[102, Bob, ]
ragged: size=2 consistent=false isSet("score")=false

API 解说:get 有三种重载(下标、列名、枚举);isMapped 判断列名是否存在映射,isSet 判断本条记录是否覆盖该列;缺列的记录(ragged 行)解析不抛异常,靠 isConsistent() 返回 false 标记,批量处理时应在循环内检查该标志。另有 getRecordNumber() 从 1 计数、toMap() 转列名到值的映射、getParser() 回溯源解析器。

十二、参考链接

  1. Commons CSV javadoc CSVFormat(1.14.1):https://commons.apache.org/proper/commons-csv/apidocs/org/apache/commons/csv/CSVFormat.html
  2. Commons CSV javadoc CSVFormat.Builder:https://commons.apache.org/proper/commons-csv/apidocs/org/apache/commons/csv/CSVFormat.Builder.html
  3. Commons CSV javadoc CSVRecord:https://commons.apache.org/proper/commons-csv/apidocs/org/apache/commons/csv/CSVRecord.html
  4. Commons CSV 用户指南:https://commons.apache.org/proper/commons-csv/user-guide.html
  5. Maven 中央仓库 commons-csv 1.14.1:https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/
  6. RFC 4180(CSV 格式规范):https://datatracker.ietf.org/doc/html/rfc4180