Kotlin

CSVファイルをスマートに読み込む方法 [Java / Kotlin]

この記事で分かること

  • Java / Kotlinで、Google Guavaを使ってCSVファイルを操作する方法
  • CSVのライブラリを用意せずに、ファイル操作・文字列操作だけで簡単に読み出す方法
  • CSVファイルの各行を List や Map に変換して取り出す方法

この記事では、Google Guavaを使ったCSVファイルの操作について、サンプルコードを用いて解説しています。

JavaやKotlinで、CSVファイルを操作するのは色々な方法が考えられますが、車輪の再発明 - Wikipedia が乱立する典型的な例では無いでしょうか?

JavaのCSVライブラリ まとめ | (oscasierra.net) の様なサイトで紹介されているCSVのライブラリを使うのも一つの手ですが、そこまで高機能なものは必要なく、シンプルで単純な機能だけで仕様を満たせる場合も往々にしてあり得ると思います。

そこで今回は、シンプルなCSVファイルの読み込み機能だけに焦点を当て、なるべく簡単に実装できる方法を紹介します。

Kotlin版を先に作りましたが、Javaに変換したコードも一緒に載せているので、参考になる部分があれば幸いです。

ゴイチ

それでは行きましょう!

サンプルコード

CSVファイルを読み込み、各行のカラムをGoogle GuavaFilesを使って処理するプログラムです。

関数/メソッドのパラメータはそれぞれ、

filePath: CSVファイルのパス
doubleQuote: ダブルクォーテーション囲み文字の有無
encoding: ファイルの文字コード

を表しています。

CSVのカラムに改行文字が含まれるなど、この例よりも複雑なCSVファイルを扱いたい場合、以前書いた以下の記事も参考になると思います。
ダブルクォーテーションが含まれるCSVのスマートなパース方法についても記載しています。

CSVにダブルクォーテーションやカンマが含まれる場合のパース処理 [Java]

この記事では、CSVファイルにダブルクォーテーションやカンマ、改行文字などの、構文を狂わせる要素が含まれる場合のパース方法について紹介しています。 仕事で外部システムと連携する場合など、時として非常に ...

続きを見る

CSVの行をListで取得する

各行のカラムをList<String>として取得します。

サンプルコードでは、forEach で各行に対する処理をしています。

Kotlin

import com.google.common.io.Files
import org.apache.commons.lang3.StringUtils
import java.io.File
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets

fun readCsvAsList(filePath: String, doubleQuote: Boolean = false, encoding: Charset = StandardCharsets.UTF_8) {
    val enclosure = if (doubleQuote) "\"" else ""

    Files.asCharSource(File(filePath), encoding)
        .lines()
        .map { StringUtils.removeStart(StringUtils.removeEnd(it, enclosure), enclosure) }
        .map { it.split("$enclosure,$enclosure") }
        .forEach {
            // 1行ごとの処理
            println(it)
        }
}

ダブルクォーテーションの事を考えなくていいなら単純に以下の処理だけでもOKです。

fun readCsvAsList(filePath: String, encoding: Charset = StandardCharsets.UTF_8) {
    Files.asCharSource(File(filePath), encoding)
        .lines()
        .map { it.split(",") }
        .forEach {
            // 1行ごとの処理
            println(it)
        }
}

Java

Kotlinと同じ処理を再現しています。

import com.google.common.io.CharSource;
import com.google.common.io.Files;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;


public class ParseCsvGuava {
    public void readCsvAsList(String filePath, boolean doubleQuote, Charset encoding) throws IOException {
        StringBuilder sb = new StringBuilder();
        if (doubleQuote) {
            sb.append("\"");
        }
        final String enclosure = sb.toString();

        Files.asCharSource(new File(filePath), encoding)
                .lines()
                .map(it -> StringUtils.removeStart(StringUtils.removeEnd(it, enclosure), enclosure))
                .map(it -> Arrays.asList(it.split(enclosure + "," + enclosure)))
                .forEach(it -> {
                    // 1行ごとの処理
                    System.out.println(StringUtils.join(it, ", "));
                });

    }
}

