Java中csv文件读写分析
一、txt、csv 与 tsv 文件
txt、csv 和 tsv 均属于文本文件,它们的区别主要体现在分隔符的使用规范上。
| 文件类型 | 英文全称 | 名称 | 分隔符 | 描述 |
|---|---|---|---|---|
| txt | text | 文本类型 | 无明确要求 | 可以有分隔符,也可以没有 |
| csv | Comma-separated values | 逗号分隔值类型 | 半角逗号:, | csv 是 txt 的特殊类型 |
| tsv | Tab-separated values | 制表符分隔值 | 制表符:\t | tsv 是 txt 的特殊类型 |
说明:CSV 也被称为 Char-separated values(字符分隔值类型),即通过字符值进行分隔。但由于半角逗号在数据内容中出现的概率较大,因此经常需要使用文本包装符(如双引号)来标识逗号为数据的一部分,或者直接使用其它特殊符号作为分隔符。
二、csv 文件规范
标准的 CSV 文件通常遵循以下规范:
- 行记录:每一行记录位于一个单独的行上,通常用回车换行符
CRLF(\r\n) 分割。 - 文件结尾:文件中的最后一行记录可以有结尾回车换行符,也可以没有。
- 标题头:第一行可以存在一个可选的标题头,格式和普通记录行一致。标题头应包含文件记录字段对应的名称,且数量应与记录字段一致。
- 字段分隔:在标题头行和普通行中,存在一个或多个由半角逗号
,分隔的字段。整个文件中每行应包含相同数量的字段。空格也是字段的一部分,不应被忽略。每一行记录最后一个字段后不能跟逗号。(通常用逗号分隔,也有其他字符分隔的 CSV,需事先约定)。 - 文本包装符:每个字段可用也可不用半角双引号
"括起来(如 Microsoft Excel 默认不用双引号)。如果字段没有用引号括起来,那么该字段内部不能出现双引号字符。 - 特殊字符处理:字段中若包含回车换行符、双引号或者逗号,该字段需要用双引号括起来。
- 转义规则:如果用双引号括字段,那么出现在字段内的双引号前必须再加一个双引号进行转义。
三、csv 使用场景
CSV 文件经常用于导出大批量数据(相比 Excel 更轻量级,更适合大批量数据)。
CSV 与 Excel 对比:
- 内容支持:CSV 只能用于存储纯文本内容;Excel 不仅支持纯文本内容,还支持二进制数据。
- 功能定位:CSV 可以看做是 Excel 的轻量级简单版实现,Excel 比 CSV 更加强大。
- 兼容性与用途:CSV 文件可以被 Excel 软件直接打开,一般用于表格数据的传输。
四、Java 中的 csv 类库
Java 中处理 CSV 的类库主要有以下几类:
- javacsv:该项目在 2014-12-10 后已停止维护。
- opencsv:Apache 项目,至今仍在维护,功能较为丰富。
- commons-csv:Apache Commons 项目。
- hutool:国产工具集,包含 CsvUtil 工具类。
1. javacsv
注意:该库已停止维护多年,新项目不建议使用。
2. opencsv
OpenCSV 是一个用 Java 来分析和生成 CSV 文件的框架。通常用来将 Bean 写入 CSV 文件和从 CSV 文件读出 Bean,并支持注解的方式。
Maven 依赖:
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.7.0</version>
</dependency>写入器
| 名称 | 描述 |
|---|---|
| CSVWriter | 简单的 CSV 写入器 |
| CSVParserWriter | 通过 CSVParser 解析数据的写入器 |
| StatefulBeanToCsv | 直接将 Bean 写入 CSV 的写入器 |
读取器
| 名称 | 描述 |
|---|---|
| CSVReader | 简单的 CSV 读取器 |
| CsvToBean | CSV 读取为 Bean 的读取器 |
| CSVReaderHeaderAware | 感知文件头的 CSV 读取器 |
解析器
| 名称 | 描述 |
|---|---|
| CSVParser | 简单的 CSV 解析器 |
| RFC4180Parser | 基于 RFC4180 规范的解析器 |
注解
| 注解 | 描述 | 主要属性 |
|---|---|---|
| @CsvBindByName | 按表头名称绑定 | required:必须字段,默认为 false,该字段为空抛异常column:对象列标题名称 |
| @CsvBindByPosition | 按位置绑定 | required:必须字段,默认为 false,该字段为空抛异常position:位置索引 |
| @CsvCustomBindByName | 与 CsvBindByName 相同,但必须提供自己的数据转换类 | required:必须字段,默认为 falsecolumn:对象列标题名称converter:转换器 |
| @CsvCustomBindByPosition | 与 CsvBindByPosition 相同,但必须提供自己的数据转换类 | required:必须字段,默认为 falsecolumn:对象列标题名称converter:转换器 |
| @CsvBindAndJoinByName | 应用于 MultiValuedMap 集合类型的 Bean 字段,通过标题名称绑定 | required:必须字段,默认为 falsecolumn:对象列标题名称converter:转换器mapType:集合类型elementType:元素类型 |
| @CsvBindAndJoinByPosition | 应用于 MultiValuedMap 集合类型的 Bean 字段,通过位置索引绑定 | required:必须字段,默认为 falseposition:位置索引converter:转换器mapType:集合类型elementType:元素类型 |
| @CsvBindAndSplitByName | 应用于 Collection 集合类型的 Bean 字段,通过标题名称绑定 | required:必须字段,默认为 falsecolumn:对象列标题名称converter:转换器mapType:集合类型elementType:元素类型splitOn:分隔符 |
| @CsvBindAndSplitByPosition | 应用于 Collection 集合类型的 Bean 字段,通过位置索引绑定 | required:必须字段,默认为 falseposition:位置索引converter:转换器mapType:集合类型elementType:元素类型splitOn:分隔符 |
| @CsvDate | 应用于日期/时间类型的 Bean 字段,与上面相关的绑定注解结合使用 | value:日期格式,例如:yyyy-MM-dd |
| @CsvNumber | 应用于数字类型的 Bean 字段,与上面相关的绑定注解结合使用 | value:数字格式,例如:000.### |
映射策略

