自定义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
相关产品推荐
相关产品推荐

