(2019/12/20修正)公式サイトの Documentation ページのURLを修正しました。
(2017/02/19修正)CsvParserSettings#setRowProcessor が非推奨になっていたので CsvParserSettings#setProcessor へ変更しました。settings.setRowProcessor(rowProcessor);
→ settings.setProcessor(rowProcessor);
へ変更しています。
Java で CSV ファイルを処理するために最初 OpenCSV の使い方を Web で調べていたのですが、stackoverflow を見ていたら uniVocity-parsers というライブラリが OpenCSV より速いと書かれている記事を見かけたので使い方等を調べてみました。
概要、使ってみた感想
- CSV、TSV、固定長ファイルを処理できる Parser ライブラリ。
- 公式サイトやJavaDoc等のドキュメントがまとまっており使用サンプルも豊富で分かりやすいです。
- CSV ファイルのレコードを JavaBean に変換する際にアノテーションで変換ルールを指定できます。
- 他の CSV ライブラリより速いらしいです ( uniVocity/csv-parsers-comparison に比較結果が書かれています、ただし自分はそこまで試していないので分かりません )
- Java8 の日付時刻API には対応していません ( 標準で用意されているのは Date, Calendar に対応するメソッドでした )。ただし、独自変換のクラスを作成すれば対応できます。
- 個人的には使いやすいライブラリだと思います。
ライブラリのサイト
GitHub
https://github.com/uniVocity/univocity-parsers
公式サイトの About ページ
http://www.univocity.com/pages/about-parsers
公式サイトの Documentation ページ ( Tutorials、GitHub、Javadocs へのリンクがあります )
https://www.univocity.com/pages/univocity_parsers_documentation
ライセンス
Apache license 2.0 です。公式サイトの About ページ に記載があります。
インストール方法
<dependency>
<groupId>com.univocity</groupId>
<artifactId>univocity-parsers</artifactId>
<version>1.5.6</version>
<type>jar</type>
</dependency>
dependencies {
.....
compile("com.univocity:univocity-parsers:1.5.6")
.....
}
サンプル ( CSV のみ )
- JUnit4 のテストメソッドで書きます。自分は Spring Boot のプロジェクトを作成して試しています。
- String の配列の取得結果を出力するのに Guava の Joiner を使用しています。
- JavaBean の Getter/Setter の定義やデータ出力のために Lombok の @Data, @ToString を使用しています。
CSV ファイルを読み込む
CSV ファイルのサンプルとして以下のものを使用します。
商品コード,商品名,価格,種別,在庫,説明,登録日
SAMPLE-001,サンプル1,1234567890,通常商品,有,"これは""サンプル""です",2015/09/27 12:03:45
SAMPLE-002,サンプル2,5827,メーカー直送商品,無,"説明に改行を入れてみます。
2行目です",未登録
SAMPLE-003,サンプル3,-,テスト商品,無,"価格も登録日も未定です",-
# ↑上は空行、←ここはコメント行です
sample-004, サンプル4 ,8987 ,通常商品,有,"商品名、価格の前後に空白あり",2015/09/25 08:31:00
サンプルを書いている時の CSV ファイルは以下の仕様です。
- 文字コードは Windows-31J
- 改行コードは CR+LF
- 項目は必要なもののみダブルクォーテーションで囲む ( 「説明」だけ囲っています )
- データ内の改行あり
- 価格、登録日は未定の場合 "-" にする
- 空行、コメント行あり
List に一気に読み込む
@Test
public void uniVocityParsersTest_001() throws Exception {
CsvParserSettings settings = new CsvParserSettings();
settings.getFormat().setLineSeparator("\r\n"); // 改行コードは CR+LF
settings.setHeaderExtractionEnabled(true); // 1行目はヘッダ行としてスキップする
CsvParser parser = new CsvParser(settings);
try (
BufferedReader br
= Files.newBufferedReader(Paths.get("C:/tmp/テストデータ.csv")
, Charset.forName("Windows-31J"));
) {
List<String[]> allRows = parser.parseAll(br);
allRows.stream().forEach(row -> System.out.println(Joiner.on(", ").join(row)));
}
}
SAMPLE-001, サンプル1, 1234567890, 通常商品, 有, これは"サンプル"です, 2015/09/27 12:03:45
SAMPLE-002, サンプル2, 5827, メーカー直送商品, 無, 説明に改行を入れてみます。
2行目です, 未登録
SAMPLE-003, サンプル3, -, テスト商品, 無, 価格も登録日も未定です, -
sample-004, サンプル4, 8987, 通常商品, 有, 商品名、価格の前後に空白あり, 2015/09/25 08:31:00
- デフォルトの設定で以下の仕様になっています。
1行ずつ読み込む
@Test
public void uniVocityParsersTest_002() throws Exception {
CsvParserSettings settings = new CsvParserSettings();
settings.getFormat().setLineSeparator("\r\n"); // 改行コードは CR+LF
settings.setHeaderExtractionEnabled(true); // 1行目はヘッダ行としてスキップする
CsvParser parser = new CsvParser(settings);
try (
BufferedReader br
= Files.newBufferedReader(Paths.get("C:/tmp/テストデータ.csv")
, Charset.forName("Windows-31J"));
) {
parser.beginParsing(br);
String[] row;
while ((row = parser.parseNext()) != null) {
System.out.println(Joiner.on(", ").join(row));
}
// ファイルの最後まで読みこめば自動的にクローズされるが、読み込まない場合には以下のメソッドを呼び出す
// parser.stopParsing();
}
}
SAMPLE-001, サンプル1, 1234567890, 通常商品, 有, これは"サンプル"です, 2015/09/27 12:03:45
SAMPLE-002, サンプル2, 5827, メーカー直送商品, 無, 説明に改行を入れてみます。
2行目です, 未登録
SAMPLE-003, サンプル3, -, テスト商品, 無, 価格も登録日も未定です, -
sample-004, サンプル4, 8987, 通常商品, 有, 商品名、価格の前後に空白あり, 2015/09/25 08:31:00
読み込む時に商品コードを大文字に変換する&ヘッダー行を取得する
@Test
public void uniVocityParsersTest_003() throws Exception {
CsvParserSettings settings = new CsvParserSettings();
settings.getFormat().setLineSeparator("\r\n"); // 改行コードは CR+LF
settings.setHeaderExtractionEnabled(true); // 1行目はヘッダ行としてスキップする
// 商品コードは必ず英大文字に変換する
ObjectRowListProcessor rowProcessor = new ObjectRowListProcessor();
rowProcessor.convertFields(Conversions.toUpperCase()).set("商品コード");
settings.setProcessor(rowProcessor);
CsvParser parser = new CsvParser(settings);
try (
BufferedReader br
= Files.newBufferedReader(Paths.get("C:/tmp/テストデータ.csv")
, Charset.forName("Windows-31J"));
) {
parser.parse(br);
String[] headers = rowProcessor.getHeaders(); // ヘッダー行を取得する
List<Object[]> allRows = rowProcessor.getRows(); // データ行を取得する
System.out.println(Joiner.on(", ").join(headers));
allRows.stream().forEach(row -> System.out.println(Joiner.on(", ").join(row)));
}
}
商品コード, 商品名, 価格, 種別, 在庫, 説明, 登録日
SAMPLE-001, サンプル1, 1234567890, 通常商品, 有, これは"サンプル"です, 2015/09/27 12:03:45
SAMPLE-002, サンプル2, 5827, メーカー直送商品, 無, 説明に改行を入れてみます。
2行目です, 未登録
SAMPLE-003, サンプル3, -, テスト商品, 無, 価格も登録日も未定です, -
SAMPLE-004, サンプル4, 8987, 通常商品, 有, 商品名、価格の前後に空白あり, 2015/09/25 08:31:00
- ObjectRowListProcessor を定義することで以下のことが出来るようになります。
- ヘッダー行を String[] 型の変数に取得できます。
- 変換ルールを定義することが出来ます。変換ルールは List にデータを取得する時よりも JavaBean に変換する時にアノテーションで指定する方がメインの使われ方になると思います。
JavaBean に変換する
import com.univocity.parsers.annotations.*;
import lombok.Data;
import lombok.ToString;
import java.math.BigDecimal;
@Data
@ToString
public class Item {
// @Trim を付けると前後の半角スペースが除去される
@Trim
// 英大文字に変換する
@UpperCase
// CSVファイルのカラムと関連付けるフィールドには @Parsed を付ける
@Parsed(field = "商品コード")
private String itemCode;
@Trim
@Parsed(field = "商品名")
private String itemName;
// "-" は null とみなす
@NullString(nulls = { "-" })
// null の場合には 0 にする ( 空の場合も 0 になる )
@Parsed(field = "価格", defaultNullRead = "0")
private BigDecimal price;
@Trim
@Parsed(field = "種別")
private String itemType;
@BooleanString(falseStrings = { "無" }, trueStrings = { "有" })
@Parsed(field = "在庫")
private Boolean stock;
@Trim
// index ( 0~ )でも指定可能
@Parsed(index = 5)
private String description;
@NullString(nulls = { "未登録", "-" })
@Parsed(field = "登録日")
private String dateOfRegistration;
}
@Test
public void uniVocityParsersTest_004() throws Exception {
CsvParserSettings settings = new CsvParserSettings();
settings.getFormat().setLineSeparator("\r\n"); // 改行コードは CR+LF
settings.setHeaderExtractionEnabled(true); // 1行目はヘッダ行としてスキップする
BeanListProcessor<Item> rowProcessor = new BeanListProcessor<>(Item.class);
settings.setProcessor(rowProcessor);
CsvParser parser = new CsvParser(settings);
try (
BufferedReader br
= Files.newBufferedReader(Paths.get("C:/tmp/テストデータ.csv")
, Charset.forName("Windows-31J"));
) {
parser.parse(br);
List<Item> allRows = rowProcessor.getBeans(); // データ行を取得する
allRows.stream().forEach(System.out::println);
}
}
Item(itemCode=SAMPLE-001, itemName=サンプル1, price=1234567890, itemType=通常商品, stock=true, description=これは"サンプル"です, dateOfRegistration=2015/09/27 12:03:45)
Item(itemCode=SAMPLE-002, itemName=サンプル2, price=5827, itemType=メーカー直送商品, stock=false, description=説明に改行を入れてみます。
2行目です, dateOfRegistration=null)
Item(itemCode=SAMPLE-003, itemName=サンプル3, price=0, itemType=テスト商品, stock=false, description=価格も登録日も未定です, dateOfRegistration=null)
Item(itemCode=SAMPLE-004, itemName=サンプル4, price=8987, itemType=通常商品, stock=true, description=商品名、価格の前後に空白あり, dateOfRegistration=2015/09/25 08:31:00)
アノテーションの設定が正しくないと現在の CsvParserSettings の設定が出力されます。上の実装だと以下のように出力されました。
Parser Configuration: CsvParserSettings:
Column reordering enabled=true
Empty value=null
Header extraction enabled=true
Headers=null
Ignore leading whitespaces=true
Ignore trailing whitespaces=true
Input buffer size=1048576
Input reading on separate thread=true
Line separator detection enabled=false
Maximum number of characters per column=4096
Maximum number of columns=512
Null value=null
Number of records to read=all
Parse unescaped quotes=true
Row processor=com.univocity.parsers.common.processor.BeanListProcessor
Selected fields=none
Skip empty lines=trueFormat configuration:
CsvFormat:
Comment character=#
Field delimiter=,
Line separator (normalized)=\n
Line separator sequence=\r\n
Quote character="
Quote escape character=quote escape
Quote escape escape character=\0
JavaBean に変換する ( 種別は文字列からコードへ変換し、登録日は String → LocalDateTime 型の変数へセットする )
独自変換をする場合には Conversion インターフェースを実装したクラスを作成して、JavaBean に @Convert アノテーションで作成したクラスを使用するよう設定します。
最初に種別を文字列からコードへ変換する ItemTypeStringToCodeConversion クラスを作成します。
import com.univocity.parsers.conversions.Conversion;
import lombok.Getter;
public class ItemTypeStringToCodeConversion implements Conversion<String, String> {
public ItemTypeStringToCodeConversion(String... args) {
}
@Override
public String execute(String input) {
return ItemTypeValues.getValue(input);
}
@Override
public String revert(String input) {
return ItemTypeValues.getText(input);
}
@Getter
public enum ItemTypeValues {
NORMAL_ITEM("0001", "通常商品")
, SENDFROMMAKER_ITEM("0002", "メーカー直送商品")
, TEST_ITEM("9999", "テスト商品");
private final String value;
private final String text;
ItemTypeValues(String value, String text) {
this.value = value;
this.text = text;
}
public static String getValue(String text) {
String result = "";
for (ItemTypeValues val : ItemTypeValues.values()) {
if (val.getText().equals(text)) {
result = val.getValue();
}
}
return result;
}
public static String getText(String value) {
String result = "";
for (ItemTypeValues val : ItemTypeValues.values()) {
if (val.getValue().equals(value)) {
result = val.getText();
}
}
return result;
}
}
}
次に登録日を String → LocalDateTime へ変換する StringToLocalDateTimeConversion クラスを作成します。
import com.univocity.parsers.conversions.Conversion;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class StringToLocalDateTimeConversion implements Conversion<String, LocalDateTime> {
public StringToLocalDateTimeConversion(String... args) {
}
@Override
public LocalDateTime execute(String input) {
LocalDateTime result = null;
if (input != null) {
result = LocalDateTime.parse(input, DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
}
return result;
}
@Override
public String revert(LocalDateTime input) {
String result = null;
if (input != null) {
result = input.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
}
return result;
}
}
種別と登録日のフィールドに @Convert で作成したクラスを指定します。
import com.univocity.parsers.annotations.*;
import lombok.Data;
import lombok.ToString;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@ToString
public class Item {
// @Trim を付けると前後の半角スペースが除去される
@Trim
// 英大文字に変換する
@UpperCase
// CSVファイルのカラムと関連付けるフィールドには @Parsed を付ける
@Parsed(field = "商品コード")
private String itemCode;
@Trim
@Parsed(field = "商品名")
private String itemName;
// "-" は null とみなす
@NullString(nulls = { "-" })
// null の場合には 0 にする ( 空の場合も 0 になる )
@Parsed(field = "価格", defaultNullRead = "0")
private BigDecimal price;
@Trim
@Convert(conversionClass = ItemTypeStringToCodeConversion.class)
@Parsed(field = "種別")
private String itemType;
@BooleanString(falseStrings = { "無" }, trueStrings = { "有" })
@Parsed(field = "在庫")
private Boolean stock;
@Trim
// index ( 0~ )でも指定可能
@Parsed(index = 5)
private String description;
@NullString(nulls = { "未登録", "-" })
@Convert(conversionClass = StringToLocalDateTimeConversion.class)
@Parsed(field = "登録日")
private LocalDateTime dateOfRegistration;
}
最後に uniVocityParsersTest_004 メソッドを実行します。
Item(itemCode=SAMPLE-001, itemName=サンプル1, price=1234567890, itemType=0001, stock=true, description=これは"サンプル"です, dateOfRegistration=2015-09-27T12:03:45)
Item(itemCode=SAMPLE-002, itemName=サンプル2, price=5827, itemType=, stock=false, description=説明に改行を入れてみます。
2行目です, dateOfRegistration=null)
Item(itemCode=SAMPLE-003, itemName=サンプル3, price=0, itemType=9999, stock=false, description=価格も登録日も未定です, dateOfRegistration=null)
Item(itemCode=SAMPLE-004, itemName=サンプル4, price=8987, itemType=0001, stock=true, description=商品名、価格の前後に空白あり, dateOfRegistration=2015-09-25T08:31)
CSVファイルを書き出す
writeRow メソッドで書き出す
@Test
public void uniVocityParsersTest_005() throws Exception {
try (
BufferedWriter bw
= Files.newBufferedWriter(Paths.get("C:/tmp/テストデータ2.csv")
, Charset.forName("Windows-31J")
, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
) {
CsvWriter writer = new CsvWriter(bw, new CsvWriterSettings());
writer.writeHeaders("商品コード", "商品名", "説明");
writer.writeRow("SAMPLE-101", "サンプル101", "これはテストです");
writer.writeRow("SAMPLE-102", "サンプル102", "途中に\"コメント\",\"コメント2\"を入れてみます");
writer.close();
}
}
商品コード,商品名,説明
SAMPLE-101,サンプル101,これはテストです
SAMPLE-102,サンプル102,"途中に""コメント"",""コメント2""を入れてみます"
JavaBean のデータを出力する
JavaBean は上で作成した Item クラスを利用します。Conversion のルールは Write 時にも適用されます。
@Test
public void uniVocityParsersTest_006() throws Exception {
List<Item> itemList = new ArrayList<>();
// 1件目
Item item = new Item();
item.setItemCode("SAMPLE-201");
item.setItemName("サンプル201号");
item.setPrice(new BigDecimal("1234567890"));
item.setItemType("0001");
item.setStock(false);
item.setDescription("\"1行目\",\"コメントあり\"の商品です");
item.setDateOfRegistration(LocalDateTime.of(2015, 9, 27, 10, 15, 0));
itemList.add(item);
// 2件目
item = new Item();
item.setItemCode("SAMPLE-305");
item.setItemName("サンプル305号");
item.setPrice(new BigDecimal("56000"));
item.setItemType("9999");
item.setStock(true);
item.setDescription("2行目はテスト商品です");
item.setDateOfRegistration(LocalDateTime.of(2015, 9, 28, 0, 0, 0));
itemList.add(item);
try (
BufferedWriter bw
= Files.newBufferedWriter(Paths.get("C:/tmp/テストデータ2.csv")
, Charset.forName("Windows-31J")
, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
) {
CsvWriterSettings settings = new CsvWriterSettings();
settings.setHeaders("商品コード", "商品名", "価格", "種別", "在庫", "説明", "登録日");
BeanWriterProcessor<Item> writerProcessor = new BeanWriterProcessor<>(Item.class);
settings.setRowWriterProcessor(writerProcessor);
CsvWriter writer = new CsvWriter(bw, settings);
writer.writeHeaders();
writer.processRecordsAndClose(itemList);
}
}
商品コード,商品名,価格,種別,在庫,説明,登録日
SAMPLE-201,サンプル201号,1234567890,通常商品,無,"""1行目"",""コメントあり""の商品です",2015/09/27 10:15:00
SAMPLE-305,サンプル305号,56000,テスト商品,有,2行目はテスト商品です,2015/09/28 00:00:00