Open CSVでCSV形式のファイルを読み込み・書き込み

公式ドキュメント

Java用のCSV (comma-separated values) パーサライブラリ。
CSV読み書きに関わる以下について実現可能。

  • 1行に表示する値の数を任意に設定可能
  • 引用符付きのエントリ内のコンマを無視
  • キャリッジリターン(CR)が埋め込まれた引用符付きのエントリ(複数行にまたがるエントリなど)の処理
  • セパレータ文字と引用符に任意のものを設定

CSVファイル書き込み

最小限のコードで実装

CSVの1レコードのデータを保持するクラスの作成

import lombok.Data;

/**
 * サンプルマッピングクラス。<br>
 */
@Data
public class Sample {

  /** 名前 */
  private String name;

  /** 住所 */
  private String address;

}

CSVファイルに書き込む処理の実装

StatefulBeanToCsv#write()メソッドは、型違いで4つ存在するので、書き込むデータによって使い分けること。

  • void write(T bean)
  • void write(List beans)
  • void write(Iterator iBeans)
  • void write(Stream beans)
/**
 * CSVファイルにデータを書き込むサンプル<br>
 */
public class SampleCsvWriter {

  public void write() {
    try (FileWriter writer = new FileWriter("sample.csv")) {
      Sample sample = new Sample();
      sample.setName("太郎");
      sample.setAddress("名古屋");

      StatefulBeanToCsv<Sample> beanToCsv = new StatefulBeanToCsvBuilder<Sample>(writer)
          .build();

      beanToCsv.write(sample);
    } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
    }
  }
}

上記の実装で出力されるCSVファイルは以下の通り

"ADDRESS","NAME"
"名古屋","太郎"

StatefulBeanToCsvBuilderの設定

区切り文字の設定

withSeparator()で設定できる。デフォルトは「,

public class SampleCsvWriter {

  public void write() {
    try (FileWriter writer = new FileWriter("sample.csv")) {
      Sample sample = new Sample();
      sample.setName("太郎");
      sample.setAddress("名古屋");

      StatefulBeanToCsv<Sample> beanToCsv = new StatefulBeanToCsvBuilder<Sample>(writer)
          .withSeparator(':')
          .build();

      beanToCsv.write(sample);
    } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
    }
  }
}

上記の実装で出力されるCSVファイルは以下の通り

"ADDRESS":"NAME"
"名古屋":"太郎"

改行コードの設定

withLineEnd()で設定できる。デフォルトは「\\n

public class SampleCsvWriter {

  public void write() {
    try (FileWriter writer = new FileWriter("sample.csv")) {
      Sample sample = new Sample();
      sample.setName("太郎");
      sample.setAddress("名古屋");

      StatefulBeanToCsv<Sample> beanToCsv = new StatefulBeanToCsvBuilder<Sample>(writer)
          .withLineEnd("\\r\\n")
          .build();

      beanToCsv.write(sample);
    } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
    }
  }
}

ヘッダやレコードの囲み文字の設定

withQuotechar()で設定できる。デフォルトは「"

public class SampleCsvWriter {

  public void write() {
    try (FileWriter writer = new FileWriter("sample.csv")) {
      Sample sample = new Sample();
      sample.setName("太郎");
      sample.setAddress("名古屋");

      StatefulBeanToCsv<Sample> beanToCsv = new StatefulBeanToCsvBuilder<Sample>(writer)
          .withQuotechar('\\'')
          .build();

      beanToCsv.write(sample);
    } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
    }
  }
}

上記の実装で出力されるCSVファイルは以下の通り

'ADDRESS','NAME'
'名古屋','太郎'

ヘッダやレコードを全て囲み文字で囲むかどうか

withApplyQuotesToAll()で設定できる。デフォルトは「true

public class SampleCsvWriter {

  public void write() {
    try (FileWriter writer = new FileWriter("sample.csv")) {
      Sample sample = new Sample();
      sample.setName("太郎");
      sample.setAddress("名古屋");

      StatefulBeanToCsv<Sample> beanToCsv = new StatefulBeanToCsvBuilder<Sample>(writer)
          .withApplyQuotesToAll(false)
          .build();

      beanToCsv.write(sample);
    } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
    }
  }
}

上記の実装で出力されるCSVファイルは以下の通り

ADDRESS,NAME
名古屋,太郎

エスケープ文字の設定

withEscapechar()で設定できる。デフォルトは「"

public class SampleCsvWriter {