| 名称 | 描述 | 重要方法 | 方法描述 |
|---|---|---|---|
| ColumnPositionMappingStrategy | 列位置映射策略,用于没有头文件(标题行)的文件 | setColumnMapping(String… columnMapping) | 设置要映射的列名集合,集合下标即为列写入顺序 |
| HeaderColumnNameMappingStrategy | 标题列名称映射策略 | setColumnOrderOnWrite(Comparator writeOrder) | 通过比较器,设置列写入顺序 |
| HeaderColumnNameTranslateMappingStrategy | 标题列名称翻译映射策略,Bean 的属性名可以与 CSV 列头不一样,通过指定 Map 来映射 | setColumnMapping(Map<String, String> columnMapping) | 设置标题名与列名的映射 |
| FuzzyMappingStrategy | 模糊映射策略 | - | - |
① ColumnPositionMappingStrategy
使用该映射策略需要 CSV 文件没有标题行。该策略通过设置列的下标位置来指定列的顺序,有两种方式来设置列的下标:
- 通过
@CsvBindByPosition、@CsvCustomBindByPosition、@CsvBindAndJoinByPosition、@CsvBindAndSplitByPosition注解来设置列的下标。 - 通过
setColumnMapping(String… columnMapping)方法来设置列的下标。
② HeaderColumnNameMappingStrategy
该映射策略用于有标题行的 CSV 文件。该策略通过指定比较器来指定列的顺序:
- 通过
setColumnOrderOnWrite(Comparator writeOrder)指定比较器。
关于标题列的名称:
- 默认使用 Bean 的字段名称大写作为标题列的名称。
- 如果使用
@CsvBindByName、@CsvCustomBindByName、@CsvBindAndJoinByName、@CsvBindAndSplitByName注解的column属性指定列名称,则使用该值;否则使用 Bean 的字段名称大写作为标题列的名称。
③ HeaderColumnNameTranslateMappingStrategy
该映射策略用于有标题行的 CSV 文件。该策略通过映射 Map 来指定标题列名与 Bean 的属性名映射关系。
映射 Map 的 key = 标题列名,value = Bean 的属性名。
需要注意:
- 该映射策略只适用于读取 CSV 文件时,指定标题列名与 Bean 的属性名的映射关系。
- 该映射策略不适用于写入 CSV 文件时,指定 Bean 的属性名与标题列名的映射关系(不要误解)。
过滤器
| 名称 | 描述 |
|---|---|
| CsvToBeanFilter | 读取时根据过滤规则过滤掉一些行 |
主要方法:boolean allowLine(String[] line)
- 入参中的
line表示一行数据的集合。 - 返回值为
false的这行数据将被过滤掉。
构建器
| 名称 | 描述 |
|---|---|
| CSVWriterBuilder | CSV 写入构建器,构建 CSVWriter 或 CSVParserWriter |
| StatefulBeanToCsvBuilder | 对象写入 CSV 构建器,构建 StatefulBeanToCsv |
| CSVReaderBuilder | CSV 读取构建器,构建 CSVReader |
| CsvToBeanBuilder | CSV 读取对象构建器,构建 CsvToBean |
| CSVReaderHeaderAwareBuilder | 构建 CSVReaderHeaderAware |
| CSVParserBuilder | CSV 解析器构造器,构建 CSVParser |
| RFC4180ParserBuilder | RFC4180 解析器构造器,构建 RFC4180Parser |
写入方式
User 类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String userId;
private String userName;
private String sex;
}User1 类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User1 {
@CsvBindByPosition(position = 0)
public String userId;
@CsvBindByPosition(position = 1)
public String userName;
@CsvBindByPosition(position = 2)
public String sex;
}User2 类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User2 {
@CsvBindByName(column = "用户 ID")
public String userId;
@CsvBindByName(column = "用户名")
public String userName;
@CsvBindByName(column = "性别")
public String sex;
}① 简单的写入
CSVWriter 的主要参数:
Writer writer:指定需要写入的源文件。char separator:分隔符(默认逗号)。char quotechar:文本边界符(默认双引号)。如果数据中包含分隔符,需要使用文本边界符包裹数据。通常用双引号、单引号或斜杠作为文本边界符。char escapechar:转义字符(默认双引号)。String lineend:行分隔符(默认为\n)。
使用方法:
/**
* 简单的写入
* @throws Exception
*/
private static void csvWriter() throws Exception {
// 写入位置
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
// 标题行
String[] titleRow = {"用户 ID", "用户名", "性别"};
// 数据行
ArrayList<String[]> dataRows = new ArrayList<>();
String[] dataRow1 = {"1", "张三", "男"};
String[] dataRow2 = {"2", "李四", "男"};
String[] dataRow3 = {"3", "翠花", "女"};
dataRows.add(dataRow1);
dataRows.add(dataRow2);
dataRows.add(dataRow3);
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName), Charset.forName("UTF-8"));
// 1. 通过 new CSVWriter 对象的方式直接创建 CSVWriter 对象
// CSVWriter csvWriter = new CSVWriter(writer);
// 2. 通过 CSVWriterBuilder 构造器构建 CSVWriter 对象
CSVWriter csvWriter = (CSVWriter) new CSVWriterBuilder(writer)
.build();
// 写入标题行
csvWriter.writeNext(titleRow, false);
// 写入数据行
csvWriter.writeAll(dataRows, false);
csvWriter.close();
}输出结果:
用户 ID,用户名,性别
1,张三,男
2,李四,男
3,翠花,女 ② 基于位置映射的写入
使用方法:
/**
* 基于位置映射的写入
* @throws Exception
*/
private static void beanToCsvByPosition() throws Exception {
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
List<User> list = new ArrayList<>();
list.add(new User("1", "张三", "男"));
list.add(new User("2", "李四", "男"));
list.add(new User("3", "翠花", "女"));
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName), Charset.forName("UTF-8"));
ColumnPositionMappingStrategy<User> strategy = new ColumnPositionMappingStrategy();
// 未指定的列不写入
String[] columns = new String[] { "userId", "userName", "sex"};
strategy.setColumnMapping(columns);
strategy.setType(User.class);
// 如果需要标题行,可这样写入
// CSVWriter csvWriter = (CSVWriter) new CSVWriterBuilder(writer)
// .build();
// String[] titleRow = {"用户 ID", "用户名", "性别"};
// csvWriter.writeNext(titleRow, false);
StatefulBeanToCsv<User> statefulBeanToCsv = new StatefulBeanToCsvBuilder<User>(writer)
.withMappingStrategy(strategy)
.withApplyQuotesToAll(false)
.build();
statefulBeanToCsv.write(list);
writer.close();
}输出结果:
1,张三,男
2,李四,男
3,翠花,女③ 基于 CsvBindByPosition 注解映射的写入
使用方法:
/**
* 基于 CsvBindByPosition 注解映射的写入
* @throws Exception
*/
private static void beanToCsvByPositionAnnotation() throws Exception {
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
List<User1> list = new ArrayList<>();
list.add(new User1("1", "张三", "男"));
list.add(new User1("2", "李四", "男"));
list.add(new User1("3", "翠花", "女"));
// 未使用@CsvBindByPosition 注解的列不写入
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName), Charset.forName("UTF-8"));
// 如果需要标题行,可这样写入
// CSVWriter csvWriter = (CSVWriter) new CSVWriterBuilder(writer)
// .build();
// String[] titleRow = {"用户 ID", "用户名", "性别"};
// csvWriter.writeNext(titleRow, false);
StatefulBeanToCsv<User1> statefulBeanToCsv = new StatefulBeanToCsvBuilder<User1>(writer)
.withApplyQuotesToAll(false)
.build();
statefulBeanToCsv.write(list);
writer.close();
}输出结果:
1,张三,男
2,李四,男
3,翠花,女④ 基于列名映射的写入
使用方法:
/**
* 基于列名映射的写入
* @throws Exception
*/
private static void beanToCsvByName() throws Exception {
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
List<User> list = new ArrayList<>();
list.add(new User("1", "张三", "男"));
list.add(new User("2", "李四", "男"));
list.add(new User("3", "翠花", "女"));
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName), Charset.forName("UTF-8"));
// 可通过比较器指定列的顺序
// 标题行的列名默认为 bean 的字段名大写
HeaderColumnNameMappingStrategy<User> strategy = new HeaderColumnNameMappingStrategy<>();
HashMap<String, Integer> columnOrderMap = new HashMap<>();
columnOrderMap.put("USERID", 1);
columnOrderMap.put("SEX", 10);
columnOrderMap.put("USERNAME", 100);
strategy.setColumnOrderOnWrite(Comparator.comparingInt(column -> (columnOrderMap.getOrDefault(column, 0))));
strategy.setType(User.class);
StatefulBeanToCsv<User> statefulBeanToCsv = new StatefulBeanToCsvBuilder<User>(writer)
.withMappingStrategy(strategy)
.withApplyQuotesToAll(false)
.build();
statefulBeanToCsv.write(list);
writer.close();
}输出结果:
USERID,SEX,USERNAME
1,男,张三
2,男,李四
3,女,翠花⑤ 基于 CsvBindByName 注解映射的写入
使用方法:
/**
* 基于 CsvBindByName 注解映射的写入
* @throws Exception
*/
private static void beanToCsvByNameAnnotation() throws Exception {
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
List<User2> list = new ArrayList<>();
list.add(new User2("1", "张三", "男"));
list.add(new User2("2", "李四", "男"));
list.add(new User2("3", "翠花", "女"));
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName), Charset.forName("UTF-8"));
// 可通过比较器指定列的顺序
// 通过 CsvBindByName 注解的 column 属性,指定标题行的列名
HeaderColumnNameMappingStrategy<User2> strategy = new HeaderColumnNameMappingStrategy<>();
// 注意这里的 key 是指的标题行的列名
HashMap<String, Integer> columnOrderMap = new HashMap<>();
columnOrderMap.put("用户 ID", 1);
columnOrderMap.put("用户名", 10);
columnOrderMap.put("性别", 100);
strategy.setColumnOrderOnWrite(Comparator.comparingInt(column -> (columnOrderMap.getOrDefault(column, 0))));
strategy.setType(User2.class);
StatefulBeanToCsv<User2> statefulBeanToCsv = new StatefulBeanToCsvBuilder<User2>(writer)
.withMappingStrategy(strategy)
.withApplyQuotesToAll(false)
.build();
statefulBeanToCsv.write(list);
writer.close();
}输出结果:
用户 ID,用户名,性别
1,张三,男
2,李四,男
3,翠花,女读取方式
以下示例基于简单的写入所生成的数据进行读取。
① 简单的读取
/**
* 简单的读取
* @throws Exception
*/
private static void csvReader() throws Exception {
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8"));
CSVReader csvReader = new CSVReaderBuilder(reader).build();
List<String[]> list = csvReader.readAll();
for (String[] strings : list) {
System.out.println(JSON.toJSONString(strings));
}
csvReader.close();
}控制台日志:
["用户 ID","用户名","性别"]
["1","张三","男"]
["2","李四","男"]
["3","翠花","女"]② 基于位置映射的读取
基于基于位置映射的写入写入的数据。
使用方法:
/**
* 基于位置映射的读取
* @throws Exception
*/
private static void csvToBeanByPosition() throws Exception {
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8"));
// 不需要标题行,列的顺序通过列位置映射指定
ColumnPositionMappingStrategy<User> strategy = new ColumnPositionMappingStrategy();
String[] columns = new String[] { "userId", "userName", "sex"};
strategy.setColumnMapping(columns);
strategy.setType(User.class);
CsvToBean<User> csvToBean = new CsvToBeanBuilder<User>(reader)
.withMappingStrategy(strategy)
.build();
List<User> list = csvToBean.parse();
for (User user : list) {
System.out.println(JSON.toJSONString(user));
}
reader.close();
}控制台日志:
{"sex":"男","userId":"1","userName":"张三"}
{"sex":"男","userId":"2","userName":"李四"}
{"sex":"女","userId":"3","userName":"翠花"}③ 基于 CsvBindByPosition 注解映射的读取
基于基于 CsvBindByPosition 注解映射的写入写入的数据。
使用方法:
/**
* 基于 CsvBindByPosition 注解映射的读取
* @throws Exception
*/
private static void csvToBeanByPositionAnnotation() throws Exception {
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8"));
// 不需要标题行,列的顺序通过 CsvBindByPosition 注解的 position 属性指定
CsvToBean<User1> csvToBean = new CsvToBeanBuilder<User1>(reader)
.withType(User1.class)
.build();
List<User1> list = csvToBean.parse();
for (User1 user : list) {
System.out.println(JSON.toJSONString(user));
}
reader.close();
}控制台日志:
{"sex":"男","userId":"1","userName":"张三"}
{"sex":"男","userId":"2","userName":"李四"}
{"sex":"女","userId":"3","userName":"翠花"}④ 基于列名映射的读取
基于基于列名映射的写入写入的数据。
使用方法:
/**
* 基于列名映射的读取
* @throws Exception
*/
private static void csvToBeanByName() throws Exception {
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8"));
// bean 的字段名称大写为标题列名
CsvToBean<User> csvToBean = new CsvToBeanBuilder<User>(reader)
.withType(User.class)
.build();
List<User> list = csvToBean.parse();
for (User user : list) {
System.out.println(JSON.toJSONString(user));
}
reader.close();
}控制台日志:
{"sex":"男","userId":"1","userName":"张三"}
{"sex":"男","userId":"2","userName":"李四"}
{"sex":"女","userId":"3","userName":"翠花"}⑤ 基于 CsvBindByName 注解映射的读取
基于基于 CsvBindByName 注解映射的写入写入的数据。
使用方法:
private static void csvToBeanByNameAnnotation() throws Exception {
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8"));
// CsvBindByName 注解的 column 属性为标题列名
CsvToBean<User2> csvToBean = new CsvToBeanBuilder<User2>(reader)
.withType(User2.class)
.build();
List<User2> list = csvToBean.parse();
for (User2 user : list) {
System.out.println(JSON.toJSONString(user));
}
reader.close();
}控制台日志:
{"sex":"男","userId":"1","userName":"张三"}
{"sex":"男","userId":"2","userName":"李四"}
{"sex":"女","userId":"3","userName":"翠花"}⑥ 基于列名转换映射的读取
基于基于 CsvBindByName 注解映射的读取写入的数据。
使用方法:
public class MyCsvToBeanFilter implements CsvToBeanFilter {
@Override
public boolean allowLine(String[] line) {
// 过滤掉用户名为李四的行
if("李四".equals(line[1])){
return false;
}
return true;
}
}
/**
* 基于列名转换映射的读取
* @throws Exception
*/
private static void csvToBeanByColumnNameTranslateMapping() throws Exception {
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
String fileName = classpath + "test/demo.csv";
InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8"));
// 指定标题列名和 bean 列名映射关系
HeaderColumnNameTranslateMappingStrategy<User> strategy = new HeaderColumnNameTranslateMappingStrategy<>();
// key:标题列名,value:bean 的属性名
HashMap<String, String> columnMappingMap = new HashMap<>();
columnMappingMap.put("用户 ID", "userId");
columnMappingMap.put("性别", "sex");
columnMappingMap.put("用户名", "userName");
strategy.setColumnMapping(columnMappingMap);
strategy.setType(User.class);
CsvToBean<User> csvToBean = new CsvToBeanBuilder<User>(reader)
.withMappingStrategy(strategy)
.withFilter(new MyCsvToBeanFilter())
.withIgnoreField(User2.class, User2.class.getField("userId"))// 忽略 userId 属性
.build();
List<User> list = csvToBean.parse();
for (User user : list) {
System.out.println(JSON.toJSONString(user));
}
reader.close();
}控制台日志:
{"sex":"男","userName":"张三"}
{"sex":"女","userName":"翠花"}3. commons-csv
本文主要介绍 OpenCSV,commons-csv 也是 Apache 旗下常用的 CSV 处理库,具体用法可参考其官方文档。
4. hutool CsvUtil(扩展)
Hutool 是一个 Java 工具集,其中包含 CsvUtil 类,提供了更简洁的 CSV 读写操作,适合快速开发场景。说明:
- 文中提到的
javacsv库已停止维护,建议在新项目中避免使用。 opencsv示例基于 5.7.0 版本,不同版本间 API 可能存在细微差异,请以官方文档为准。- 代码示例中使用了 Lombok (
@Data等) 和 JSON 工具类,实际使用时请确保引入相应依赖。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/java-zhong-csv-wen-jian-du-xie-fen-xi.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。