Java - CSV oder Excel Datei in Objekt parsen

Java - CSV oder Excel Datei in Objekt parsen

Excel Dateien sind in der Arbeitswelt allgegenwärtig. Vor allem findet Excel meiner Erfahrung nach vornämlich bei Projektleitern und anderen eher zahlenorientiert arbeitenden Mitarbeitern Anwendung. Wenngleich sich Excel und CSV Dateien in ihrem Aufbau doch sehr unterscheiden können, so sind sich diese beiden Dateiformate in der grundlegenden Struktur doch recht ähnlich. Wer schon einmal in Excel oder bspw. LibreOffice die Funktion "Speichern unter" verwendet hat, der weiß, dass das die beiden Dateiformate nur zwei Klicks von einander entfernt sind.

Es ist innerhalb meiner Zeit als Programmierer schon häufiger mal vorgekommen, dass ich CSV Dateien einlesen und verarbeiten musste. Immerhin sind diese Art von Dateien recht simpel aufgebaut und eignen sich bestens für den Datenaustausch zwischen meheren System. Beispielsweise für die nächtliche Übertragung von Produktdaten. Das Ganze klappt eigentlich auch recht simpel. Folgende Schritte sind nötig:

  1. CSV Datei zeilenweise einlesen (je ein Datensatz)
  2. Jede Zeile an ihrem Trennzeichen (bspw. "," oder ";") trennen (Liste an Werten pro Datensatz)
  3. Die Liste an Werten in eine Zeile der Ergebnisliste schreiben
  4. Zurück zu Punkt 1, bis CSV Datei keine neuen Zeilen mehr besitzt

So weit so gut. Allerdings entstehen hier oftmals sehr unstrukturierte Daten, wie im nachfolgenden Bild dargestellt. Wer weiß am Ende denn noch was an Stelle 0-3 für ein Wert stand? War das der Name oder die Nummer?

Darstellung der unstrukturierten Daten

Sinnvoller ist es also die entsprechenden Zeilen jeweils in ein neues Java Objekt zu verpacken. Somit hätten wir eine Liste an Objekten, welche ihrerseits die Werte strukturiert in Attributen halten. Will ich den Namen auslesen, so muss ich mir nicht merken, ob dieser an Stelle 2, 3 oder 65 gestanden hat. Super Idee also!

Leider ist das Aufbereiten dieser Zeilen mit viel Aufwand verbunden, da jede Spalte einer Zeile wiederum den entsprechenden Setter aufrufen muss. Nun gibt es allerdings eine relativ einfache Lösung, welche ich hier einmal vorstellen möchte. - Viel Spaß


Aufbau einer CSV-Datei

Okay, ich gebe zu das war jetzt etwas abstrakt. Vielleicht sollten wir den Aufbau einer CSV Datei erst einmal aufarbeiten und die generellen Begrifflichkeiten klären.
Im allgemeinen ist eine CSV Dateien so aufgebaut:

  • Jede Zeile beschreibt einen Datensatz, welcher in sich geschlossen ist.
  • Jede Spalte ist mit einem bestimmten Trennzeichen voneinander getrennt und beschreibt bestimmte Merkmalsausprägungen.
  • Die Nummer der Spalte bestimmt welches Merkmal in der Spalte beschrieben wird.

Beispielsweise steht in einer beispielhaft angenommenen CSV-Datei an Stelle null immer die Nummer des Produktes. An Stelle fünf der CSV Datei steht immer der Name des Produktes. An Stelle sieben steht jeweils der Preis des Produktes. So könnten wir diese Liste jetzt theoretisch unendlich lang fortführen.
Weist die CSV Datei einen Headerzeile auf so ist diese immer in der ersten Zeile angesiedelt und beschreibt in welcher Spalte welche Merkmalsausprägung zu finden ist. So würde in Zeile 0 und Spalte 0 "ID des Produktes" (Merkmalsbeschreibung) stehen. In Zeile 1 und Spalte 0 steht "76345" (Merkmalswert).

Unser Ziel ist es im folgenden jeden Datensatz einzulesen und jeden Merkmalswert pro Datensatz in sein korrespondierendes Attribut zu schreiben.

Das Ganze habe ich noch einmal in einem kleinen Schaubild demonstrieren wollen :P



CSV-Datei Zeilenweise einlesen

Eingelesen werden soll eine CSV-Datei mit einem Header (die erste Zeile der Datei beschreibt was die einzelnen Spalten beinhalten).
Das Trennzeichen ist dabei ein Komma.

Verwendet wird eine freie Apache Library:

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;


Einlesen und über die Zeilen iterieren:

private List<Product> getListFromCsv() throws IOException, IllegalAccessException {
        Reader in = new FileReader("liste.csv");
        Iterable<CSVRecord> records = CSVFormat.DEFAULT.withDelimiter(',').withHeader().parse(in);
        ArrayList<Product> list = new ArrayList<>();

        for (CSVRecord record : records) {
           // Hier wird jede Zeile durchlaufen.
        }

        return list;
    }


