JavaScript Node JS Service zum Konvertieren von CSV zu XML

coder_lui

Banned
Registriert
Apr. 2022
Beiträge
7
Hallo zusammen,

ich bin gerade im Rahmen eines privaten Projektes dabei einen Node JS Service zu bauen, welcher mir CSV-Dateien einliest, verarbeitet, konvertiert und letztlich als XML ausgibt. Das grobe Konstrukt steht in einer ersten Version. Ich habe einen Express-Server gebaut, der über einen POST-Request angesprochen werden kann. Die im Body mitgegebene CSV wird eingelesen. Daraus wird ein Array aus JSON-Objekten gebaut, welcher im letzten Schritt in eine XML konvertiert wird.

1650719228609.png


Soweit so gut, leider ist die XML-Struktur die am Ende rauskommen soll m.E. etwas komplex und bringt ein paar Schwierigkeiten mit sich. Im Folgenden habe ich eine stark abstrahierte Form des Inputs und gewünschten Outputs dargestellt, welcher das Problem, welches ich habe, verdeutlichen soll.

XML:
<!-- CSV Input
Name,Strasse,Hausnummer,Postleitzahl
Mueller,Baumweg,3,34553
Maier,Marktstrasse,23462
-->

<!-- XML Output -->
<schule>
    <allgemeineinfos>
        <name>Waldschule</name>
        <schulart>Gymnasium</schulart>
    </allgemeineinfos>
    <allelehrer>
        <lehrer>
            <name>Mueller</name>
            <adresse>
                <strasse>Baumweg</strasse>
                <hausnummer>3</hausnummer>
                <plz>34553</plz>
            </adresse>
        </lehrer>
        <lehrer>
            <name>Maier</name>
            <adresse>
                <strasse>Marktstrasse</strasse>
                <plz>23462</plz>
            </adresse>
        </lehrer>
    </allelehrer>
</schule>

Ich tue mir aktuell dabei schwer eine Lösung zu finden, wie ich die einfach strukturierten CSV-Datensätze in eine "Nested" XML-Struktur einbauen kann. Des Weiteren möchte ich in die finale XML nicht nur Informationen aus der CSV einbauen, sondern auch Daten aus anderen Quellen, die mir im Code vorliegen (z.B. der Tag <allgemeineinfos>). Außerdem kann es sein, dass Informationen, wie z.B. die zweite Hausnummer in der CSV fehlen, dann soll der XML Tag entfernt werden. Zusammengefasst reicht eine 1:1 Konvertierung nicht aus (sonst könnte ich auch Online-Konverter nehmen :D).

Leider habe ich nicht wirklich einen Ansprechpartner/in, welcher mir weiter helfen könnte. Deshalb würde ich mich freuen wenn sich hier jemand findet, der mehr Erfahrung hat als ich und mir sagen könnte wie ich solche Probleme angehen kann. Vielleicht gibt es Packages die dafür geeignet sind oder öffentlich zugängliche Projekte in GitHub, Stackblitz, o.Ä., leider finde ich aktuell nicht wirklich was.

Beste Grüße und Danke schon mal!
 
Da wird es wohl nichts geben, da sowas unique problems sind und man nur für jeden Fall was zusammenzimmern kann.

CSV: Matrix. JSON und XML: Hierarchische Struktur.

Man kann natürlich hergehen und JSON/XML die triviale Matrix abbilden lassen, aber entlang Deines Beispiels gehe ich davon aus, daß Du sowas nicht suchst.

Du müßtest also Deinen (CSV-) Parser so erweitern, daß dieser jede Eingabezeile in eine Objektstruktur stopft - das muß ja nicht unbedingt exakt "ein" Zielobjekt sein (dann brauchst Du aber eindeutige IDs, die die Eingabe nicht mitbringt => automatisch und eineindeutig erzeugen, eg. als GUID).

Und diese "Ziel-Struktur" müßte dann Deine gewünschte Ergebnisform implementieren. Hier wäre das, wenn ich das richtig sehe, ein "Schule"-Objekt mit den passenden Eigenschaften (und ggfs. Methoden).

Da die Ausgabe nicht 1:1 auf die Eingabe abbildet, wirst Du auch nicht einfach zeilenweise die Ausgabe schreiben können, sondern müßtest erst die CSV vollständig lesen, bevor mit der Ausgabe begonnen werden kann.

Note - nicht so sehr in die Idee mit "Tags weglassen wenn leer" verrennen, bevor nicht alles andere schon klar ist. Das wäre nochmal zusätzlicher Extraaufwand und Matrix in Hierarchie überführen ist schon vergleichsweise aufwendig.
 
Die Struktur sieht doch schon mal gut aus.

