首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 高性能 CSV 解析:支持引號嵌套、逗號轉義、換行符的標準合規實現

     編輯:Java 相關知識

CSV 格式痛點

RFC 4180 標準規定:字段包含逗號、雙引號、換行符時必須用雙引號包裹;雙引號本身用雙雙引號 `""` 轉義。簡單 `split(",")` 完全無法處理這些情況。

有限狀態機 (FSM) 解析器

單次遍歷、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]
        */
    }
}

流式處理大文件 (避免 OOM)

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、高吞吐解析

生產環境檢查清單

  1. 編碼統一:強制 UTF-8 (或檢測 BOM/編碼),避免中文亂碼
  2. 異常行處理:字段數不匹配時記錄日誌、跳過或進入死信隊列
  3. 類型轉換:字符串 → LocalDate/BigDecimal/Enum 統一轉換器
  4. 內存控制:大文件必須流式處理,禁用 `readAllLines`
  5. 安全性:CSV 注入防護 (字段以 `=`, `+`, `-`, `@` 開頭前加 `'` 或 `\t`)
  6. 並發安全:解析器無狀態,可安全共享;`RowConsumer` 需線程安全

CSV 解析是「簡單格式、複雜邊界」的典型代表。手寫 FSM 解析器能深度理解狀態機思想,也是面試高頻考點。生產項目建議直接用 OpenCSV 或 Jackson CSV,專注業務邏輯而非造輪子;但在無法引入依賴的受限環境,本文實現足以應對 99% 標準 CSV 場景。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved