You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义RFC4180兼容CSV文件读取功能实现咨询

完善兼容RFC4180规范的Java CSV读取方法

我来帮你搞定这个自定义CSV读取的需求——这类场景在实际开发中太常见了:有时候遇到的CSV不是标准逗号分隔,用了竖线或者其他符号,还有的字段带双引号包围,甚至字段里还嵌着换行符,直接用split方法肯定会翻车。下面是完整的实现,严格兼容RFC4180规范,同时支持你需要的自定义分隔符、行结束符和包围符:

import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
import java.util.ArrayList;
import java.util.List;

public class CustomCsvReader {

    public List<List<String>> readFileAsListOfList(File file, String delimiter, String lineEnding, String enclosure) throws Exception {
        // 基础校验:文件不存在或不是文件的情况
        if (!file.exists()) {
            throw new IllegalArgumentException("File doesn't exist.");
        }
        if (!file.isFile()) {
            throw new IllegalArgumentException("Provided path is not a file.");
        }
        // 处理默认值:遵循RFC4180规范,默认分隔符为逗号
        if (delimiter == null || delimiter.isEmpty()) {
            delimiter = ",";
        }
        // 包围符默认用双引号,为空则表示不启用包围符解析
        boolean useEnclosure = enclosure != null && !enclosure.isEmpty();
        String enclosureChar = useEnclosure ? enclosure : "";
        // 行结束符默认用系统换行,为空则 fallback 到"\n"
        if (lineEnding == null || lineEnding.isEmpty()) {
            lineEnding = System.lineSeparator();
        }

        List<List<String>> result = new ArrayList<>();
        StringBuilder currentField = new StringBuilder();
        boolean inEnclosure = false;
        boolean escapedEnclosure = false;

        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            StringBuilder currentLine = new StringBuilder();
            String line;
            // 逐行读取,但要处理字段内的换行(RFC4180允许字段包含换行)
            while ((line = reader.readLine()) != null) {
                currentLine.append(line);
                // 检查当前行是否处于未闭合的包围状态
                int enclosureCount = countOccurrences(currentLine.toString(), enclosureChar);
                // 如果包围符出现奇数次,说明字段还没结束,继续读取下一行合并
                if (useEnclosure && enclosureCount % 2 != 0) {
                    currentLine.append(lineEnding); // 保留字段内的换行符
                    continue;
                }
                // 解析当前完整的行(或多行合并的字段)
                char[] chars = currentLine.toString().toCharArray();
                for (int i = 0; i < chars.length; i++) {
                    char c = chars[i];
                    // 处理包围符转义:连续两个包围符表示一个普通的包围符字符
                    if (useEnclosure && c == enclosureChar.charAt(0)) {
                        if (escapedEnclosure) {
                            currentField.append(c);
                            escapedEnclosure = false;
                        } else {
                            // 检查下一个字符是否也是包围符
                            if (i + 1 < chars.length && chars[i + 1] == enclosureChar.charAt(0)) {
                                escapedEnclosure = true;
                            } else {
                                inEnclosure = !inEnclosure;
                            }
                        }
                    } else if (!inEnclosure && String.valueOf(c).equals(delimiter)) {
                        // 不在包围状态时,遇到分隔符就结束当前字段
                        result.computeIfAbsent(result.size(), k -> new ArrayList<>()).add(currentField.toString().trim());
                        currentField.setLength(0);
                    } else {
                        // 普通字符,追加到当前字段
                        currentField.append(c);
                    }
                }
                // 处理当前行的最后一个字段
                result.computeIfAbsent(result.size(), k -> new ArrayList<>()).add(currentField.toString().trim());
                currentField.setLength(0);
                currentLine.setLength(0);
                inEnclosure = false;
                escapedEnclosure = false;
            }
        } catch (Exception e) {
            throw new Exception("Failed to read or parse CSV file: " + e.getMessage(), e);
        }

        return result;
    }

    // 辅助方法:统计字符串中指定子串出现的次数
    private int countOccurrences(String str, String target) {
        if (str == null || target == null || target.isEmpty()) {
            return 0;
        }
        int count = 0;
        int index = 0;
        while ((index = str.indexOf(target, index)) != -1) {
            count++;
            index += target.length();
        }
        return count;
    }
}

关键逻辑拆解

  • 默认值适配:如果未传入分隔符,自动使用RFC4180规定的逗号;包围符为空则跳过包围解析逻辑,行结束符默认用系统自带换行。
  • 多行字段处理:通过统计当前行内包围符的出现次数,判断字段是否未闭合——如果是奇数,就继续读取下一行并合并,保留字段内的换行符。
  • 包围符转义:遇到连续两个包围符时,会解析为一个普通的包围符字符,而不是字段的开始/结束标记,符合RFC4180的转义规则。
  • 分隔符判断:只有当不在包围状态时,分隔符才会被视为字段分隔标记,避免误解析字段内部的分隔符。
  • 资源安全:使用try-with-resources自动关闭流,避免资源泄漏。

使用示例

public static void main(String[] args) {
    try {
        CustomCsvReader reader = new CustomCsvReader();
        File csvFile = new File("test.csv");
        // 读取用|分隔、双引号包围、行结束符为\n的CSV
        List<List<String>> data = reader.readFileAsListOfList(csvFile, "|", "\n", "\"");
        // 输出解析结果
        for (List<String> row : data) {
            System.out.println("Row: " + row);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

比如测试用的test.csv内容:

Name|Age|Description
"Alice Smith"|30|"Likes hiking\nand reading"
Bob|25|No quotes here
"Charlie""s Dog"|5|"Brown and white"

解析后会正确识别包含换行的Description字段,以及转义后的双引号(Charlie"s Dog)。

内容的提问来源于stack exchange,提问作者Pascal Petruch

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 03:57:47