Java中两种写入CSV文件的方法详解

11 次阅读

CSV(Comma-Separated Values,逗号分隔值)是一种结构简单、兼容性强的数据交换格式,常用于报表导出、数据备份、批量导入以及系统之间的数据传输。Java本身并没有专门用于CSV读写的标准API,因此实际开发中通常有两类实现思路:一种是直接使用Java原生IO手动拼接CSV内容,另一种是借助Apache Commons CSV等第三方库完成写入。

两种方式各有适用场景。数据结构简单、依赖要求较低的项目可以采用Java原生方式;如果CSV中包含大量特殊字符、引号、换行或者需要处理复杂格式,则更适合使用专业CSV库。

一、CSV文件的基本格式

一个简单的CSV文件通常按照行保存数据,每一行对应一条记录,不同字段之间使用逗号分隔。

例如:

csv
id,name,age,city
1,张三,25,北京
2,李四,30,上海
3,王五,28,深圳

其中第一行可以作为表头,后续每一行表示一条数据。

不过,CSV并不是简单地把字符串通过逗号连接起来。例如下面的数据:

张三,Java开发工程师

直接拼接没有问题,但如果字段本身包含逗号:

张三,Java,高级开发工程师

解析程序就可能把它识别成三个字段。

标准CSV通常需要使用双引号处理这种情况:

csv
张三,"Java,高级开发工程师"

如果字段中本身包含双引号,还需要进行转义:

csv
张三,"他说""你好"""

因此,CSV写入看似简单,实际开发时需要考虑字段转义、换行、字符编码等问题。

二、方法一:使用Java原生IO写入CSV

对于结构简单的CSV文件,可以直接使用Java的BufferedWriter进行写入。

这种方案不需要额外引入第三方依赖,适合简单的数据导出功能。

1. 基本写入示例

