さぁCSVの時間だ
言ってみただけ。
本題
JAVAでCSVパースをしたくなったのでやるざんす。
やりたいこととしては以下。
- 一行がString配列のリストにしたい
- ヘッダー情報とか意識しないでBeanにもしたい
- ファイル内にヘッダー行があっても無視できたりしたい
- てかぶっちゃけ、呼ぶメソッド変えるだけでどっちにもいい感じになって欲しい
こんなとこですかね。
何故Stringリストでなく配列かというと、
『Listは縦に積む、配列は横に連なる』イメージだからです!(`^´) ドヤッ!
はい、好みの問題です。
パフォーマンスとかは今回は対象外にしてます。
『数千件程度でそんな差が出るようなものは世に出てないでしょう!』という世間を信頼しての観点です。
ということで早速幾つかを試してみたいと思いますー。
JAVAでCSVを扱うライブラリは結構ありますが、今回はその中で以下3つを試してみたザンス。
- opencsv http://opencsv.sourceforge.net/
- Commons CSV http://commons.apache.org/proper/commons-csv/index.html
- SuperCSV Annotation http://mygreen.github.io/super-csv-annotation/
※『CommonsCSV』という名前のApacheCommonsじゃない、別(?)ライブラリがいるのでご注意
おかげで調べるのにすごく時間がかかった・・・OTZ
http://grepcode.com/project/repo1.maven.org/maven2/org.marketcetera.fork/commons-csv/
ということでやってみたよ。
準備
CSVファイルの準備がいりますね。
CSVは下のWebツールを使わせてもらいました。
- なんちゃって個人情報 http://kazina.com/dummy/
まー便利。
何がいいって今回の検証に必要なString,int,Date型が全て入ってるとこね。
作ってくださった方、ありがとうございます。届かないでしょうが。
検証開始
検証用クラス/データ
- CSV
名前,ふりがな,アドレス,性別,年齢,誕生日
茂木 康文,もぎ やすふみ,mogi_yasufumi@example.com,男,32,1983/4/25
立川 弘也,たちかわ ひろなり,tachikawa_hironari@example.com,男,69,1946/5/12
宮田 真一,みやた しんいち,miyata_shinichi@example.com,男,79,1935/9/13
浅田 俊二,あさだ しゅんじ,asada_shunji@example.com,男,53,1961/10/29
- JAVABean
public class TeamMenber {
// 名前,ふりがな,アドレス,性別,誕生日
private String name, furigana, address, sex, birthday;
// 年齢
private Integer old;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFurigana() {
return furigana;
}
public void setFurigana(String furigana) {
this.furigana = furigana;
}
public String getAddress() {
return address;
}
public void setAddress(String adress) {
this.address = adress;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Integer getOld() {
return old;
}
public void setOld(Integer old) {
this.old = old;
}
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
}
opencsv
private static final String[] HEADER = new String[] { "name", "furigana", "address", "sex", "old", "birthday" };
public static List<String[]> opencsvToStringArray(File file) {
try {
CSVReader reader = new CSVReader(new InputStreamReader(new FileInputStream(file), "SJIS"));
return reader.readAll();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static List<TeamMenber> opencsvToBean(File file) {
try {
CSVReader reader = new CSVReader(new InputStreamReader(new FileInputStream(file), "SJIS"), ',', '"', 1);
ColumnPositionMappingStrategy<TeamMenber> strat = new ColumnPositionMappingStrategy<TeamMenber>();
strat.setType(TeamMenber.class);
strat.setColumnMapping(HEADER);
CsvToBean<TeamMenber> csv = new CsvToBean<TeamMenber>();
return csv.parse(strat, reader);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
んで、結果は以下のように問題なく取れました、と。
############# TO STRING ARRAY #############
名前 | ふりがな | アドレス | 性別 | 年齢 | 誕生日 |
茂木 康文 | もぎ やすふみ | mogi_yasufumi@example.com | 男 | 32 | 1983/4/25 |
立川 弘也 | たちかわ ひろなり | tachikawa_hironari@example.com | 男 | 69 | 1946/5/12 |
宮田 真一 | みやた しんいち | miyata_shinichi@example.com | 男 | 79 | 1935/9/13 |
浅田 俊二 | あさだ しゅんじ | asada_shunji@example.com | 男 | 53 | 1961/10/29 |
############# TO JAVA BEAN #############
茂木 康文 | もぎ やすふみ | mogi_yasufumi@example.com | 男 | 32 | 1983/4/25 |
立川 弘也 | たちかわ ひろなり | tachikawa_hironari@example.com | 男 | 69 | 1946/5/12 |
宮田 真一 | みやた しんいち | miyata_shinichi@example.com | 男 | 79 | 1935/9/13 |
浅田 俊二 | あさだ しゅんじ | asada_shunji@example.com | 男 | 53 | 1961/10/29 |
では、やりたいことに対してどうだったかというと
一行がString配列のリストにしたい
楽勝。『readAll()』を『readNext()』とすれば一行ずつ読ませることも可能。文句なし!理想的だよ!
ヘッダー情報とか意識しないでBeanにしたい
ヘッダー情報を渡さなきゃいけないところがちょっとめんどくさい。
Class.class.getFields[]
とか使えば少し簡単になりそうですが、フィールド定義に縛りが出てしまいますし。。。
まーヘッダー行があれば、そのヘッダー行とBeanのフィールド名同士を結びつけるマップを定義すれば可能ですが、 (※)
やっぱりちとめんどくさい。
※その方法は長くなるので、その方法はこちらをご覧ください。m(__)m
http://tomoyamkung.net/2014/01/30/java-opencsv-headercolumnnametranslatemappingstrategy/
ファイル内にヘッダー行があっても無視できたりしたい
これは可能。
CSVReaderのコンストラクタが『ToStringArray』と『ToBean』で異なるのは、ヘッダー行を無視するようにしているためです。
てかぶっちゃけ、呼ぶメソッド変えるだけでどっちにもいい感じになって欲しい
そうはいきませんでしたが、変換自体はちょっと手間をかけるかどうかの差くらいですかね。
配列から"何番目"を指定して取るのと、ヘッダー定義してからBeanで扱うのではあまり差はなさそうな気もします。
String配列は完璧でしたし、Beanにもできる。使いやすくていいんじゃないでしょうか。
Commons CSV
private static final String[] HEADER = new String[] { "name", "furigana", "address", "sex", "old", "birthday" };
public static List<String[]> commoncsvToStringArray(File file) {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "SJIS"));
CSVParser parse = CSVFormat.DEFAULT.parse(br);
List<String[]> lines = new ArrayList<String[]>();
for (CSVRecord line : parse) {
String[] array = new String[(int) line.size()];
int i = 0;
for (Iterator<String> iterator = line.iterator(); iterator.hasNext(); i++) {
array[i] = iterator.next();
}
lines.add(array);
}
return lines;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static List<TeamMenber> commoncsvToBean(File file) {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "SJIS"));
CSVFormat csvFormat = CSVFormat.DEFAULT.withHeader(HEADER).withSkipHeaderRecord();
CSVParser parse = new CSVParser(br, csvFormat);
List<TeamMenber> menbers = new ArrayList<TeamMenber>();
for (CSVRecord line : parse) {
TeamMenber menber = new TeamMenber();
menber.setAddress(line.get("address"));
menber.setName(line.get("name"));
menber.setFurigana(line.get("furigana"));
menber.setSex(line.get("sex"));
menber.setOld(Integer.decode(line.get("old")));
menber.setBirthday(line.get("birthday"));
menbers.add(menber);
}
return menbers;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
結果はもちろん同じなので省略。
で、やりたいことに対してどうだったかというと・・・
一行がString配列のリストにしたい
『commoncsvToStringArray』の通り、無理やりです。。
一発では無理でした。(CSVRecord
の中で持ってるくせに・・・)
ヘッダー情報とか意識しないでBeanにもしたい
コチラも同様「できないこともない」といった具合。
ヘッダー定義はopencsv同様に必要。
そして返ってきたと思ったら、Beanではなく独自の型(CSVRecord)っていうね。
CSVRecord
の使い方はMap<ヘッダーのプロパティ名, 値>と同じみたい、、、ならせめてMapで返してよ。。。
まだ使いこなせてない気もしますので、断言はできませんが
ファイル内にヘッダー情報があるならそれをキーにCSVFormat
を定義させることもできそう。
なので上記コードより若干少なくなるかもですが、それにしたって、、というところ。
ファイル内にヘッダー行があっても無視できたりしたい
これはできました。withSkipHeaderRecord();
がこれ。
てかぶっちゃけ、呼ぶメソッド変えるだけでどっちにもいい感じになって欲しい
どっちでもないMapでのみ返ってくる感じ。
「まあMapなら、まあまあ、ね。」といいたいのですが、CSVRecord
なのが個人的には好きくない。
あまり利点も感じられませんし。
すごく簡単、て訳でもないので、今のところopencsvに軍配かしら。
SuperCSV-Annotation
ちょこっとJAVABeanを変更します。
ポイントは『birthday』の型ですね。あとはアノテーションがついたこと!
@CsvBean(header=true)
public class TeamMenber2 {
// 名前,ふりがな,アドレス,性別
@CsvColumn(position = 0)
private String name;
@CsvColumn(position = 1)
private String furigana;
@CsvColumn(position = 2)
private String address;
@CsvColumn(position = 3)
private String sex;
// 年齢
@CsvColumn(position = 4)
private Integer old;
// 誕生日
@CsvColumn(position = 5)
@CsvDateConverter(pattern="yyyy/MM/dd")
private Date birthday;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFurigana() {
return furigana;
}
public void setFurigana(String furigana) {
this.furigana = furigana;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Integer getOld() {
return old;
}
public void setOld(Integer old) {
this.old = old;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
}
で、下が検証用の実装
public static List<String[]> supercsvannotationToStringArray(File file) {
try {
ICsvListReader csvReader = new CsvListReader(new InputStreamReader(new FileInputStream(file), "SJIS"),
CsvPreference.STANDARD_PREFERENCE);
List<String[]> lines = new ArrayList<String[]>();
List<String> line = null;
while ((line = csvReader.read()) != null) {
String[] array = new String[(int) line.size()];
int i = 0;
for (Iterator<String> iterator = line.iterator(); iterator.hasNext(); i++) {
array[i] = iterator.next();
}
lines.add(array);
}
return lines;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static List<TeamMenber2> supercsvannotationToBean(File file) {
try {
CsvAnnotationBeanReader<TeamMenber2> csvReader = new CsvAnnotationBeanReader<TeamMenber2>(
TeamMenber2.class, new InputStreamReader(new FileInputStream(file), "SJIS"),
CsvPreference.STANDARD_PREFERENCE);
List<TeamMenber2> list = new ArrayList<TeamMenber2>();
TeamMenber2 menber = null;
csvReader.getHeader(false);
while ((menber = csvReader.read()) != null) {
list.add(menber);
}
return list;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
結果はもちろん同じなので省略。
で、やりたいことに対してどうだったかですが、
一行がString配列のリストにしたい
リストで返ってきちゃうんですねー。
悪くは無いけどー。。。は、"配列"が好きです・・・( *´ェ`*)ポッ
なので配列に無理やりしました。
リストでいいという方は問題ないかと思います。
ただ他と違うのは一行ずつでしか読み込めない。
ループさせればいいだけなのですが、違いとして触れておきました。
ヘッダー情報とか意識しないでBeanにもしたい
このライブラリの強みはここですね!
アノテーションで順番を指定でき、ヘッダー情報とかアウトオブ眼中(死語)ですよ!
しかも、Date型対応!
触れなかったのですが、実は他の2つのライブラリはDate型などに対応しておりません。
そのためライブラリに手を加えるなり、ちょっといろいろとめんどくさいことをしなくてはいけないのです。
ですがこのライブラリは、アノテーションで指定することでDate型への変換が可能なのです。
更にValidationとかデフォルト値とか、できることはもっといろいろあるんですよ!
アノテーション あのてーしょん Anotation
あゝ、素晴らしきかな、アノテーション!
ファイル内にヘッダー行があっても無視できたりしたい
これも可能。
だけどー・・・csvReader.getHeader(false);
で合ってるのかしら。
別のやり方もありそうなものですが。とりあえずこれでもできることはできます。
てかぶっちゃけ、呼ぶメソッド変えるだけでどっちにもいい感じになって欲しい
さすがにメソッドのみとはいかないですが、ちょっと前処理を変えるだけでStringのリストにもJAVABeanにもなります。
変数との依存性も無視できますし、使い方もあまり複雑ではないですし、使いやすくていいライブラリだと思いますよー。
あと、ドキュメントが日本語なのがちょっとアツい。
まとめ
正直ApacheCommonsのが一番使いにくかった。
使えないこともないけど、あとの二つの方が『簡単』『便利』って感が強いですね。
String配列なら
opencsv >> SuperCSV-Anotation >= Commons CSV
JAVABeanなら
SuperCSV-Anotation >> opencsv >> Commons CSV
といったところですかね。
CSVの時間はこれにて閉幕。チャンチャン