ハッキリ言って、百万回実装された猫だと思うけど。
ぼくも所用あって実装した事があるので、せっかくだからGitHubに上げてみた。
■概要:
◇背景
要件としては物凄くありがちな「CSVパーサを実装する必要が出て来た」事による。
ぼく個人としてはCSVと言うファイル形式は色々と面倒事が多いので好きじゃないので、
システム間IFとしてはXMLなりJSONなり、他の構造化文字列形式を採用して欲しいのだが、
残念ながら諸事情あって何が何でもCSVと言う事に。
◇独自実装する前に探してみた
要するに欲しいのはCSVパーサであって、出来ればそんな泥臭いものは自分でガリガリ書きたいモノではない。
という事で、使えそうなライブラリを探してみたのだが、どれもいまいちピンと来ない。
主に以下の点がネックになった。
- CSVのエスケープ実装が弱い。
- 一旦ファイルの全量をメモリに乗っけてしまう。
(恐らく、内部的にScannerを使ってるとか、正規表現を使っているとかで、その辺の事情だと思う)
どうにかして実装済みのライブラリで済ませたい(自分で泥臭いコードは書きたくない)ので、結構頑張って探したんだけど、
要件を全てクリア出来るいいモノは結局見付からなかった。
◇実装要件
- CSVに含まれるデータには改行(LF line-feed : \n)が含まれ、これをきちんと処理する必要がある。
- レコードの区切りはCrLf、データ内改行はLfのみ、と言うのは仕様として確定している。
- CSVファイルのデータ量はそこそこ大きく、全量メモリに展開するのはNG。ストリーム処理が出来る事が必須。
で、まぁテキストファイルを一行ずつ読み込んでって、
普通にインクォート処理しながらCSVのトークンパースしてくだけじゃん。
泥臭いけどまぁ楽勝楽勝。
って思ってたんですが、残念なことに BufferedReader#readLine() が肝心の CrLf と Lf を区別してくれない と言う事を知らず。
トークンパーサはすぐに出来たのに、肝心の行読み込みが上手く行かず、結局そっちを自作する必要がでた、という。
■実装コード:
コアロジック部分のみ抜粋。
関連する他クラスとかは省略してるので、全量はGitHubの方を見て下さい。
(コメントも全部削除してコードだけ乗っけてるので、コメント必要な方もそっち推奨)
public String next()
{
this.sb.setLength( 0 );
if ( this.build() )
{
return this.sb.toString();
}
else
{
return null;
}
}
private boolean build()
{
if ( this.end )
{
return false;
}
else
{
this.readline( false );
return true;
}
}
private class Buffer
{
private final char[] temp;
private int size;
private int index;
private boolean eof;
public Buffer(int size)
{
this.temp = new char[max( size, MIN_SIZE )];
this.size = 0;
this.index = this.temp.length;
this.eof = false;
}
public boolean fill()
{
if ( !eof )
{
size = reader.read( temp );
index = 0;
eof = -1 == size;
}
return !eof;
}
public boolean seekable()
{
return index < size;
}
public char seek()
{
return temp[index++];
}
}
private static final char CR = '\r';
private static final char LF = '\n';
private void readline(boolean cr)
{
while ( this.buffer.seekable() )
{
final char c = this.buffer.seek();
if ( cr )
{
if ( LF == c ) return;
this.sb.append( CR );
}
cr = CR == c;
if ( !cr )
{
this.sb.append( c );
}
}
if ( this.buffer.fill() )
{
this.readline( cr );
}
else
{
if ( cr ) sb.append( CR );
this.end = true;
}
}
■GitHub
repository URL
https://github.com/sugaryo/sharp4j
class FQN
sharp4j.util.io.CrLfReader
余談
C#だったら yield return 使って更に便利なユーティリティに出来るんだけど、
JavaだったらStreamAPIを使う事になるのかな?
ちょっとその辺の勉強がてら、拡張してみたいな。