Apache Commons CSV 配置参数详解
解析 Apache Commons CSV 1.14.1 的 CSVFormat 全部定制参数:12 个预设方言总览、分隔符、引号模式、转义符、nullString、表头枚举映射、注释与空行空白控制,每项附实测输出。
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 4180 | Excel 方言 | MySQL 方言 |
|---|---|---|---|
| 字段含分隔符 | 双引号包裹 | 同左 | 不引(tab 分隔天然避让) |
| 字段内引号 | 两个连续引号 "" 转义 | 同左 | 反斜杠 " |
| 字段内换行 | 允许,须包裹在引号内 | 同左 | 反斜杠 \n |
| 行结束符 | CRLF | CRLF | LF |
| 注释 | 无此概念 | 无 | 无(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() 回溯源解析器。
十二、参考链接
- Commons CSV javadoc CSVFormat(1.14.1):https://commons.apache.org/proper/commons-csv/apidocs/org/apache/commons/csv/CSVFormat.html
- Commons CSV javadoc CSVFormat.Builder:https://commons.apache.org/proper/commons-csv/apidocs/org/apache/commons/csv/CSVFormat.Builder.html
- Commons CSV javadoc CSVRecord:https://commons.apache.org/proper/commons-csv/apidocs/org/apache/commons/csv/CSVRecord.html
- Commons CSV 用户指南:https://commons.apache.org/proper/commons-csv/user-guide.html
- Maven 中央仓库 commons-csv 1.14.1:https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/
- RFC 4180(CSV 格式规范):https://datatracker.ietf.org/doc/html/rfc4180