  public void write() {
    try (FileWriter writer = new FileWriter("sample.csv")) {
      Sample sample = new Sample();
      sample.setName("太郎");
      sample.setAddress("名古屋");

      StatefulBeanToCsv<Sample> beanToCsv = new StatefulBeanToCsvBuilder<Sample>(writer)
          .withEscapechar('\\\\')
          .build();

      beanToCsv.write(sample);
    } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
    }
  }
}

上記の実装で出力されるCSVファイルは以下の通り

"ADDRESS","NAME"
"\\"名古屋\\"","太郎"

カラムに付与可能なアノテーション

出力の確認には以下の処理を用いた。

public class SampleCsvWriter {

  public void write() {
    try (FileWriter writer = new FileWriter("sample.csv")) {
      Sample sample = new Sample();
      sample.setName("太郎");
      sample.setAddress("名古屋");

      StatefulBeanToCsv<Sample> beanToCsv = new StatefulBeanToCsvBuilder<Sample>(writer)
          .build();

      beanToCsv.write(sample);
    } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
    }
  }
}

beanの項目のCSV出力の順番を定義する

@CsvBindByPositionで指定可能。

@Data
public class Sample {

  /** 名前 */
  @CsvBindByPosition(position = 0)
  private String name;

  /** 住所 */
  @CsvBindByPosition(position = 1)
  private String address;

  /** 年齢 */
  @CsvBindByPosition(position = 2)
  private int age;

}

ただし、このアノテーションを付けた状態でCSV出力しようとするとヘッダ行が表示されなくなる。
これは、デフォルトで設定されているMappingStrategyでは、CsvBindByNameCsvBindByPositionを同時に使用することができないためである。
CsvBindByNameCsvBindByPositionを同時に使用するためには、別途MappingStrategyを実装する必要がある。(後述あり)

出力されるCSVファイル:

"太郎","名古屋","10"

beanの項目のCSV出力の順番を定義する(コレクション用)

@CsvBindAndSplitByPositionを用いて、リストなどのコレクションをCSV出力する場合のCSV出力の順番や、出力する際の要素間の区切り文字等を設定できる。

@Data
public class Sample {

  /** 名前 */
  @CsvBindByPosition(position = 0)
  private String name;

  /** 住所 */
  @CsvBindByPosition(position = 1)
  private String address;

  /** 年齢 */
  @CsvBindByPosition(position = 2)
  private int age;

  /** リスト */
  @CsvBindAndSplitByPosition(elementType = String.class, position = 3, writeDelimiter = " ")
  private List<String> list;

}

出力されるCSVファイル:

"太郎","名古屋","10","aiueo あいうえお"

ヘッダ名を定義する

@CsvBindByNameで指定可能。

ヘッダ名には日本語も設定可能。
ヘッダ名を指定すると動くcom.opencsv.bean.HeaderColumnNameMappingStrategyの処理の中で、 アノテーションの属性に指定されたヘッダ名を大文字にしている処理があるため、英字のヘッダ名を指定すると全て大文字となる。
独自MappingStrategyを定義することで任意の英字をヘッダに指定可能。参考

@Data
public class Sample {

  /** 名前 */
  @CsvBindByName(column = "名前")
  private String name;

  /** 住所 */
  @CsvBindByName(column = "address")
  private String address;

  /** 年齢 */
  @CsvBindByName(column = "age")
  private int age;

}

出力されるCSVファイル:

"ADDRESS","AGE","名前"
"名古屋","10","太郎"

ヘッダ名を定義する(コレクション用)

@CsvBindAndSplitByNameを用いてリストなどのコレクションをCSV出力する場合のヘッダ名や、出力する際の要素間の区切り文字等を設定できる。

@Data
public class Sample {

  /** 名前 */
  @CsvBindByName(column = "名前")
  private String name;

  /** 住所 */
  @CsvBindByName(column = "address")
  private String address;

  /** 年齢 */
  @CsvBindByName(column = "age")
  private int age;

  /** リスト */
  @CsvBindAndSplitByName(elementType = String.class, column = "list", writeDelimiter = " ")
  private List<String> list;

}

出力されるCSVファイル:

"ADDRESS","AGE","LIST","名前"
"名古屋","10","aiueo あいうえお","太郎"

日付のマッピング

日付フィールドに付与する。日付のフォーマットを指定可能。

@CsvDateを付与するフィールドには@CsvBindByPositionもしくは@CsvBindByNameも付与する必要がある。

@Data
public class Sample {

  /** 名前 */
  @CsvBindByPosition(position = 0)
  private String name;

  /** 住所 */
  @CsvBindByPosition(position = 1)
  private String address;

  /** 年齢 */
  @CsvBindByPosition(position = 2)
  private int age;

