challenge RFC 4180対応版 CSVレコードの分解

ある関数(splitCSV)に渡された文字列を配列に分解して列ごとに表示してください。
渡される文字列は、CSVデータの1レコードが設定されているとします。

使用するデータはK3形式が元になっている仕様で
エクセルが出力しているような形式です。

書式には次のような特徴があります。
1. 各レコードは「改行」によって区切られている。
2. 各列は「,」によって区切られている。
3. 列のデータは「"」によって囲んでも良い。
4. 列に「,」「改行」「"」いずれかを含む場合「"」で
   囲わなければならない。
5. 列データに「"」を含める場合「""」とする。

本来、改行コードはCRLFですが今回は特に指定しません。

次の入力があった場合
"aaa","b
bb","ccc",zzz,"y""Y""y",xxx

出力は
1 => aaa
2 => b
bb
3 => ccc
4 => zzz
5 => y"Y"y
6 => xxx

となります。
このお題はraynstardさんの投稿によるものです。ご投稿ありがとうございます。助かります。

Posted feedbacks - Java

無理矢理、正規表現でやってみました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.regex.*;
import java.util.*;

public class Sample {
    private static final Pattern spliter = 
        Pattern.compile("((\"[^\"]*+\")+|[^,]*+),?");
    private static final Pattern doubleQuote = Pattern.compile("\"\"");

    public static String[] splitCSV(String rec) {
        Matcher m = spliter.matcher(rec);
        ArrayList<String> cols = new ArrayList<String>();
        while (m.find()) {
            String col = m.group(1);
            if (col != null) {
                if (col.startsWith("\"")) {
                    col = col.substring(1, col.length() - 1);
                }
                col = doubleQuote.matcher(col).replaceAll("\"");
                cols.add(col);
            }
            if (m.end() >= rec.length())
                break;
        }
        return cols.toArray(new String[cols.size()]);
    }

    public static void main(String[] args) throws Exception {
        String sample = "\"aaa\",\"b\nbb\",\"ccc\",zzz,\"y\"\"Y\"\"y\",xxx";
        String[] cols = splitCSV(sample);
        for (int i = 0; i < cols.length; i++) {
            System.out.printf("%d => %s%n", i + 1, cols[i]);
        }
    }
}

Javaらしく(?)Readerクラスで処理してみました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

