一、txt、csv 与 tsv 文件

txt、csv 和 tsv 均属于文本文件,它们的区别主要体现在分隔符的使用规范上。

文件类型英文全称名称分隔符描述
txttext文本类型无明确要求可以有分隔符,也可以没有
csvComma-separated values逗号分隔值类型半角逗号:,csv 是 txt 的特殊类型
tsvTab-separated values制表符分隔值制表符:\ttsv 是 txt 的特殊类型
说明:CSV 也被称为 Char-separated values(字符分隔值类型),即通过字符值进行分隔。但由于半角逗号在数据内容中出现的概率较大,因此经常需要使用文本包装符(如双引号)来标识逗号为数据的一部分,或者直接使用其它特殊符号作为分隔符。

二、csv 文件规范

标准的 CSV 文件通常遵循以下规范:

  1. 行记录:每一行记录位于一个单独的行上,通常用回车换行符 CRLF (\r\n) 分割。
  2. 文件结尾:文件中的最后一行记录可以有结尾回车换行符,也可以没有。
  3. 标题头:第一行可以存在一个可选的标题头,格式和普通记录行一致。标题头应包含文件记录字段对应的名称,且数量应与记录字段一致。
  4. 字段分隔:在标题头行和普通行中,存在一个或多个由半角逗号 , 分隔的字段。整个文件中每行应包含相同数量的字段。空格也是字段的一部分,不应被忽略。每一行记录最后一个字段后不能跟逗号。(通常用逗号分隔,也有其他字符分隔的 CSV,需事先约定)。
  5. 文本包装符:每个字段可用也可不用半角双引号 " 括起来(如 Microsoft Excel 默认不用双引号)。如果字段没有用引号括起来,那么该字段内部不能出现双引号字符。
  6. 特殊字符处理:字段中若包含回车换行符、双引号或者逗号,该字段需要用双引号括起来。
  7. 转义规则:如果用双引号括字段,那么出现在字段内的双引号前必须再加一个双引号进行转义。

三、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 读取器
CsvToBeanCSV 读取为 Bean 的读取器
CSVReaderHeaderAware感知文件头的 CSV 读取器

解析器

名称描述
CSVParser简单的 CSV 解析器
RFC4180Parser基于 RFC4180 规范的解析器

注解

注解描述主要属性
@CsvBindByName按表头名称绑定required:必须字段,默认为 false,该字段为空抛异常
column:对象列标题名称
@CsvBindByPosition按位置绑定required:必须字段,默认为 false,该字段为空抛异常
position:位置索引
@CsvCustomBindByName与 CsvBindByName 相同,但必须提供自己的数据转换类required:必须字段,默认为 false
column:对象列标题名称
converter:转换器
@CsvCustomBindByPosition与 CsvBindByPosition 相同,但必须提供自己的数据转换类required:必须字段,默认为 false
column:对象列标题名称
converter:转换器
@CsvBindAndJoinByName应用于 MultiValuedMap 集合类型的 Bean 字段,通过标题名称绑定required:必须字段,默认为 false
column:对象列标题名称
converter:转换器
mapType:集合类型
elementType:元素类型
@CsvBindAndJoinByPosition应用于 MultiValuedMap 集合类型的 Bean 字段,通过位置索引绑定required:必须字段,默认为 false
position:位置索引
converter:转换器
mapType:集合类型
elementType:元素类型
@CsvBindAndSplitByName应用于 Collection 集合类型的 Bean 字段,通过标题名称绑定required:必须字段,默认为 false
column:对象列标题名称
converter:转换器
mapType:集合类型
elementType:元素类型
splitOn:分隔符
@CsvBindAndSplitByPosition应用于 Collection 集合类型的 Bean 字段,通过位置索引绑定required:必须字段,默认为 false
position:位置索引
converter:转换器
mapType:集合类型
elementType:元素类型
splitOn:分隔符
@CsvDate应用于日期/时间类型的 Bean 字段,与上面相关的绑定注解结合使用value:日期格式,例如:yyyy-MM-dd
@CsvNumber应用于数字类型的 Bean 字段,与上面相关的绑定注解结合使用value:数字格式,例如:000.###

映射策略

image.png

名称描述重要方法方法描述
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 的这行数据将被过滤掉。

构建器

名称描述
CSVWriterBuilderCSV 写入构建器,构建 CSVWriter 或 CSVParserWriter
StatefulBeanToCsvBuilder对象写入 CSV 构建器,构建 StatefulBeanToCsv
CSVReaderBuilderCSV 读取构建器,构建 CSVReader
CsvToBeanBuilderCSV 读取对象构建器,构建 CsvToBean
CSVReaderHeaderAwareBuilder构建 CSVReaderHeaderAware
CSVParserBuilderCSV 解析器构造器,构建 CSVParser
RFC4180ParserBuilderRFC4180 解析器构造器,构建 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 读写操作,适合快速开发场景。

说明

  1. 文中提到的 javacsv 库已停止维护,建议在新项目中避免使用。
  2. opencsv 示例基于 5.7.0 版本,不同版本间 API 可能存在细微差异,请以官方文档为准。
  3. 代码示例中使用了 Lombok (@Data 等) 和 JSON 工具类,实际使用时请确保引入相应依赖。