RFC 4180 標準規定:字段包含逗號、雙引號、換行符時必須用雙引號包裹;雙引號本身用雙雙引號 `""` 轉義。簡單 `split(",")` 完全無法處理這些情況。
單次遍歷、O(n) 時間、O(1) 額外空間 (不計輸出),正確處理所有邊界情況:
import java.util.*;
public class CsvParser {
/** 解析單行 CSV (不跨行),返回字段列表 */
public static List parseLine(String line) {
List fields = new ArrayList<>();
if (line == null || line.isEmpty()) return fields;
char[] chars = line.toCharArray();
StringBuilder field = new StringBuilder();
boolean inQuotes = false;
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (inQuotes) {
if (c == '"') {
// 雙引號轉義: "" -> "
if (i + 1 < chars.length && chars[i + 1] == '"') {
field.append('"');
i++; // 跳過下一個引號
} else {
inQuotes = false; // 結束引號區塊
}
} else {
field.append(c); // 引號內原樣保留 (含逗號、換行)
}
} else {
if (c == '"') {
inQuotes = true; // 開始引號區塊
} else if (c == ',') {
fields.add(field.toString());
field.setLength(0);
} else {
field.append(c);
}
}
}
fields.add(field.toString()); // 最後一個字段
return fields;
}
/** 解析多行 CSV (支持字段內換行) */
public static List> parse(Reader reader) throws IOException {
List> rows = new ArrayList<>();
StringBuilder lineBuffer = new StringBuilder();
boolean inQuotes = false;
int c;
while ((c = reader.read()) != -1) {
char ch = (char) c;
if (inQuotes) {
lineBuffer.append(ch);
if (ch == '"') {
// 檢查是否為轉義雙引號
reader.mark(1);
int next = reader.read();
if (next == '"') {
lineBuffer.append('"'); // 保留第二個引號
} else {
reader.reset(); // 非轉義,回退
inQuotes = false;
}
}
} else {
if (ch == '"') {
inQuotes = true;
lineBuffer.append(ch);
} else if (ch == '\n') {
// 行結束
rows.add(parseLine(lineBuffer.toString()));
lineBuffer.setLength(0);
} else if (ch != '\r') { // 忽略 \r (Windows 換行符 \r\n)
lineBuffer.append(ch);
}
}
}
// 處理最後一行 (無換行結尾)
if (lineBuffer.length() > 0) {
rows.add(parseLine(lineBuffer.toString()));
}
return rows;
}
/** 便捷方法:解析文件 */
public static List> parseFile(File file) throws IOException {
try (FileReader fr = new FileReader(file, StandardCharsets.UTF_8)) {
return parse(fr);
}
}
/** 便捷方法:解析字符串 */
public static List> parseString(String csv) {
try (StringReader sr = new StringReader(csv)) {
return parse(sr);
} catch (IOException impossible) {
throw new UncheckedIOException(impossible);
}
}
}
public class CsvParserTest {
public static void main(String[] args) {
String csv = """
id,name,description,price
1,"普通商品",無特殊字符,10.5
2,"含逗號,商品","描述,有逗號",20.0
3,"含換行
商品","多行
描述",30.0
4,"含""雙引號""商品","說明: ""引用""",40.0
5,"空字段測試",,50.0
6,,,60.0
""";
List> rows = CsvParser.parseString(csv);
for (int i = 0; i < rows.size(); i++) {
System.out.printf("Row %d: %s%n", i, rows.get(i));
}
/* 預期輸出:
Row 0: [id, name, description, price]
Row 1: [1, 普通商品, 無特殊字符, 10.5]
Row 2: [2, 含逗號,商品, 描述,有逗號, 20.0]
Row 3: [3, 含換行\n商品, 多行\n描述, 30.0]
Row 4: [4, 含"雙引號"商品, 說明: "引用", 40.0]
Row 5: [5, 空字段測試, , 50.0]
Row 6: [6, , , 60.0]
*/
}
}
public class StreamingCsvProcessor {
public static void processLargeCsv(File file, RowConsumer consumer) throws IOException {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
String headerLine = br.readLine();
if (headerLine == null) return;
List headers = CsvParser.parseLine(headerLine);
StringBuilder lineBuffer = new StringBuilder();
boolean inQuotes = false;
String line;
while ((line = br.readLine()) != null) {
lineBuffer.append(line).append('\n');
// 簡單引號計數判斷是否跨行 (更精確需字符級掃描)
long quoteCount = lineBuffer.chars().filter(ch -> ch == '"').count();
if (quoteCount % 2 == 0) { // 引號配對,行結束
List fields = CsvParser.parseLine(lineBuffer.toString());
if (fields.size() == headers.size()) {
Map rowMap = new LinkedHashMap<>();
for (int i = 0; i < headers.size(); i++) {
rowMap.put(headers.get(i), fields.get(i));
}
consumer.accept(rowMap);
}
lineBuffer.setLength(0);
}
}
}
}
@FunctionalInterface
public interface RowConsumer {
void accept(Map row);
}
}
// 使用示例
StreamingCsvProcessor.processLargeCsv(new File("big.csv"), row -> {
// 處理單行,如入庫、轉換、過濾
System.out.println(row.get("name"));
});
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 手寫 FSM (本文) | 零依賴、完全可控、極輕量、教學價值高 | 需自行維護、擴展功能弱 (類型轉換、Schema 驗證) | 嵌入式、Android、極簡依賴項目、學習原理 |
| OpenCSV | 成熟、注解映射、流式讀寫、Spring 整合好 | 較重 (200KB+)、性能中等 | 企業級應用、標準 Java 項目 |
| Jackson CSV | 統一 ObjectMapper、樹模型、高性能、模塊化 | 學習曲線陡、配置複雜 | 已用 Jackson JSON 的項目、高性能需求 |
| Apache Commons CSV | 輕量、標準 API、RFC 4180 合規 | 功能基礎、無 Bean 映射 | 簡單解析、中間件集成 |
| uniVocity-parsers | 極快、功能最全 (固定寬度、TSV、自動檢測) | 商業友好許可證 (Apache 2.0 但有條款) | 大數據 ETL、高吞吐解析 |
CSV 解析是「簡單格式、複雜邊界」的典型代表。手寫 FSM 解析器能深度理解狀態機思想,也是面試高頻考點。生產項目建議直接用 OpenCSV 或 Jackson CSV,專注業務邏輯而非造輪子;但在無法引入依賴的受限環境,本文實現足以應對 99% 標準 CSV 場景。