CSVファイルの中にカンマ付きの項目があった場合、通常のString.splitメソッドで分割すると、項目の途中で分割されてしまう問題を解決しようという話です。
例えば、こんなCSVのレコードがあった場合
ABC,DEF,GHI,'1,000,000',JKL,MN
このレコードをカンマで分割したい時に、このシングルクォートで囲まれた文字列はカンマで区切らず、1つの文字列として分割したいとします。
そうした時に期待する結果は
ABC
DEF
GHI
'1,000,000'
JKL
MN
ですが、普通にsplitメソッドを使うとこんな感じに分かれてしまいます。
ABC
DEF
GHI
'1
000
000'
JKL
MN
どうしたものかと、正規表現とか色々調べてみましたが、そもそもsplitメソッドは
「条件に一致するときに分割する」
メソッドであって、
「条件に一致するときに分割しない」
ということはできないので(今回の場合だと「引用符で囲まれているカンマ」が条件で、その条件に一致する時のみ分割しない、ということはできない)、諦めて自作メソッドを作りました。
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) throws IOException {
try {
String fileName= "C:\\temp\\test.csv"; // 読み込みたいCSVファイル
// 入力CSVファイルの読み込み
File file= new File(fileName);
FileInputStream input = new FileInputStream(file);
InputStreamReader stream= new InputStreamReader(input,"SJIS");
BufferedReader br = new BufferedReader(stream);
String line = br.readLine();
// 自作メソッド呼び出し
List<String> data = csvSplit(line);
for (String col : data) {
System.out.print(col + "\r\n");
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static List<String> csvSplit(String line) {
char c;
StringBuilder s = new StringBuilder();
List<String> data = new ArrayList<String>();
boolean singleQuoteFlg = false;
for(int i=0; i < line.length(); i++){
c = line.charAt(i);
if (c == ',' && !singleQuoteFlg) {
data.add(s.toString());
s.delete(0,s.length());
} else if (c == ',' && singleQuoteFlg) {
s.append(c);
} else if (c == '\'') {
singleQuoteFlg = !singleQuoteFlg;
s.append(c);
} else {
s.append(c);
}
}
return data;
}
}
以下、実行結果です。
ABC
DEF
GHI
'1,000,000'
JKL
引用符の中のカンマは区切られず、きちんと1つの文字列としてみなされています。
やっていることは、
- CSVのレコードを1行読み込む(String line = br.readLine();)
- 読み込んだレコードから1文字ずつ取り出し、(c = line.charAt(i);)StringBuilderに連結して格納する。(s.append(c);)
- その1文字のパターンによって処理を分ける。
- カンマで、かつカンマがシングルクォートに囲まれていないとき
- カンマで、かつカンマがシングルクォートに囲まれているとき
- シングルクォートのとき
- それ以外
- シングルクォートに囲まれていないカンマは、区切り文字としてみなし、それまでに読み込んだ文字列をリストに格納する。(data.add(s.toString());)
- シングルクォートに囲まれているカンマは、文字列の一部としてみなすため、StringBuilderに連結する。(s.append(c);)
- シングルクォートのときは、自作のシングルクォートフラグのtrue,falseを切り替えます。(singleQuoteFlg = !singleQuoteFlg)シングルクォートフラグは、今シングルクォートの中を読み込んでいるかどうかを判定するフラグです。シングルクォートも出力に含めるならStringBuilderに連結します。(s.append(c);)
- それ以外の場合は、StringBuilderへの連結のみします。(s.append(c);)
ロジックの説明はだいたいこんな感じです。
ちなみに、シングルクォートを出力に含めたくないときは、StringBuilderへの連結をしなければ出力には含まれません。
private static List<String> csvSplit(String line) {
char c;
StringBuilder s = new StringBuilder();
List<String> data = new ArrayList<String>();
boolean singleQuoteFlg = false;
for(int i=0; i < line.length(); i++){
c = line.charAt(i);
if (c == ',' && !singleQuoteFlg) {
data.add(s.toString());
s.delete(0,s.length());
} else if (c == ',' && singleQuoteFlg) {
s.append(c);
} else if (c == '\'') {
singleQuoteFlg = !singleQuoteFlg;
// s.append(c); // 文字連結をやめるとシングルクォートは出力されないよ!
} else {
s.append(c);
}
}
return data;
}
ABC
DEF
GHI
1,000,000
JKL
こんな感じになります。
何かほかにいい方法がありましたら、ご教示いただけますと幸いです!