web-dev-qa-db-ja.com

新しいJava 8 Streams APIを使用して、一意の行のCSVファイルを解析する

新しいJava 8 Streams API(私は完全な初心者です)を使用して、CSV内の特定の行(名前列に「Neda」が含まれる行)を解析しようとしています。 file。次の article を動機付けとして使用して、「name」、「age」、「height」の3つの列を含むファイルを解析できるようにいくつかのエラーを修正および修正しました。

name,age,height
Marianne,12,61
Julie,13,73
Neda,14,66
Julia,15,62
Maryam,18,70

解析コードは次のとおりです。

@Override
public void init() throws Exception {
    Map<String, String> params = getParameters().getNamed();
    if (params.containsKey("csvfile")) {
        Path path = Paths.get(params.get("csvfile"));
        if (Files.exists(path)){
            // use the new Java 8 streams api to read the CSV column headings
            Stream<String> lines = Files.lines(path);
            List<String> columns = lines
                .findFirst()
                .map((line) -> Arrays.asList(line.split(",")))
                .get();
            columns.forEach((l)->System.out.println(l));
            // find the relevant sections from the CSV file
            // we are only interested in the row with Neda's name
            int nameIndex = columns.indexOf("name");
            int ageIndex columns.indexOf("age");
            int heightIndex = columns.indexOf("height");
            // we need to know the index positions of the 
            // have to re-read the csv file to extract the values
            lines = Files.lines(path);
            List<List<String>> values = lines
                .skip(1)
                .map((line) -> Arrays.asList(line.split(",")))
                .collect(Collectors.toList());
            values.forEach((l)->System.out.println(l));
        }
    }        
}

ヘッダー行の抽出後にファイルの再読み取りを回避する方法はありますか?これは非常に小さなサンプルファイルですが、このロジックを大きなCSVファイルに適用します。

ストリームAPIを使用して、(ファイルの最初のスキャンで)抽出された列名と残りの行の値の間のマップを作成する手法はありますか?

(すべての行を含むList<String>の代わりに)List<List<String>>の形式で1行だけを返すにはどうすればよいですか。列名とそれに対応する値の間のマッピングとして行を見つけることをお勧めします。 (JDBCの結果セットに少し似ています)。ここで役立つ可能性のあるCollectors.mapMerger関数が表示されますが、その使用方法がわかりません。

9
johnco3

BufferedReaderを明示的に使用します。

_List<String> columns;
List<List<String>> values;
try(BufferedReader br=Files.newBufferedReader(path)) {
    String firstLine=br.readLine();
    if(firstLine==null) throw new IOException("empty file");
    columns=Arrays.asList(firstLine.split(","));
    values = br.lines()
        .map(line -> Arrays.asList(line.split(",")))
        .collect(Collectors.toList());
}
_

Files.lines(…)BufferedReader.lines(…)に頼ります。唯一の違いは、_Files.lines_がストリームを構成して、ストリームを閉じるとリーダーが閉じるようにすることです。明示的なtry(…)ステートメントはすでにBufferedReaderを閉じるため、ここでは必要ありません。 。

リーダーの状態についての保証はないことに注意してくださいafterlines()によって返されたストリームは処理されましたが、行を安全に読み取ることができますbefore実行中ストリーム操作。

11
Holger

まず、このコードがファイルを2回読み取っているという懸念は根拠がありません。実際には、 _Files.lines_ は、レイジーポピュレートされた行のストリームを返します。したがって、コードの最初の部分は最初の行のみを読み取り、コードの2番目の部分は残りを読み取ります(ただし、無視された場合でも、最初の行は2回目に読み取られます)。そのドキュメントを引用する:

ファイルからすべての行をStreamとして読み取ります。 readAllLinesとは異なり、このメソッドはすべての行をListに読み込むのではなく、ストリームが消費されるときに遅延してデータを入力します。

1行だけを返すことについての2番目の懸念について。関数型プログラミングでは、あなたがやろうとしていることは フィルタリング と呼ばれます。 Stream APIは、 _Stream.filter_ の助けを借りてそのようなメソッドを提供します。このメソッドは、引数として Predicate を取ります。これは、保持する必要のあるすべてのアイテムに対してtrueを返し、それ以外の場合はfalseを返す関数です。

この場合、名前が_"Neda"_と等しいときにPredicateを返すtrueが必要です。これは、ラムダ式s -> s.equals("Neda")として記述できます。

したがって、コードの2番目の部分では、次のようにすることができます。