CSVの行をMapで取得する

各行のカラムをMap<String, String>として取得します。

1行目にヘッダ行が含まれるCSVを想定しています。
2行目以降にデータ行が続きます。

頻繁にCSVのフォーマットが変更される可能性がある場合など、Mapとして取得しておくと便利な場合があるかも知れません。

Kotlin

import com.google.common.io.Files
import org.apache.commons.lang3.StringUtils
import java.io.File
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets

fun readCsvAsMap(filePath: String, doubleQuote: Boolean = false, encoding: Charset = StandardCharsets.UTF_8) {
    val enclosure = if (doubleQuote) "\"" else ""
    val charSource = Files.asCharSource(File(filePath), encoding)

    // ヘッダ行
    val headers = charSource.readFirstLine()?.let {
        parseLine(it, enclosure)
    } ?: throw RuntimeException("Header is empty.")

    // データ行
    charSource.lines()
        .skip(1)
        .map { parseLine(it, enclosure) }
        .map { it.mapIndexed { index, column -> (headers[index] to column) }.toMap() }
        .forEach {
            // 1行ごとの処理
            println(it)
        }
}

private fun parseLine(line: String, enclosure: String): List<String> {
    return line
        .let { StringUtils.removeStart(line, enclosure) }
        .let { StringUtils.removeEnd(it, enclosure) }
        .let { it.split("$enclosure,$enclosure") }
}

Java

Kotlinとほぼ同じ処理を再現しています。

import com.google.common.io.CharSource;
import com.google.common.io.Files;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class ParseCsvGuava {
    public void readCsvAsMap(String filePath, boolean doubleQuote, Charset encoding) throws IOException {
        StringBuilder sb = new StringBuilder();
        if (doubleQuote) {
            sb.append("\"");
        }
        final String enclosure = sb.toString();
        CharSource charSource = Files.asCharSource(new File(filePath), encoding);

        // ヘッダ行
        final List<String> headers = parseLine(charSource.readFirstLine(), enclosure);

        // データ行
        charSource.lines()
                .skip(1)
                .map(it -> parseLine(it, enclosure))
                .map(it -> {
                    Map<String, String> map = new LinkedHashMap();
                    for (int i = 0; i < it.size(); i++) {
                        map.put(headers.get(i), it.get(i));
                    }
                    return map;
                })
                .forEach(it -> {
                    // 1行ごとの処理
                    System.out.println(it);
                });
    }

    private List<String> parseLine(String line, String enclosure) {
        return Arrays.asList(
                StringUtils.removeStart(
                        StringUtils.removeEnd(line, enclosure),
                        enclosure
                ).split(enclosure + "," + enclosure)
        );
    }
}

まとめ

最後に、この記事の内容をまとめます。

要点

  • Guavaの Files.asCharSource() でファイルを開く
  • charSource.lines() で取得できるStreamで各行を処理する
  • Stream.map() でCSVのカンマやダブルクォーテーションを除去するとスマートな処理になる

Google Guavaのファイル操作ライブラリはあまり頻繁に使われませんが、意外と便利なメソッドが揃っていて、この記事の例以外にも使えるシーンがありそうです。↓

Files (Guava: Google Core Libraries for Java HEAD-jre-SNAPSHOT API)

もっと簡単に書けるよ!」とか「他に良い方法を知ってる!」などありましたら、ぜひコメント欄で教えて下さい。

改善できる箇所があれば記事を修正していきたいと思います。

ゴイチ

最後まで読んで頂きありがとうございました!

この記事は役に立ちましたか?

  • この記事を書いた人
アバター画像

ゴイチ

ソフトウェアエンジニア歴20年。 C/C++、Java、C#、Kotlinが得意で、組込系からAndroidアプリ、大規模なWebサービスなど幅広いプログラミング経験があります。 現在は某SNSの会社でWebエンジニアをしています。

-Kotlin
-, ,