ライブラリを使用せずCSV読込する汎用ロジック

概要

タイトルそのまま。

単純な制御構文と文字列操作のみで実装することで、 採用ライブラリを制限された環境下でなるべく短く最低限実用性のあるCSVパーサを実装するための実験。

あるいは、VBAのようにそもそも一般的なライブラリのない言語でも使えるロジックを作成する。
(参考実装に使用したのはJava

主に使用条件を限定することでコード量を削減している。

使用条件

  • 区切り文字は半角カンマ、囲み文字は半角ダブルクオート
    (囲み文字は無くてもよい)
  • 半角ダブルクオートで囲まれたカラムには半角カンマおよび改行を含むことができる
    (半角ダブルクオートをカラム内に含める場合、後述の変更が必要)
  • カラム内に含まれた改行の改行コード種類は無視され、出力では常にLFのみになる
  • 区切り文字と囲み文字の間に文字があった場合、データ内に含める

コード

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class CsvCutter {

    /**
     * テスト実行
     */
    public static void main(String[] args) {

        try {
            // 文字コード取得
            Charset cs = (args.length == 1) ? StandardCharsets.UTF_8
                    : Charset.forName(args[1]);

            // 入力表示
            System.out.println("■入力");
            System.out.println("-----------------------------------");
            Files.readAllLines(Paths.get(args[0]), cs)
                    .forEach(System.out::println);
            System.out.println("-----------------------------------\n");

            // 分解実行
            System.out.println("■出力");
            try (BufferedReader br = Files.newBufferedReader(Paths.get(args[0]), cs)) {
                List<String> lnCsv;
                int row = 1;

                // レコード取得
                while ((lnCsv =readCsvLine(br)) != null) {
                    // 結果出力
                    for (int col = 0; col < lnCsv.size(); col++) {
                        System.out.println(row + "行" + (col + 1) + "列 = \""
                                + lnCsv.get(col) + "\"");
                    }
                    row++;
                }
            }
        } catch (Exception e) {
            // エラー
            e.printStackTrace();
        }
    }

    /**
     * CSVファイルから1レコードを取得
     */
    public static List<String> readCsvLine(BufferedReader reader)
        throws IOException {

        List<String> lnCsv = new ArrayList<>();
        StringBuilder buf = new StringBuilder();
        String line;
        boolean inQuote = false;

        // 行ループ
        while ((line = reader.readLine()) != null) {
            // 行内処理
            for (int i = 0; i < line.length(); i++) {
                char c = line.charAt(i);
                if (c == '"') {
                    inQuote = !inQuote;
                } else if (c == ',' && !inQuote) {
                    lnCsv.add(buf.toString());
                    buf = new StringBuilder();
                } else {
                    buf.append(c);
                }
            }

            // 行末判定
            if (inQuote) {
                // 次行へレコード継続
                buf.append("\n");
            } else {
                // 行終了
                lnCsv.add(buf.toString());
                buf = new StringBuilder();
                break;
            }
        }
        if (0 < buf.length()) {
            // イレギュラー:クオート内のままファイル終了
            lnCsv.add(buf.toString());
        }

        return !lnCsv.isEmpty() ? lnCsv : null;
    }

}

実行例

■入力
-----------------------------------
"Name","Age","City"
"John Doe","25","New York"
"Mary Lee","45","Houston, TX"
"Tom Brown","35","San
Francisco"
"Jane Smith", "30",+"Los Angeles"*
-----------------------------------

■出力
1行1列 = "Name"
1行2列 = "Age"
1行3列 = "City"
2行1列 = "John Doe"
2行2列 = "25"
2行3列 = "New York"
3行1列 = "Mary Lee"
3行2列 = "45"
3行3列 = "Houston, TX"
4行1列 = "Tom Brown"
4行2列 = "35"
4行3列 = "San
Francisco"
5行1列 = "Jane Smith"
5行2列 = " 30"
5行3列 = "+Los Angeles*"

補足

  • カラム内にダブルクオートを含めたい場合
    CSVではダブルクオートは二個連続することでエスケープするため、 クオート判定後の処理に先読みを追加する必要がある。
    (対応前の状態では単純に二字無視される)
if (c == '"') {
    inQuote = !inQuote;
} else //〜後略if (c == '"') {
    if (inQuote && i + 1 < line.length() && line.charAt(i + 1) == '"') {
        buf.append(c);
        i++;
    } else {
        inQuote = !inQuote;
    }
} else //〜後略