Ob du aus der Adresse im XML noch ein Unterelement, wie in deinem Beispiel, machen solltest, hängt davon ab ob du das für die Adressbestandteile fest einprogrammieren willst.

Was suchst du am Ende genau? Eine Bibliothek die dir das komplett fertig macht oder nur eine Bibliothek die dich beim Erzeugen des XML unterstützt.

Am Ende musst du nur dein basis XML erzeugen, mit den allgemeinen Information füllen, fügst dein Listenelement ("alleLehrer") hinzu und gehst ein einer Schleife über deine CSV Datensätzte und machst aus denen immer ein Listenelement ("lehrer").
 
  • Gefällt mir
Reaktionen: coder_lui
just for fun in python :)

Python:
import io
import csv
import flask
from lxml import etree


def subelement(parent, tag, text=None):
    element = etree.SubElement(parent, tag)
    element.text = text
    return element


def remove_nodes_with_empty_text(xml):
    for child in xml:
        if len(child):
            remove_nodes_with_empty_text(child)
        else:
            if not child.text:
                child.getparent().remove(child)


class Server(object):

    def __init__(self):

        self.host = '0.0.0.0'
        self.port = 8080

    def run(self):
        app = flask.Flask('csv2xml')

        @app.route('/csv2xml', methods=['POST'])
        def csv2xml():
            reader = csv.DictReader(
                io.StringIO(flask.request.data.decode())
            )

            xml = etree.Element("schule")

            allgemeineinfos = subelement(xml, "allgemeineinfos")
            subelement(allgemeineinfos, "name", "Waldschule")
            subelement(allgemeineinfos, "schulart", "Gymnasium")

            allelehrer = subelement(xml, "allelehrer")

            for row in reader:
                lehrer = subelement(allelehrer, "lehrer")
                subelement(lehrer, "name", row["Name"])

                adresse = subelement(lehrer, "adresse")
                subelement(adresse, "strasse", row["Strasse"])
                subelement(adresse, "hausnummer", row["Hausnummer"])
                subelement(adresse, "plz", row["Postleitzahl"])

            remove_nodes_with_empty_text(xml)

            response = flask.make_response(etree.tostring(xml, pretty_print=True, encoding=str))
            response.content_type = "text/xml"
            return response

        app.run(host=self.host, port=self.port, threaded=True)


if __name__ == '__main__':
    Server().run()
 

Anhänge

  • Screenshot from 2022-04-23 16-07-00.png
    Screenshot from 2022-04-23 16-07-00.png
    138,8 KB · Aufrufe: 210
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: playerthreeone, coder_lui und C:\Defuse_Kit
Erstmal danke euch drei für die schnellen Antworten!

@dasbene Ich suche nach irgendeiner Bibliothek, die mich bei dem Vorhaben unterstützt. Dein Vorschlag mit dem "Zuerst die XML bauen und dann befüllen" finde ich gut und wenn ich richtig sehe (Pyhton kann ich leider nicht) hat das @0x8100 ähnlich umgesetzt (Respekt übrigens, diese Skills hätt ich auch gerne :D)
Ich habe mittlerweile eine npm-Library gefunden, mit der ich XML's aufbauen kann. Habe eine erste grobe Struktur erstellt und bin jetzt gerade beim dynamischen Befüllen. Mal schauen wie das so klappt, würde mich höchstwahrscheinlich nochmal mit ein zwei Fragen melden je nachdem wie es läuft :D
 
@coder_lui Welche library verwendest du? Ich habe xmlbuilder verwendet. Die ist etwas tricky, aber sobald man das mit dem '.up()' verstanden hat geht das ganz gut.

Darüber hinaus, in deinem Beispiel CSV hast du das Feld mit der einen Hausnummer weggelassen. Falls du in einer CSV ein Feld weglässt musst du trotzdem das Trennzeichen mit angeben. Es wird also effektiv ein leerer String.

Javascript:
// npm init
// add "type": "module" to package.json

// npm i express
import express from 'express'

// npm i xmlbuilder
import builder from 'xmlbuilder'

// npm i csv-parse
import { parse } from 'csv-parse/sync';

const app = express()
const port = 3000

app.use(express.text())