Java
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class CsvWriter {

    public static void main(String[] args) {
        String filePath = "users.csv";

        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {

            writer.write("id,name,age");
            writer.newLine();

            writer.write("1,张三,25");
            writer.newLine();

            writer.write("2,李四,30");
            writer.newLine();

            writer.write("3,王五,28");
            writer.newLine();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

执行后会生成:

csv
id,name,age
1,张三,25
2,李四,30
3,王五,28

try-with-resources会在代码执行结束后自动关闭BufferedWriter,能够避免文件资源没有释放的问题。

2. 使用集合批量写入

实际项目中,CSV数据通常来自数据库或者Java集合,而不是手动写死。

可以定义一个简单的实体类:

Java
public class User {

    private Long id;
    private String name;
    private Integer age;

    public User(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }
}

然后批量生成CSV:

Java
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class CsvExport {

    public static void main(String[] args) {

        List<User> users = Arrays.asList(
                new User(1L, "张三", 25),
                new User(2L, "李四", 30),
                new User(3L, "王五", 28)
        );

        String filePath = "users.csv";

        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {

            writer.write("id,name,age");
            writer.newLine();

            for (User user : users) {
                writer.write(user.getId() + ","
                        + user.getName() + ","
                        + user.getAge());
                writer.newLine();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这种写法比较直观,特别适合字段数量少、数据格式固定的场景。

三、原生方式必须处理CSV特殊字符

直接使用字符串拼接最大的隐患就是没有处理特殊字符。

例如:

Java
writer.write("1,张三,北京,朝阳区");

如果某个字段中存在逗号,就会导致CSV列数量发生变化。

因此,可以封装一个CSV字段转义方法。

Java
public static String escapeCsv(String value) {

    if (value == null) {
        return "";
    }

    boolean needQuote = value.contains(",")
            || value.contains(""")
            || value.contains("
")
            || value.contains("
");

    if (!needQuote) {
        return value;
    }

    value = value.replace(""", """");

    return """ + value + """;
}

使用时:

Java
String name = escapeCsv(user.getName());
String address = escapeCsv("北京市,朝阳区");

writer.write(user.getId() + "," + name + "," + address);
writer.newLine();

这样可以避免字段中的逗号、双引号和换行符破坏CSV结构。

为什么双引号需要转换?

CSV规范中,字段内部的双引号通常需要使用两个连续双引号表示。

例如原始字符串:

他说"你好"

CSV中的字段应该写成:

csv
"他说""你好"""

所以代码:

Java
value.replace(""", """");

是原生CSV实现中非常重要的一步。

四、使用UTF-8解决中文乱码问题

Java旧版本中的:

Java
new FileWriter(filePath)

并不能显式指定字符集。在需要明确使用UTF-8的项目中,更推荐使用OutputStreamWriter。

Java
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;

try (BufferedWriter writer = new BufferedWriter(
        new OutputStreamWriter(
                new FileOutputStream("users.csv"),
                StandardCharsets.UTF_8))) {

    writer.write("id,name,city");
    writer.newLine();

    writer.write("1,张三,北京");
    writer.newLine();

}

这种方式能够明确指定UTF-8编码,对于中文数据尤其重要。

如果CSV主要面向Windows环境中的Excel用户,还可能遇到UTF-8中文显示问题。部分Excel版本对UTF-8 CSV的识别依赖BOM,因此某些业务场景会考虑写入UTF-8 BOM。

不过是否增加BOM,应根据目标软件的兼容性要求决定,并不是所有系统都需要。

五、方法二:使用Apache Commons CSV写入

当CSV数据结构变得复杂以后,手动处理字段转义会增加代码量,也容易遗漏边界情况。

Apache Commons CSV是Java生态中常见的CSV处理库,可以帮助开发者处理字段分隔、引号、换行等问题。

1. 添加Maven依赖

Maven项目可以添加:

XML

    org.apache.commons
    commons-csv
    1.14.1

实际项目中建议根据当前项目依赖管理策略选择合适的稳定版本。

2. 基本写入示例

Java
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;

import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class CommonsCsvWriter {

    public static void main(String[] args) {

        Path path = Path.of("users.csv");

        CSVFormat format = CSVFormat.DEFAULT.builder()
                .setHeader("id", "name", "age")
                .build();

        try (Writer writer = Files.newBufferedWriter(
                path,
                StandardCharsets.UTF_8);
             CSVPrinter printer = new CSVPrinter(writer, format)) {

            printer.printRecord(1, "张三", 25);
            printer.printRecord(2, "李四", 30);
            printer.printRecord(3, "王五", 28);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

生成的内容类似:

csv
id,name,age
1,张三,25
2,李四,30
3,王五,28

相比手动拼接字符串,CSVPrinter会自动处理CSV格式细节。

六、处理包含逗号和换行的字段

例如:

Java
printer.printRecord(
        1,
        "张三",
        "Java开发工程师,后端开发",
        "北京市
朝阳区"
);

CSV库会按照CSV规则处理特殊字符,而不需要开发者自己判断什么时候添加双引号。

最终文件中的字段可能表现为:

csv
1,张三,"Java开发工程师,后端开发","北京市
朝阳区"

对于需要处理用户输入、数据库文本、备注信息、地址等复杂内容的系统,这种能力非常重要。

七、使用List批量写入CSV

如果项目中已经存在集合数据,可以直接循环调用printRecord。

Java
List<User> users = List.of(
        new User(1L, "张三", 25),
        new User(2L, "李四", 30),
        new User(3L, "王五", 28)
);

try (Writer writer = Files.newBufferedWriter(
        Path.of("users.csv"),
        StandardCharsets.UTF_8);
     CSVPrinter printer = CSVPrinter.create(writer, CSVFormat.DEFAULT)) {

    printer.printRecord("id", "name", "age");

    for (User user : users) {
        printer.printRecord(
                user.getId(),
                user.getName(),
                user.getAge()
        );
    }
}

这种代码结构清晰,数据字段与CSV列之间的对应关系也比较容易维护。

八、两种Java写CSV方法的对比

原生IO和Apache Commons CSV都可以完成CSV文件写入,但侧重点不同。

对比项Java原生IOApache Commons CSV
是否需要第三方依赖不需要需要
基本CSV写入支持支持
自动处理特殊字符需要自行实现支持
代码复杂度较低较低
复杂CSV场景需要额外开发更方便
项目依赖更轻量增加一个依赖
可维护性数据复杂后下降较好
适合场景简单导出通用CSV处理

如果只是生成一个简单的三四列CSV文件,原生IO已经足够。

如果项目需要处理复杂字段、不同CSV格式、特殊字符以及较多导出逻辑,使用CSV专用库通常更省心。

九、CSV大数据量写入需要注意什么

当导出的数据达到几十万甚至几百万条时,不建议把所有数据一次性加载到内存中。

例如:

Java
List<User> users = userService.findAll();

如果数据库中有数百万条数据,这种方式可能造成较高的堆内存压力。

更合理的方案是采用分页查询或者流式读取。

例如:

查询第1页数据
    ↓
写入CSV
    ↓
查询第2页数据
    ↓
写入CSV
    ↓
查询第3页数据
    ↓
写入CSV
    ↓
直到数据处理完成

核心思想是让内存中只保留当前批次的数据。

伪代码如下:

Java
int page = 1;
int pageSize = 1000;

while (true) {

    List<User> users = queryUsers(page, pageSize);

    if (users.isEmpty()) {
        break;
    }

    for (User user : users) {
        printer.printRecord(
                user.getId(),
                user.getName(),
                user.getAge()
        );
    }

    page++;
}

这种方式更适合后台数据导出任务。

十、数据库数据直接导出CSV

实际业务中,CSV导出经常来源于数据库。

例如用户表:

user
----------------
id
username
email
age
create_time

可以查询数据后直接写入CSV:

Java
CSVFormat format = CSVFormat.DEFAULT.builder()
        .setHeader("ID", "用户名", "邮箱", "年龄")
        .build();

try (Writer writer = Files.newBufferedWriter(
        Path.of("user.csv"),
        StandardCharsets.UTF_8);
     CSVPrinter printer = new CSVPrinter(writer, format)) {

    List<User> users = userService.findUsers();

    for (User user : users) {
        printer.printRecord(
                user.getId(),
                user.getName(),
                user.getEmail(),
                user.getAge()
        );
    }
}

如果数据量较大,则需要把findUsers()改造成分页查询、游标查询或者其他流式处理方式。

十一、Web项目中直接下载CSV

Spring Boot项目中,CSV通常不是简单地保存到服务器,而是直接返回给浏览器下载。

可以使用HttpServletResponse:

Java
@GetMapping("/export")
public void export(HttpServletResponse response) throws IOException {

    response.setContentType("text/csv");
    response.setCharacterEncoding("UTF-8");
    response.setHeader(
            "Content-Disposition",
            "attachment; filename=users.csv"
    );

    try (Writer writer = response.getWriter();
         CSVPrinter printer = new CSVPrinter(
                 writer,
                 CSVFormat.DEFAULT.builder()
                         .setHeader("id", "name", "age")
                         .build())) {

        printer.printRecord(1, "张三", 25);
        printer.printRecord(2, "李四", 30);
    }
}

这样用户访问导出接口时,浏览器即可触发文件下载。

实际生产环境还需要考虑文件名编码、浏览器兼容性、权限校验、查询条件以及大数据量导出等问题。

十二、CSV导出中的常见问题

1. 中文乱码

首先检查写入时使用的字符集:

Java
StandardCharsets.UTF_8

同时检查打开CSV的软件是否正确识别UTF-8。

2. 数据列错位

重点检查字段中是否存在:

,
"
换行符

如果使用原生字符串拼接,需要进行CSV转义。

如果使用Apache Commons CSV,则尽量交给CSVPrinter处理。

3. 最后一行是否需要换行

通常不需要刻意处理最后一行是否存在换行符。使用专业CSV库时,由库负责记录分隔即可,不建议为了格式强行修改文件内容。

4. null值处理

数据库中的NULL与字符串"null"并不是同一个概念。

导出时应该根据业务需求决定:

NULL → 空字段

还是:

NULL → null

不要直接使用:

Java
String.valueOf(value)

后又误把数据库空值转换成字符串"null"。

5. Excel打开数据异常

Excel对CSV的处理存在一些特殊规则,例如编码识别、日期自动转换、数字格式转换等。

如果CSV主要用于Excel用户打开,需要重点测试:

  • 中文显示;

  • 日期格式;

  • 长数字;

  • 邮箱地址;

  • 身份编号等字符串字段;

  • 前导零;

  • UTF-8编码。

尤其是类似:

001234

这样的数据,某些办公软件可能自动转换为:

1234

如果业务上要求保持原始格式,就需要针对目标软件进行额外处理。

十三、原生IO什么时候更合适

Java原生IO最大的优势是轻量。

如果需求非常简单:

固定表头
+
固定字段
+
简单字符串
+
少量数据

直接使用BufferedWriter即可。

例如日志导出、简单测试数据生成、内部工具等场景,没有必要为了几行CSV代码引入复杂依赖。

但要注意,即使使用原生API,也不要简单地认为:

Java
writer.write(a + "," + b + "," + c);

就等于完整的CSV实现。

一旦字段允许用户输入,就应该考虑CSV转义。

十四、Apache Commons CSV什么时候更合适

如果项目存在以下需求,专业CSV库会更加合适:

  • CSV字段较多;

  • 字段内容来自用户输入;

  • 数据包含逗号;

  • 数据包含双引号;

  • 数据包含换行;

  • 需要批量导出;

  • 需要兼容不同CSV格式;

  • 需要同时实现CSV读取和写入;

  • 项目本身已经使用Apache Commons生态。

此时让成熟库处理CSV格式细节,可以减少大量重复代码,也降低格式错误的概率。

十五、两种方法的选择建议

可以根据项目实际情况进行选择。

简单的内部工具可以采用Java原生IO,代码少、依赖少,而且容易部署。

一般业务系统的CSV导出功能,更建议使用Apache Commons CSV等专用库。尤其当CSV数据来源于数据库并且字段内容不可控时,专业库能够明显降低特殊字符导致的格式问题。

无论选择哪种方式,都应该重点关注三个方面:字符编码、CSV字段转义以及大数据量处理。

对于小数据量文件,代码可读性通常比极限性能更加重要;对于大数据量导出,则应该优先采用流式写入、分页查询等方案,避免一次性将全部数据加载到内存中。

Java写入CSV文件并不复杂,但真正可靠的CSV导出功能并不是简单地使用逗号连接字符串。理解CSV格式规则,再结合数据规模、依赖管理和业务使用场景选择实现方式,才能让导出的文件在数据库、Java程序、Excel以及其他数据处理工具之间保持良好的兼容性。