public class Answer33 {
    public static void main(String[] args) {
        String str = "\"aaa\",\"b\nbb\",\"ccc\",zzz,\"y\"\"Y\"\"y\",xxx";
        CSVDataReader reader = new CSVDataReader(new StringReader(str));
        try {
            while (true) {
                int cell = reader.getCellNumber();
                String s = reader.readCell();
                if (s == null) break;
                
                System.out.println(cell + " => " + s);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class CSVDataReader extends BufferedReader {
    private int cellCount_;
    
    public CSVDataReader(Reader reader) {
        super(reader);
        cellCount_ = 1;
    }

    public int getCellNumber() {
        return cellCount_;
    }

    public String readCell() throws IOException {
        int c = read();
        if (c < 0) return null;
        cellCount_++;

        StringBuilder builder = new StringBuilder();
        boolean quote = (c == '"');
        if (!quote) {
            if (c == '\r' || c == '\n') return "";
            builder.append((char) c);
        }
        OUTER: while ((c = read()) >= 0) {
            if (quote) {
                INNER: switch (c) {
                case '"':
                    int next = read();
                    switch (next) {
                    case '"':
                        builder.append('"');
                        break INNER;
                    case ',':
                        break OUTER;
                    default:
                        throw new IllegalStateException();
                    }
                default:
                    builder.append((char) c);
                }
            } else {
                switch (c) {
                case ',':
                case '\r':
                case '\n':
                    break OUTER;
                case '"':
                    throw new IllegalStateException();
                default:
                    builder.append((char) c);
                }
            }
        }
        return builder.toString();
    }
}

オブジェクト指向らしく(?)STATEパターン的なものを作りました。
CallBackにはレコード/CSVの終了がプッシュされてきますがこのお題では使ってません。
改行コードはReaderが全て\nに変換してくれるので、どのタイプでも大丈夫なはずです。
個人的にはappend(), collectToken(), endOfParse() あたりをもう少しなんとか…とか
各Handler#handle()がどうも似てるような気がしてどうにかならないものかと…。

実行例:
$java CSVParser
1 => aaa
2 => b
bb
3 => ccc
4 => zzz
5 => y"Y"y
6 => xxx
7 => eee,EEE
8 => 
9 => 
10 => 
11 => 
12 => DDD
13 => 
14 => fff
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.StringReader;

public class CSVParser {
    public static void main(String[] args) throws IOException {
        CSVParser parser = new CSVParser(new CallBack() {
            private int num = 0;
            @Override
            public void setToken(String token) {
                System.out.println((++num)+" => "+token);
            }
            @Override
            public void endRecord() {}
            @Override
            public void endCsv() {}
        });

        parser.parse(new StringReader("\"aaa\",\"b\nbb\",\"ccc\",zzz,\"y\"\"Y\"\"y\",xxx,\"eee,EEE\",,,\n,DDD,\"\",fff"));
    }

    public interface CallBack {
        /**
         * CSVのトークンを切り出した後に、格納するためにパーサから呼び出される。
         * @param token CSVトークン
         */
        void setToken(String token);
        /**
         * CSVレコードの最後のトークンの setToken() を呼び出した後にパーサから呼び出される。
         */
        void endRecord();
        /**
         * 最後のCSVレコードのendRecord() を呼び出した後にパーサから呼び出される。
         */
        void endCsv();
    }

    /**
     * CSVParserが処理中に発生した例外を示す実行時例外クラス
     * <p>処理中だったCSVファイルの行番号を格納する。
     */
    public class ParseException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        private int linenumber;
        ParseException(int linenumber) {
            super();
            this.linenumber = linenumber;
        }
        ParseException(String message, int linenumber) {
            super(message);
            this.linenumber = linenumber;
        }
        ParseException(String message, int linenumber, Throwable cause) {
            super(message, cause);
            this.linenumber = linenumber;
        }
        ParseException(int linenumber, Throwable cause) {
            super(cause);
            this.linenumber = linenumber;
        }
        /**
         * CSVファイルの行番号を返す
         * @return 行番号(1~)
         */
        public int getLineNumber() { return linenumber; }
        /**
         * 詳細メッセージを返します。
         * @return 詳細メッセージ(無い場合はnull)
         */
        @Override
        public String getMessage() {
            return super.getMessage()+" [line="+linenumber+"]";
        }
    }

    private LineNumberReader lnreader;
    private CallBack callback;
    private StringBuffer tokenBuffer = new StringBuffer();

    public CSVParser(CallBack callback) {
        if(callback == null) throw new NullPointerException();
        this.callback = callback;
    }

    public void parse(Reader reader) throws IOException {
        lnreader = new LineNumberReader(reader);
        for(Handler h=firstRecordCharHandler; h!=null; h=h.handle(lnreader.read()));
    }

    protected boolean isComment(int c) { return c == '#'; }
    protected boolean isQuot(int c) { return c == '"'; }
    protected boolean isEndOfToken(int c) { return c == ',' || isEndOfRecord(c); }
    protected boolean isEndOfRecord(int c) { return c == '\n' || isEndOfCsv(c); }
    protected boolean isEndOfCsv(int c) { return c == -1; }

    protected Handler append(int c, Handler next) {
        tokenBuffer.append((char)c);
        return next;
    }

    protected Handler collectToken(int c) {
        callback.setToken(tokenBuffer.toString());
        tokenBuffer.setLength(0);
        if(isEndOfRecord(c)) {
            callback.endRecord();
            if(isEndOfCsv(c)) return endOfParse();
            return firstRecordCharHandler;
        }
        return firstTokenCharHandler;
    }

    protected Handler endOfParse() {
        callback.endCsv();
        return null;
    }

    private abstract class Handler {
        abstract Handler handle(int c);
    }

    private final Handler firstRecordCharHandler = new FirstRecordCharHandler();
    private final Handler skipLineHandler = new SkipLineHandler();
    private final Handler firstTokenCharHandler = new FirstTokenCharHandler();
    private final Handler quottingTokenHandler = new QuottingTokenHandler();
    private final Handler quotInQuotCharHandler = new QuotInQuotCharHandler();
    private final Handler unquottingTokenHandler = new UnquottingTokenHandler();

    /**
     * CSVのレコード(行)の最初の文字を処理するハンドラクラス
     * <p>
     * '"'で始まる場合はコメント行として改行までスキップし、
     * それ以外の(有効)文字はCSVトークンの始まりとして処理し、
     * EOFなら解析を終了する。
     *
     * @see CSVParser#isComment(int)
     * @see CSVParser#isEndOfCsv(int)
     * @see CSVParser#endOfParse(int)
     */
    private class FirstRecordCharHandler extends Handler {
        @Override
        public Handler handle(int c) {
            if(isComment(c)) return skipLineHandler;
            if(isEndOfCsv(c)) return endOfParse();
            return firstTokenCharHandler.handle(c);
        }
    }
    /**
     * CSVの一行を読み飛ばすハンドラクラス
     * <p>
     * EOFなら解析を終了し、改行なら firstRecordCharHandler に以降の処理を任せる。
     */
    private class SkipLineHandler extends Handler {
        @Override
        public Handler handle(int c) {
            if(isEndOfCsv(c)) return endOfParse();
            if(isEndOfRecord(c)) return firstRecordCharHandler;
            return this;
        }
    }
    /**
     * CSVトークンの最初の文字を処理するハンドラクラス
     * <p>
     * '"'なら次の'"'までをトークンとして扱い、それ以外の(有効)文字なら
     * トークン終了文字(','、改行、EOF)までをトークンとして扱うようにする。
     * 
     * @see CSVParser#isQuot(int)
     * @see CSVParser.QuottingTokenHandler
     * @see CSVParser.UnquottingTokenHandler
     */
    private class FirstTokenCharHandler extends Handler {
        @Override
        public Handler handle(int c) {
            if(isQuot(c)) return quottingTokenHandler;
            return unquottingTokenHandler.handle(c);
        }
    }
    /**
     * '"'で囲まれたCSVトークンを取り出すハンドラクラス
     * <p>
     * '"'が現れるまで文字格納処理を行い、現れたらエスケープ判定を行うハンドラクラスに委譲する。
     * 
     * @throws ParseException 終了を示す'"'が現れる前にEOFになった場合
     * 
     * @see CSVParser#isQuot(int)
     * @see CSVParser#isEndOfCsv(int)
     * @see CSVParser.QuotInQuotCharHandler
     */
    private class QuottingTokenHandler extends Handler {
        @Override
        public Handler handle(int c) {
            if(isQuot(c)) return quotInQuotCharHandler;
            if(isEndOfCsv(c)) throw new ParseException("quot error.", lnreader.getLineNumber()+1);
            return append(c, this);
        }
    }
    /**
     * '"'で始まったCSVトークン内で'"'が表れた場合のハンドラクラス
     * <p>
     * 続いて'"'が来ればエスケープされた'"'と見なして文字格納処理を行い、
     * トークン終了文字が来ればトークン終了と見なしてトークン格納処理を行う。
     * 
     * @throws ParseException '"'もしくはトークン終了文字以外が表れた場合
     * 
     * @see CSVParser#isQuot(int)
     * @see CSVParser#isEndOfToken(int)
     * @see CSVParser.QuottingTokenHandler
     */
    private class QuotInQuotCharHandler extends Handler {
        @Override
        public Handler handle(int c) {
            if(isEndOfToken(c)) return collectToken(c);
            if(isQuot(c)) return append(c, quottingTokenHandler);
            throw new ParseException("quot error.", lnreader.getLineNumber()+1);
        }
    }
    /**
     * '"'で囲まれていないCSVトークンを取り出すハンドラクラス
     * <p>
     * トークン終了文字が表れるまで文字格納処理を行い、表れたらトークン格納処理を行う。
     * 
     * @throws ParseException '"'が表れた場合
     * 
     * @see CSVParser#isQuot(int)
     * @see CSVParser#isEndOfToken(int)
     * @see CSVParser#collectToken(int)
     */
    private class UnquottingTokenHandler extends Handler {
        @Override
        public Handler handle(int c) {
            if(isEndOfToken(c)) return collectToken(c);
            if(isQuot(c)) throw new ParseException("quot error.", lnreader.getLineNumber()+1);
            return append(c, this);
        }
    }
}

Index

Feed

Other

Link

Pathtraq

loading...