CSVの解析を機能はSAXのようにメソッドで投げてやろうかとおもったが、
それは結構扱いが悪いことがわかった。
.NET FrameworkにはSAXってないんですね。その代わりにSystem.Xml.XmlReaderってのがあるんですよ。
これは自分で読み進めていくXMLパーサーなんですね。
SAXより扱いがよいよと説明されていて、確かに考えてみるとこれには説得力があると思った。
これは結構便利なんじゃないかと思ってこれをまねてCSVReaderを作った。
というかなんでC#ではCSVを配列の配列に変換するものを作っておきながら、
C++になったらそんな風に読み進めていくやつを作っているのかと言えば、
C++のvectorにvectorを入れると取り扱いが悪いからですね。なので使う方が賢く読んでデータに落としてくれと。
そういうことです。例えば1行分を1つのオブジェクトにして、これの配列を作るとかなら一旦配列の配列に変換するより便利だよねということ。
作ったわけですが、あまりの大作でここに貼ることができないので、別に置いておく。
csvreader.tbz (804Bytes)
804Bytesってえらい少ないな。元々3.24KBもあったんですけどね。恐るべしBZip2。
ここに貼れない理由はあと1つあって、C++だからヘッダファイルとソースファイルをセットで置かないとなと思ったから。
さて、使い方ですが、ストリームを与えてコンストラクタでオブジェクトを作成する。
そして各フィールドをReadFieldメソッドで読み込む。行末についたかはEndOfRowメソッドで確認する。
ただ行末についてもReadFieldメソッドは空白を返し続ける。表計算に消されたけど実際はあるんだよというのもあるかもしれないから。
それで次の行に進むためにGoToEndOfRowメソッドを動かす。行末についていなくても使える。
それで全部読み終わったかどうかはEndOfCSVメソッドで確認できる。
そんなわけで例を2つ書く。まずはあるセルを全部読み込んで表示するプログラム。
#include "CSVReader.h"
#include
#include
#include
void main(){
try{
std::ifstream csvfile("..\\test.csv",std::ios::binary);
if(!csvfile) throw std::exception("File open error!!");
CSVReader mycsv(csvfile);
while(!mycsv.EndOfCSV()){
while(!mycsv.EndOfRow()){
std::cout<<">"<< mycsv.ReadField() <<"< ";
}
mycsv.GoToEndOfRow();
std::cout<< "/" < }
}catch(std::exception &e){
std::cout << "\nException : " << e.what() << std::endl;
}
}
行の最後まで読んで次の行に進むから、mycsv.EndOfRow()がtrueになるまで読み続けて、
終わったらGoToEndOfRow()で次の行にすすむという調子でやっています。
いろいろ置いて試してみるといいですが、一応RFC 4180に沿ったものはうまいこと読めるはずです。
RFC4180では、内容にカンマ・CR・LF・ダブルクオートが入る場合はダブルクオートで囲み、
ダブルクオートは2つ連ねてエスケープするということになっています。
foo”Bar”と書きたければ、”foo””Bar”””のように記載するのがよいとされています。
まぁカンマがあればダブルクオートで囲むというのは浸透しているが、
ダブルクオートがあればダブルクオートで囲むというのはあんまり浸透していない気がする。
そこで、最初がダブルクオートから始まらなければ、ダブルクオートだろうが書いてあるまま解釈するようにしてある。
すなわち、単にfoo”Bar”と書いてあってもfoo”Bar”と解釈してくれる。
けど”Foo”barはダブルクオートから始まってるからうまいこと解釈しないで例外を投げる。
あと改行コードはCR+LFじゃなくてもLFでも失敗しないようにしてある。
そうそう、WindowsがCR+LFをLFに勝手に変換するから、混乱を避けるために開くときバイナリモードにしてる。
Windows以外ではこれが当然なんですけどWindowsだけはこういう問題ありますから。
さて、もう1つ例を挙げる。これは1行目の1つ目に列数が書いてあり、2列目以降に内容があるCSVを考える。
ただ行によっては無駄な列があったり、列が足らなかったりする。
3,,,,,,,,,,,,,,
イ,ロ,ハ
ニ,ホ
へ,ト,チ,リ
ニから始まる行が3列目がないわけですが、実は空白でした。
というわけでこれを解釈するプログラム。
void main(){
try{
std::ifstream csvfile("..\\test.csv",std::ios::binary);
if(!csvfile) throw std::exception("File open error!!");
CSVReader mycsv(csvfile);
int columns=boost::lexical_cast(mycsv.ReadField());
mycsv.GoToEndOfRow();
while(!mycsv.EndOfCSV()){
for(int i=0;i if(i!=0) std::cout << " - ";
std::cout << mycsv.ReadField();
}
mycsv.GoToEndOfRow();
std::cout< }
}catch(std::exception &e){
std::cout << "\nException : " << e.what() << std::endl;
}
}
boost::lexical_castですが、くわしくはletsboost::lexical_castを見てください。
まぁこれがなければcstdlibをincludeしてstd::atoiを使えばいいかなという気はするけど。
解釈すると多い分は読み飛ばして、足りない分は空白で補ってくれます。
OKですね。
ソースコードを見てみると、かなりひどいことになっています。
RFC4180に書かれていることを忠実に実現しようとするとかなり複雑になるんですね。
ただカンマで区切ればいいというものでもないんです。
しかし実際こんなに厳密にCSVを考える必要はそう多くないですけどね。
ただ後で知らなかったとならないように質のいいものを用意しておこうと。そういうことです。
もしかしたらなんか抜けがあるかもしれないけど、これでよさそうですね。
必要に応じて活用してください。
ただできるだけRFC4180に沿ったCSVを投げ込んでくださいね。
多少は外れたのを許容してるけど、だからといって意図したとおり解釈するとは限らないわけだから。
あとは…逆にCSVをうまいこと書き出すやつが欲しいか。
けど書き出す分にはダブルクオートがない限りはなんでもかんでもダブルクオートで囲めばいいからなぁ。
楽なもんです。