  /** リスト */
  @CsvBindAndSplitByPosition(elementType = String.class, position = 3, writeDelimiter = " ")
  private List<String> list;

  /** 日時 */
  @CsvDate("yyyy/MM/dd HH:mm:ss")
  @CsvBindByPosition(position = 4)
  private LocalDateTime datetime;

}

出力されるCSVファイル:

"太郎","名古屋","10","aiueo あいうえお","2022/11/11 13:10:47"

数値のマッピング

数値を扱うラッパークラスやBigDecimal、BigIntegerなフィールドに付与する。

@CsvNumberを付与するフィールドには@CsvBindByPositionもしくは@CsvBindByNameも付与する必要がある。

そんなに使わない気がするアノテーションたち

CsvBindAndJoinByName, CsvBindAndJoinByPosition

汎用的に値をマッピングさせたいときに使用する。参考
org.apache.commons.collections4.MultiValuedMap」型のフィールドに付与する。

ヘッダ名を定義したい場合は「CsvBindAndJoinByName」で、 順番を定義したい場合は「CsvBindAndJoinByPosition」を使用する

利用シーンとしては、以下のBeanで複数のCSVファイルのマッピングに対応させたい場合っぽい・・・

@Data
public class Person {
  /** ID */
  @CsvBindByName(column = "id")
  String id;

  /** 名前 */
  @CsvBindByName(column = "name")
  String name;

  /** 追加情報 */
  @CsvBindAndJoinByName(column = ".*", elementType = String.class)
  Map<String,String> additionalInfo;
}

上のクラスに以下のCSVどっちもマッピングできる

"ID","NAME","ADDRESS"
"1","山田","名古屋"
"ID","NAME","AGE"
"1","山田","30"

CsvCustomBindByName, CsvCustomBindByPosition

CsvBindByNameCsvBindByPositionと同じであるが、独自のデータ変換クラスを作成する必要がある。


CSVファイル読み込み

文字列配列で読み込む

public class SampleCsvReader {

  public void read() {
    try (FileReader reader = new FileReader("sample.csv")) {
      CSVReader csvReader = new CSVReaderBuilder(reader)
          .build();

      // 全行まとめて読み込む
      List<String[]> list = csvReader.readAll();
    } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
      fail();
    } catch (CsvException e) {
      fail();
    }
  }
}

1行ずつ読み込むこともできる。

public class SampleCsvReader {

  public void read() {
    try (FileReader reader = new FileReader("sample.csv")) {
      CSVReader csvReader = new CSVReaderBuilder(reader)
          .build();

      while ((nextLine = csvReader.readNext()) != null) {
        // 読み込んだ行に対する処理
      }
    } catch (IOException | CsvException e) {
      fail();
    }
  }
}

beanで読み込む

JavaBeans形式でCSV読み込みする方法を見るに、 読み込みの時は全部Stringでないとマッピング出来なさそう・・・


任意のMappingStrategy実装

デフォルトの設定で気になる以下に対応するための独自クラスを作成する。

  • ヘッダ名も指定したいし、CSVに出力する順番も定義したい
  • ヘッダ名に小文字を設定したい
import org.apache.commons.lang3.StringUtils;

import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {

  public CustomMappingStrategy(Class<? extends T> type) {
    setType(type);
  }

  @Override
  public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
    final int numColumns = getFieldMap().values().size();
    super.generateHeader(bean);

    String[] header = new String[numColumns];

    BeanField beanField;
    for (int i = 0; i < numColumns; i++) {
      beanField = findField(i);
      String columnHeaderName = extractHeaderName(beanField);
      header[i] = columnHeaderName;
    }
    return header;
  }

  private String extractHeaderName(final BeanField beanField) {
    if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(
        CsvBindByName.class).length == 0) {
      return StringUtils.EMPTY;
    }

    final CsvBindByName bindByNameAnnotation = beanField.getField()
        .getDeclaredAnnotationsByType(CsvBindByName.class)[0];
    return bindByNameAnnotation.column();
  }

}

作成したMappingStrategyクラスは、StatefulBeanToCsvBuilder#withMappingStrategy()で設定する。

public class SampleCsvWriter {

  public void write() {
    try (FileWriter writer = new FileWriter("sample.csv")) {
      Sample sample = new Sample();
      sample.setName("太郎");
      sample.setAddress("名古屋");

      CustomMappingStrategy<Sample> strategy = new CustomMappingStrategy<>(Sample.class);

      StatefulBeanToCsv<Sample> beanToCsv = new StatefulBeanToCsvBuilder<Sample>(writer)
          .withMappingStrategy(strategy)
          .build();

      beanToCsv.write(sample);
    } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
    }
  }
}