Posted by lawrhino - 2007.07.19,Thu
MSのCSV形式をプログラムから扱う為の
モジュールを作成する事になった。
使用言語はJAVA。
まず出力されるCSVの仕様の定義から
・全てのデータは,(カンマ)区切り。
・文字列データは"(ダブルクォート)で囲まれている。
・文字列中の"は""としてダブルクォート2回でエスケープする
・文字列データ中には,、"、改行コード(
)が含まれる。
処理の肝となる部分は
・文字列中の改行コードの処理
・文字列中のカンマの処理
の2点だろう。
とりあえず単純にそれら特殊文字を考慮に入れずに
1行分のカンマ区切りデータを読み込む処理を作成。
ファイルの読み込みにはBufferedReaderクラスを使う。
次に文字列データ中に含まれる改行コードを考慮に入れた処理を追加する。
判断方法はまず文字列中にある連続したWクォート(エスケープされているので連続している)
をreplaceAll(""""","")を使って全て消す。
空文字列の場合も連続しているが一緒に消しても問題はない。
すると残りは文字列を囲む前後のWクォートだけとなるので、
列ごとに必ず2個ずつ存在することになる。
途中で改行が含まれてる列があればその列は前の1個だけとなるので、
読み込んだ行全体としてはWクォートの数が奇数になる。
その場合は続けて次の行を読み込む。
これを繰り返し偶数になるところまで読み込む事で1レコード分を
読み込める。
主要部分だけを書くと以下のようなイメージ
残るはカンマの処理だがこれも同様の方法で判定できる。
とりあえず改行判定で取得できた1レコード分の文字列を
splitを使いカンマで分割する。
次に分割されたトークン配列の先頭から順番にチェックしていき、
Wクォートが含まれた場合は文字列型データ列とみなす。
文字列型データ列の場合は、改行判定と同じ方法で
Wクォートの数が偶数かどうかで判定。
奇数の場合は途中にカンマが含まれていたと考えられるので
次のトークンと文字列結合を行う。
(この際、カンマを追加する必要がある)
結合された文字列に対して再度チェックを行い奇数であれば
さらに次のトークンと結合。
以下これを繰り返すことで一つの文字列に復元ができる。
実際のコードは以下のような感じ。
あとはこれらを使いやすいようクラス設計をする事で完成。
ただカンマ分割部分は正規表現を上手く使う事でもっとシンプルにできるだろう。
・Wクォートで囲まれたカンマを除く、カンマ
という正規表現が書けたらおそらく一発で分割できる。
ただ正規表現は不勉強ですぐには思い浮かばなかった為、
今回は力技でやってみた。
わかる人いたら是非ご教授ください。
モジュールを作成する事になった。
使用言語はJAVA。
まず出力されるCSVの仕様の定義から
・全てのデータは,(カンマ)区切り。
・文字列データは"(ダブルクォート)で囲まれている。
・文字列中の"は""としてダブルクォート2回でエスケープする
・文字列データ中には,、"、改行コード(
)が含まれる。
処理の肝となる部分は
・文字列中の改行コードの処理
・文字列中のカンマの処理
の2点だろう。
とりあえず単純にそれら特殊文字を考慮に入れずに
1行分のカンマ区切りデータを読み込む処理を作成。
ファイルの読み込みにはBufferedReaderクラスを使う。
BufferedReader br = new BufferedReader();
String line = br.readLine();
String[] cols = line.split(",");
for (int i = 0; i < cols.length; i++)
cols[i] = cols[i].replaceFirst("^"","").replaceFirst(""$","");
次に文字列データ中に含まれる改行コードを考慮に入れた処理を追加する。
判断方法はまず文字列中にある連続したWクォート(エスケープされているので連続している)
をreplaceAll(""""","")を使って全て消す。
空文字列の場合も連続しているが一緒に消しても問題はない。
すると残りは文字列を囲む前後のWクォートだけとなるので、
列ごとに必ず2個ずつ存在することになる。
途中で改行が含まれてる列があればその列は前の1個だけとなるので、
読み込んだ行全体としてはWクォートの数が奇数になる。
その場合は続けて次の行を読み込む。
これを繰り返し偶数になるところまで読み込む事で1レコード分を
読み込める。
主要部分だけを書くと以下のようなイメージ
String line = br.readLine();
while(countChar(replaceAll("""""",""),'"',0) % 2 == 1){
line += "
" + br.readLine();
}
// 文字列中の特定文字をカウントする関数
private int countChar(String str, char c, int start) {
int i = 0, index = str.indexOf(c, start);
return index == -1 ? i : countChar(str, c, index + 1) + 1;
}
残るはカンマの処理だがこれも同様の方法で判定できる。
とりあえず改行判定で取得できた1レコード分の文字列を
splitを使いカンマで分割する。
次に分割されたトークン配列の先頭から順番にチェックしていき、
Wクォートが含まれた場合は文字列型データ列とみなす。
文字列型データ列の場合は、改行判定と同じ方法で
Wクォートの数が偶数かどうかで判定。
奇数の場合は途中にカンマが含まれていたと考えられるので
次のトークンと文字列結合を行う。
(この際、カンマを追加する必要がある)
結合された文字列に対して再度チェックを行い奇数であれば
さらに次のトークンと結合。
以下これを繰り返すことで一つの文字列に復元ができる。
実際のコードは以下のような感じ。
private String[] csvSplits(String line, String delimiter) {
LinkedList results = new LinkedList();
String[] tokens = line.split(delimiter, -1);
for (int i = 0; i < tokens.length; i++) {
if (results.isEmpty() || isCorrectElement((String)results.getLast())) {
results.add(tokens[i]);
} else {
results.add( tokens.removeLast() + delimiter + tokens[i] );
}
}
return (String[])results.toArray(new String[0]);
}
private boolean isCorrectElement(String str) {
return 1 == countChar(str, '"', 0) % 2 ;
}
あとはこれらを使いやすいようクラス設計をする事で完成。
ただカンマ分割部分は正規表現を上手く使う事でもっとシンプルにできるだろう。
・Wクォートで囲まれたカンマを除く、カンマ
という正規表現が書けたらおそらく一発で分割できる。
ただ正規表現は不勉強ですぐには思い浮かばなかった為、
今回は力技でやってみた。
わかる人いたら是非ご教授ください。
PR
Comments
Post a Comment
カレンダー
プロフィール
HN:
lawrhino
性別:
非公開
最新記事
忍者アド
最新TB
アクセス解析
Template by mavericyard*
Powered by "Samurai Factory"
Powered by "Samurai Factory"