Wie ihr sehen könnt verwende ich zum Einlesen einer CSV oder auch Excel (das geht ebenfalls!!) eine Klasse mit dem Namen "CSVFormat", welche ihrerseits mit dem Befehl "parse(in)" die Datei "liste.csv" einliest.

Bisher können wir so also eine CSV-Datei einlesen und über jede Zeile iterieren. Was jedoch fehlt ist das Einsetzen der entsprechenden Werte pro Spalte in die Attribute eines Java Objektes. Außerdem haben wir bis dato noch kein Java Objekt erstellt und müssten uns noch Gedanken darüber machen wie wir diese Daten ohne großen Mehraufwand in die jeweiligen Attribute mappen. Bevor wir uns jedoch Gedanken darüber machen können wie wir die Attribute befüllen und die entsprechenden Spalten welche den Attributen korrespondierend gegenüberstehen auslesen, sollten wir uns überlegen wie es im Allgemeinen überhaupt funktionieren könnte. Hierzu möchte ich ganz gerne ein kurzes Beispiel zur Verfügung stellen und anschließend demonstrieren wie es viel schneller gehen könnte. Verwenden werden wir  in erster Instanz erst einmal das Beispiel aus dem obigen Bild. In diesem Bild wird demonstriert, dass wir eine CSV Datei einlesen könnten welche aus den folgenden Merkmalen pro Spalte besteht: Nummer des Produktes, Name des Produktes, Preis des Produktes, Kategorie des Produktes
Denkbar wäre selbstverständlich, dass wir nicht nur vier verschiedene Merkmale und damit Attributsausprägungen pro Zeile besitzen, sondern möglicherweise 100 Merkmale und damit 100 Merkmalsausprägungen.

Erst einmal gibt es also hier den Code der Klasse "Produkt":

public class Produkt {

    public String id;
    public String name;
    public String preis;
    public String kategorie;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPreis() {
        return preis;
    }

    public void setPreis(String preis) {
        this.preis = preis;
    }

    public String getKategorie() {
        return kategorie;
    }

    public void setKategorie(String kategorie) {
        this.kategorie = kategorie;
    }
}

Anschließend schauen wir uns das umständliche Beispiel an, da jedes Attribut einzeln befüllt werden muss:

private List<Product> getListFromCsv() throws IOException, IllegalAccessException {
        Reader in = new FileReader("liste.csv");
        Iterable<CSVRecord> records = CSVFormat.DEFAULT.withDelimiter(',').withHeader().parse(in);
        ArrayList<Product> list = new ArrayList<>();

        for (CSVRecord record : records) {
            Product newProduct = new Product();

            newProduct.setId(record.get("id"));
            newProduct.setName(record.get("name"));
            newProduct.setKategorie(record.get("kategorie"));
            newProduct.setPreis(record.get("preis"));

            list.add(newProduct);
        }

        return list;
    }


Und nun kommen wir zum eigentlichen Hauptproblem. Sollten wir eine Klasse oder CSV-Datei vorfinden, welche eben nicht nur vier Attribute enthält sondern vielleicht 10,15 oder 75, so müssen wir jedes Attribut einzeln über seinen Setter befüllen.
Alternativ können wir folgendem Methode verwenden, um alle Attribute der Klasse automatisch mit den Inhalten der Zeile zu füllen:

private List<Product> getListFromCsv() throws IOException, IllegalAccessException {
        Reader in = new FileReader("liste.csv");
        Iterable<CSVRecord> records = CSVFormat.DEFAULT.withDelimiter(',').withHeader().parse(in);
        ArrayList<Product> list = new ArrayList<>();

        for (CSVRecord record : records) {
            Product newProduct = new Product();
            Field[] fields = Product.class.getDeclaredFields();
            for (Field field : fields){
                field.set(newProduct, record.get(field.getName()));
            }
            list.add(newProduct);
        }

        return list;
    }


Einen kleinen Wermutstropfen gibt es jedoch. Die CSV Datei benötigt in jedem Fall ein Headerzeile und alle zu befüllenden Attribute müssen innerhalb der zu generierenden in Klasse auf Public gesetzt sein. Wenn diese Voraussetzungen erfüllt sind können wir mit wenigen Zeilen Code auch mehrere hundert Attribute gleichzeitig befüllen, ohne diese einzeln per Hand einprogrammieren zu müssen.


Marvin

Ich bin ein Mensch, der sich neben der Programmierung noch für tausend andere Dinge interessiert, die mal mehr und mal weniger verrückt sind. Vor allem aber bin ich Feuer und Flamme mit der Programmierung von eigenen kleinen Apps und Programmen, die mein Leben bereichern.

Hat dir dieser Artikel gefallen?

Kommentar hinzufügen

*Pflichtfeld