web-dev-qa-db-fra.com

Analyse d'un fichier CSV pour une ligne unique à l'aide de la nouvelle API Java 8 Streams

J'essaie d'utiliser la nouvelle API Java 8 Streams (pour laquelle je suis un novice complet) pour analyser une ligne particulière (celle avec 'Neda' dans la colonne du nom) dans un fichier CSV. À l'aide de article pour motivation, j'ai modifié et corrigé certaines erreurs afin de pouvoir analyser le fichier contenant 3 colonnes - 'nom', 'âge' et 'hauteur'.

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

Le code d'analyse est le suivant:

@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));
        }
    }        
}

Existe-t-il un moyen d'éviter de relire le fichier après l'extraction de la ligne d'en-tête? Bien que ce soit un très petit fichier exemple, je vais appliquer cette logique à un gros fichier CSV.

Existe-t-il une technique permettant d'utiliser l'API de flux pour créer un mappage entre les noms de colonne extraits (lors de la première analyse du fichier) et les valeurs des lignes restantes?

Comment puis-je renvoyer une seule ligne sous la forme List<String> (au lieu de List<List<String>> contenant toutes les lignes). Je préférerais simplement trouver la ligne en tant que mappage entre les noms de colonne et leurs valeurs correspondantes. (un peu comme un résultat défini dans JDBC). Je vois une fonction Collectors.mapMerger qui pourrait être utile ici, mais je ne sais pas comment l'utiliser.

7
johnco3

Utilisez explicitement une 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(…) a également recours à BufferedReader.lines(…). La seule différence est que Files.lines configurera le flux de sorte que sa fermeture ferme le lecteur, ce dont nous n’avons pas besoin ici, car l’instruction explicite try(…) garantit déjà la fermeture de la BufferedReader.

Notez qu'il n'y a aucune garantie sur l'état du lecteur après le flux renvoyé par lines() a été traité, mais nous pouvons lire en toute sécurité les lignes avant effectuant l'opération de flux.

11
Holger

Premièrement, votre préoccupation que ce code ne lit pas le fichier n’est pas fondée. En fait, Files.lines renvoie un flux de lignes rempli paresseux. Ainsi, la première partie du code ne lit que la première ligne et la deuxième partie, le reste (elle lit la première ligne une seconde fois, même si elle est ignorée). Citant sa documentation:

Lire toutes les lignes d'un fichier en tant que Stream. Contrairement à readAllLines, cette méthode ne lit pas toutes les lignes dans un List, mais remplit la paresseuse au fur et à mesure que le flux est consommé.

Sur votre deuxième préoccupation concernant le retour d'une seule ligne. En programmation fonctionnelle, ce que vous essayez de faire s'appelle filtrage . L'API Stream fournit une telle méthode à l'aide de Stream.filter . Cette méthode prend l'argument Predicate , qui est une fonction qui renvoie true pour tous les éléments à conserver, et false sinon.

Dans ce cas, nous voulons une Predicate qui renverrait true lorsque le nom est égal à "Neda". Cela pourrait être écrit comme l'expression lambda s -> s.equals("Neda").

Donc, dans la deuxième partie de votre code, vous pourriez avoir:

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());

Notez cependant que cela ne garantit pas qu'il n'y a qu'un seul élément dont le nom est "Neda", il regroupe tous les éléments possibles dans un List<List<String>>. Vous pouvez ajouter une logique pour rechercher le premier élément ou émettre une exception si aucun élément n'est trouvé, en fonction des besoins de votre entreprise.


Notez encore qu'il est possible d'éviter d'appeler deux fois Files.lines(path) en utilisant directement une BufferedReader comme dans la réponse de @ Holger.

6
Tunaki

Je sais que je réponds si tard, mais peut-être que cela aidera quelqu'un à l'avenir

J'ai créé un analyseur/graveur CSV facile à utiliser grâce à son motif constructeur

Pour votre cas: vous pouvez filtrer les lignes que vous souhaitez analyser à l'aide de

csvLineFilter(Predicate<String>) 

J'espère que vous le trouverez pratique, voici le code source https://github.com/i7paradise/CsvUtils-Java8/

J'ai rejoint une classe principale Demo.Java pour afficher son fonctionnement

0
Ismail Ferdous