app.post('/converter', (req, res) => {
    // read CSV file or get request body
    const requestBody = req.body;
    console.log(requestBody)

    // parse CSV into JSON Array with header row as keys
    const entriesCSV = parse(requestBody, {
        delimiter: ',',
        trim: true,
        columns: true
    });
    console.log(JSON.stringify(entriesCSV))

    // create XML
    var xmlBuilder = builder.create('schule')
        .ele('allgemeineinfos')
        .ele('name', {}, 'Waldschule').up()
        .ele('schulart', {}, 'Gymnasium').up()
        .up()
        .ele('allelehrer');

    entriesCSV.forEach(entry => {
        xmlBuilder = xmlBuilder.ele('lehrer')
            .ele('Name', {}, entry.Name).up()
            .ele('Adresse')
            .ele('Strasse', {}, entry.Strasse).up()
            .ele('Hausnummer', {}, entry.Hausnummer).up()
            .ele('Postleitzahl', {}, entry.Postleitzahl).up()
            .up().up();
    });

    xmlBuilder = xmlBuilder.end({ pretty: true });

    res.type('xml')
    res.send(xmlBuilder)
})

app.listen(port, () => {
    console.log(`Converter app listening on port ${port}`)
})
 
  • Gefällt mir
Reaktionen: coder_lui
@dasbene
Erstmal vielen Dank für die Mühe, bin gerade echt begeistert wie nett mir hier geholfen wird :)

Bzgl. Library, ja ich habe xmlbuilder2 verwendet, daher passt das schonmal.
Das mit der Hausnummer ist mein Fehler, da hast du Recht, ist mir vorhin auch noch aufgefallen, pass ich direkt mal an.

Deinen Code schaue ich mir morgen mal im Detail an, hab heute leider keine Zeit mehr. Aber schonmal danke & ich melde mich nochmal!
 
Haha, ich bleibe jetzt erstmal bei npm js ;)

@dasbene Ich habe mir deinen Code angeschaut und mit meinem verglichen. Das Auslesen der CSV ist bei dir deutlich eleganter. Das habe ich direkt mal übernommen. Meine Logik, an der ich auf Basis deiner ersten Antwort Mittags rumgebastelt habe, kommt deiner, die du Abends dann geschickt hast, sehr nahe. Danke für's Schicken, da konnte ich dann bei meinen Bugs schön vergleichen, das war echt praktisch :)

Eine Rückfrage hätte ich aber noch zum Code: Du hast beim CSV-Parser folgenden Kommentar: "// read CSV file or get request body". Das Einlesen klappt bei mir aber "nur", wenn ich bei z.B. Postman den Content-Type "text/plain" wähle und nicht text/csv oder dateiorientierte Typen. Das mit text/plain, also Request Body auslesen reicht mir eigentlich. Dennoch würde mich interessieren ob ich den "CSV-Request" falsch aufbaue oder ob der Code-Snippet überhaupt auch text/csv bzw. Dateien im Body auslesen kann?
 
Das liegt an Express. Man benötigt einen Body Parser. In meinem Beispiel ('app.use(express.text())') habe ich einen Text Parser genommen. Da wird es vermutlich auch etwas für CSVs geben.
 
Hallo zusammen,

das Entwickeln läuft soweit ganz gut und ich kann nun erfolgreich meine XML generieren und im Response zurückschicken. D.h. die Server2Server-Seite ist abgedeckt. Danke nochmal für die Hilfe! :)

Nun stehe ich aber vor einer neuen Herausforderung und hätte mal wieder eine Frage :D Ich habe zusätzlich zum Backend ein Angular-Projekt als Frontend aufgesetzt, bei dem ich manuell meine Daten für in die XML-Knoten eingeben kann. Diese werden als JSON an den existierenden Backend-Server geschickt, welcher diese wiederum nach dem oben diskutierten Prinzip verarbeitet und genau so eine XML generiert.
Soweit so gut. Jetzt ist mir aber aufgefallen, dass Angular als Response Type nur JSON, ArrayBuffer, Blob, Text akzeptiert. Ich kann zwar "Text" wählen, dann sieht man als Client die XML in z.B. der Konsole, aber ich hätte schon gerne das ich am Ende eine .xml-Datei und keine .txt erhalte, die auch heruntergeladen werden kann. Nun frage ich mich, ob über einen dieser vier Typen die XML so übertragen werden kann, sodass ich später einen solchen Download zur Verfügung stellen kann?
Meine aktuelle "beste" Idee wäre, dass ich im Backend statt des XML ein z.B. Array von JSON-Objekten baue, die dann ans Frontend schicke und daraus dort dann das XML baue. Das würde ich selbstverständlich gerne vermeiden, da ich dann nicht meinen eigentlichen XML-Generator im Backend nutzen kann.

Wie immer würde ich mich sehr über eure Tipps freuen - Danke :)

1651067287756.png
 
Zuletzt bearbeitet:
Lösung: Ich konnte die Datei als Blob seitens Angular empfangen und daraus mit dem Package File-Saver direkt im HTTP-Service eine .xml-Datei als Download generieren.
 
Zurück
Oben