_lines = Files.lines(path);
List<List<String>> values = lines
            .skip(1)
            .map(line -> Arrays.asList(line.split(",")))
            .filter(list -> list.get(0).equals("Neda")) // keep only items where the name is "Neda"
            .collect(Collectors.toList());
_

ただし、これは名前が_"Neda"_であるアイテムが1つだけであることを保証するものではなく、すべての可能なアイテムを_List<List<String>>_に収集することに注意してください。ビジネス要件に応じて、最初のアイテムを検索するロジックを追加したり、アイテムが見つからない場合に例外をスローしたりできます。


@Holgerの回答のように、BufferedReaderを直接使用することで、Files.lines(path)を2回呼び出すことを回避できることに注意してください。

6
Tunaki

私はとても遅く応答していることを知っています、しかし多分それは将来誰かを助けるでしょう

ビルダーパターンのおかげで使いやすいcsvパーサー/ライターを作成しました

あなたの場合:解析したい行をフィルタリングすることができます

csvLineFilter(Predicate<String>) 

お手元にあるといいのですが、ここにソースコードがあります https://github.com/i7paradise/CsvUtils-Java8/

メインクラスDemo.Javaに参加して、その動作を表示しました

0
Ismail Ferdous

CSV処理ライブラリの使用

他の答えは良いです。ただし、CSV処理ライブラリを使用して入力ファイルを読み取ることをお勧めします。他の人が指摘したように、CSV形式は見た目ほど単純ではありません。まず、値は引用符で囲まれている場合とされていない場合があります。また、Postgres、MySQL、Mongo、Microsoft Excelなどで使用されているものなど、CSVにはさまざまなバリエーションがあります。

Javaエコシステムはそのようなライブラリをいくつか提供します。私は Apache Commons CSV を使用します。

Apache Commons CSVライブラリはストリームを使用しません。ただし、ライブラリを使用してscut作業を行う場合は、作業にストリームは必要ありません。ライブラリを使用すると、大きなファイルをメモリにロードすることなく、ファイルから行をループする作業が簡単になります。

(ファイルの最初のスキャンで)抽出された列名と残りの行の値の間にマップを作成しますか?

Apache Commons CSVは、withHeaderを呼び出すと自動的にこれを行います。

リストの形式で1行だけを返します

はい、簡単です。

ご要望に応じて、特定の1行の3つのフィールド値のそれぞれを List に入力できます。このListタプル として機能します。

List < String > Tuple = List.of();  // Our goal is to fill this list of values from a single row. Initialize to an empty nonmodifiable list.

入力ファイルに期待する形式を指定します:standard [〜#〜] csv [〜#〜]RFC 418 )、最初の行には列名が入力されます。

CSVFormat format =  CSVFormat.RFC4180.withHeader() ;

入力ファイルを見つけるファイルパスを指定します。

Path path = Path.of("/Users/basilbourque/people.csv");

Try-with-resources構文( チュートリアル を参照)を使用して、パーサーを自動的に閉じます。

各行を読みながら、名前がNedaであることを確認します。見つかった場合は、その行のフィールド値を使用してファイルListを報告します。そして、ループを中断します。 List.of を使用して、変更できない不明な具象クラスのListオブジェクトを便利に返します。つまり、リストに要素を追加したり、リストから要素を削除したりすることはできません。

try (
        CSVParser parser =CSVParser.parse( path , StandardCharsets.UTF_8, format ) ;
)
{
    for ( CSVRecord record : parser )
    {
        if ( record.get( "name" ).equals( "Neda" ) )
        {
            Tuple = List.of( record.get( "name" ) , record.get( "age" ) , record.get( "height" ) );
            break ;
        }
    }
}
catch ( FileNotFoundException e )
{
    e.printStackTrace();
}
catch ( IOException e )
{
    e.printStackTrace();
}

成功した場合は、Listにいくつかの項目が表示されます。

if ( Tuple.isEmpty() )
{
    System.out.println( "Bummer. Failed to report a row for `Neda` name." );
} else
{
    System.out.println( "Success. Found this row for name of `Neda`:" );
    System.out.println( Tuple.toString() );
}

実行時。

成功。 Nedaの名前でこの行が見つかりました:

[ネダ、14、66]

Listをタプルとして使用する代わりに、このデータを適切なデータ型で表すためにPersonクラスを定義することをお勧めします。ここでのコードは、List<String>ではなくPersonインスタンスを返します。

0
Basil Bourque