CSV-zu-XML-Konverter
Wandelt nach XML, Überschriften werden gültige Elementnamen.
Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.
Fügen Sie ein CSV ein, und jede Zeile wird ein Element. Die erste Zeile liefert die Elementnamen, und Sie wählen den Wurzelnamen, den Zeilennamen, das Trennzeichen und ob Werte als Kindelemente oder als Attribute geschrieben werden. Parser, Namensbereiniger und Serialisierer laufen in einem Web Worker in diesem Tab, es wird also nichts hochgeladen.
Man greift danach, wenn eine Tabelle etwas beliefern muss, das nur XML annimmt: ein Massenimport in ein älteres ERP, eine Nutzlast für einen SOAP-Endpunkt, ein Katalogfeed, oder eine Fixture für eine Testsuite, die XML liest. Es ist meist eine Einbahnstraße, und darum zählt es mehr, das Maskieren beim ersten Mal richtig zu machen, als der Rückweg.
Zwei Dinge unterscheiden das von einem Aufteilen an Kommata. Der Parser setzt die RFC 4180 um, also überleben Felder in Anführungszeichen, eingebettete Kommata, eingebettete Zeilenumbrüche und verdoppelte Anführungszeichen alle. Und die zwei Stellen, an denen CSV und XML wirklich uneins sind – Überschriften, die keine zulässigen XML-Namen sind, und Zeilen mit falscher Feldzahl –, werden gemeldet statt hinter Ihrem Rücken still behoben.
An Kommata aufzuteilen ist kein CSV-Parsen
Die RFC 4180 erlaubt, ein Feld in doppelte Anführungszeichen zu setzen, und ein so gesetztes Feld darf Kommata, Zeilenumbrüche und Anführungszeichen enthalten, wobei ein wörtliches Anführungszeichen als zwei geschrieben wird. Ein Konverter, der auf einem Split an Kommata aufbaut, zerlegt alle vier Fälle, und er tut es still: Die Zeilenzahl sieht weiter plausibel aus, der Schaden kommt also erst später heraus, in dem, was das XML verarbeitet hat.
Der Parser behandelt diese Fälle und entscheidet bewusst über die, die die RFC nicht abdeckt, denn echte Exporte sind nicht immer konform. CRLF und einzelne CR werden zu LF normalisiert, ein einzelner Zeilenumbruch am Ende wird verworfen statt eine gespenstische Leerzeile zu erzeugen, und Leerzeilen werden übersprungen. Ein Anführungszeichen öffnet ein Feld nur dann, wenn es dessen erstes Zeichen ist, ein verirrtes Anführungszeichen in einem ungequoteten Wert bleibt also Daten, statt den Rest der Datei aus dem Takt zu bringen.
sku,description,qty,unit price
WID-9,"Widget, large",3,12.50
GRM-2,"Grommet ""heavy duty""",1,27.35
<row>
<sku>GRM-2</sku>
<description>Grommet "heavy duty"</description>
<qty>1</qty>
<unit_price>27.35</unit_price>
</row>Spaltenüberschriften werden Elementnamen, und die meisten können es nicht
XML 1.0 ist streng mit Namen: Ein Name beginnt mit einem Buchstaben, einem Unterstrich oder einem Doppelpunkt und setzt sich dann mit Buchstaben, Ziffern, Bindestrichen, Punkten oder Doppelpunkten fort. Tabellenüberschriften halten sich fast nie daran. „unit price“, „2024 total“ und „price (GBP)“ sind alle unzulässig, und ein Konverter muss mit jeder davon etwas tun.
Jede Überschrift wird geprüft und, wenn sie durchfällt, auf dieselbe Weise bereinigt: Zeichen außerhalb der zulässigen Menge werden Unterstriche, ein Name, der mit einer Ziffer beginnen würde, bekommt einen Unterstrich davor, und ein Name, der in beliebiger Schreibung mit den Buchstaben xml beginnt, ebenfalls, denn XML 1.0 reserviert dieses Präfix. Eine leere Überschrift wird column1, column2 und so weiter.
Jede Umbenennung wird im Panel als „unit price zu unit_price“ aufgeführt, damit Sie sehen, worauf Ihr nachgelagertes XPath passen muss. Diese Liste wird aus einem Grund gedruckt und nicht bloß gezählt: Bereinigen ist nicht umkehrbar, zwei Überschriften, die sich nur in den ersetzten Zeichen unterscheiden, können also auf demselben Namen landen, und XML erlaubt Geschwisterelementen denselben Namen, es gibt also keinen Fehler.
Elemente oder Attribute
Elemente sind die Voreinstellung und meist das Richtige. Attribute sind kompakter und sind, was manche alten Importer erwarten; das Kästchen „Spalten als Attribute“ schreibt jeden Wert als Attribut an einem selbstschließenden Zeilenelement. Die Wurzel- und Zeilennamen, die Sie eingeben, werden genau so verwendet, ohne Bereinigung, halten Sie sie also zulässig.
Der Handel ist in einer Richtung echt. XML 1.0 normalisiert Attributwerte beim Parsen und ersetzt einen wörtlichen Tabulator oder Zeilenumbruch durch ein Leerzeichen, eine mehrzeilige Zelle würde einen Hin- und Rückweg also nicht überleben. Der Serialisierer schreibt Tabulatoren, Wagenrückläufe und Zeilenvorschübe in Attributen deshalb als die Zeichenreferenzen 	, und , die nicht normalisiert werden. Textinhalt braucht diese Behandlung nicht.
Zwei weitere Unterschiede zählen. Ein Attribut kann sich an demselben Element nicht wiederholen, eine doppelte Überschrift erzeugt also ein nicht wohlgeformtes Dokument, und ein leeres Attribut ist von einem fehlenden nicht zu unterscheiden.
Ungleiche Zeilen werden gemeldet, nicht aufgefüllt
Ein CSV, in dem manche Zeilen weniger oder mehr Felder als der Kopf tragen, ist meist ein Symptom: ein nicht maskiertes Trennzeichen, ein kaputter Export, oder zwei aneinandergehängte Dateien. Es still aufzufüllen erzeugt XML, das gut aussieht und falsch ist, deshalb wird stattdessen die Anzahl gemeldet: „4 Zeilen haben eine andere Feldzahl als der Kopf.“
Die Ausgabe wird dennoch erzeugt. Fehlende Werte werden als leere Elemente geschrieben, damit die Datensatzstruktur gleichförmig bleibt, und zusätzliche Werte werden unter erzeugten Namen columnN behalten statt verworfen. Beides bleibt in der Ausgabe sichtbar, und beides bedeutet dasselbe: Sehen Sie sich diese Zeilen in der Quelle an.
Warum der CSV-Parser handgeschrieben ist
Die naheliegende Antwort ist eine Bibliothek, und sie wurde wegen des Gewichts verworfen. Diese Seite verspricht, auf einem Telefon im Hotel-WLAN schnell zu laden, und ein CSV-Parser plus ein YAML-Ausgeber plus ein XML-Bauer legen für etwa achtzig Zeilen Verhalten einige hundert Kilobyte auf jeden Besuch. Der Parser ist eine Zeichenschleife mit einem Boolean für „innerhalb von Anführungszeichen“, und das ist die ganze RFC 4180.
Der Preis ist die Abdeckung von Dialekten. Es gibt kein Erschnüffeln des Trennzeichens über die drei Auswahlmöglichkeiten hinaus, kein anderes Anführungszeichen als das doppelte, keine Konvention für Kommentarzeilen und keine Typermittlung, denn für XML ist jeder CSV-Wert eine Zeichenkette. Benutzt Ihre Datei einen senkrechten Strich als Trennzeichen oder eine Maskierung mit Rückstrich, normalisieren Sie sie vor dem Einfügen.
Das im Code tun
Dieselbe Umwandlung in den Sprachen, die XML verarbeiten. Das Risiko ist hier das Gegenteil der Richtung XML nach CSV: Es wird nichts unsicher geparst, aber alles geschrieben, und ein Wert mit einem Kaufmanns-Und oder einer spitzen Klammer erzeugt ein kaputtes Dokument, wenn er nicht maskiert wird. Jedes Beispiel benutzt einen Writer, der für Sie maskiert, statt Zeichenketten zusammenzufügen.
// No dependencies. The parser is RFC 4180: one loop and one flag.
function parseCsv(text, delimiter = ',') {
const src = text.replace(/\r\n?/g, '\n').replace(/\n$/, '');
const rows = [];
let row = [], field = '', quoted = false;
for (let i = 0; i < src.length; i++) {
const c = src[i];
if (quoted) {
if (c === '"' && src[i + 1] === '"') { field += '"'; i++; }
else if (c === '"') quoted = false;
else field += c;
continue;
}
if (c === '"' && field === '') quoted = true;
else if (c === delimiter) { row.push(field); field = ''; }
else if (c === '\n') { row.push(field); rows.push(row); row = []; field = ''; }
else field += c;
}
if (field !== '' || row.length) { row.push(field); rows.push(row); }
return rows;
}
// XML 1.0 names: start with a letter, underscore or colon; the 'xml'
// prefix is reserved in any case combination.
function xmlName(heading, index) {
const base = heading.trim() || 'column' + (index + 1);
if (/^[A-Za-z_][\w.\-]*$/.test(base)) return base;
let name = base.replace(/[^\w.\-]/g, '_');
if (!/^[A-Za-z_]/.test(name)) name = '_' + name;
if (/^xml/i.test(name)) name = '_' + name;
return name;
}
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
function csvToXml(text, { root = 'rows', row = 'row', delimiter = ',' } = {}) {
const rows = parseCsv(text, delimiter);
const headers = rows[0].map(xmlName);
const out = ['<?xml version="1.0" encoding="UTF-8"?>', '<' + root + '>'];
for (const r of rows.slice(1)) {
if (r.length === 1 && r[0] === '') continue; // blank line
if (r.length !== headers.length) {
console.warn('row has ' + r.length + ' fields, header has ' + headers.length);
}
out.push(' <' + row + '>');
headers.forEach((h, i) => out.push(' <' + h + '>' + esc(r[i] ?? '') + '</' + h + '>'));
out.push(' </' + row + '>');
}
out.push('</' + root + '>');
return out.join('\n');
}# Standard library only. csv handles RFC 4180 and ElementTree escapes
# the output, so neither quoting nor & and < are your problem.
import csv
import re
import sys
import xml.etree.ElementTree as ET
NAME_OK = re.compile(r"^[A-Za-z_][\w.\-]*$")
def xml_name(heading, index):
base = heading.strip() or f"column{index + 1}"
if NAME_OK.match(base):
return base
name = re.sub(r"[^\w.\-]", "_", base)
if not re.match(r"^[A-Za-z_]", name):
name = "_" + name
if name[:3].lower() == "xml": # reserved by XML 1.0 section 2.3
name = "_" + name
return name
def csv_to_xml(path, root_name="rows", row_name="row", delimiter=","):
# newline="" lets csv handle line breaks inside quoted fields itself.
with open(path, newline="", encoding="utf-8-sig") as f:
rows = list(csv.reader(f, delimiter=delimiter))
headers = [xml_name(h, i) for i, h in enumerate(rows[0])]
root = ET.Element(root_name)
for r in rows[1:]:
if not any(field.strip() for field in r):
continue
if len(r) != len(headers):
print(f"row has {len(r)} fields, header has {len(headers)}", file=sys.stderr)
row = ET.SubElement(root, row_name)
for i, name in enumerate(headers):
ET.SubElement(row, name).text = r[i] if i < len(r) else ""
tree = ET.ElementTree(root)
ET.indent(tree, space=" ") # Python 3.9+
tree.write(sys.stdout.buffer, encoding="UTF-8", xml_declaration=True)
csv_to_xml(sys.argv[1])// Apache Commons CSV for RFC 4180 parsing, StAX for escaped output.
// <dependency>org.apache.commons:commons-csv:1.11.0</dependency>
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import javax.xml.stream.*;
import org.apache.commons.csv.*;
public class CsvToXml {
static String xmlName(String heading, int index) {
String base = heading == null ? "" : heading.trim();
if (base.isEmpty()) return "column" + (index + 1);
if (base.matches("[A-Za-z_][\\w.\\-]*")) return base;
String name = base.replaceAll("[^\\w.\\-]", "_");
if (!name.matches("^[A-Za-z_].*")) name = "_" + name;
if (name.regionMatches(true, 0, "xml", 0, 3)) name = "_" + name;
return name;
}
public static void main(String[] args) throws Exception {
CSVFormat format = CSVFormat.RFC4180.builder()
.setHeader().setSkipHeaderRecord(true).build();
try (Reader in = Files.newBufferedReader(Path.of(args[0]), StandardCharsets.UTF_8);
CSVParser parser = CSVParser.parse(in, format)) {
List<String> raw = parser.getHeaderNames();
List<String> names = new ArrayList<>();
for (int i = 0; i < raw.size(); i++) names.add(xmlName(raw.get(i), i));
XMLStreamWriter out = XMLOutputFactory.newInstance()
.createXMLStreamWriter(new OutputStreamWriter(
new FileOutputStream("out.xml"), StandardCharsets.UTF_8));
out.writeStartDocument("UTF-8", "1.0");
out.writeStartElement("rows");
for (CSVRecord record : parser) {
if (record.size() != names.size()) {
System.err.printf("row %d has %d fields, header has %d%n",
record.getRecordNumber(), record.size(), names.size());
}
out.writeStartElement("row");
for (int i = 0; i < names.size(); i++) {
out.writeStartElement(names.get(i));
// writeCharacters escapes &, < and >. Never concatenate.
out.writeCharacters(i < record.size() ? record.get(i) : "");
out.writeEndElement();
}
out.writeEndElement();
}
out.writeEndElement();
out.writeEndDocument();
out.close();
}
}
}// dotnet add package CsvHelper
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using CsvHelper;
using CsvHelper.Configuration;
static string XmlName(string heading, int index)
{
var b = (heading ?? "").Trim();
if (b.Length == 0) return "column" + (index + 1);
if (Regex.IsMatch(b, @"^[A-Za-z_][\w.\-]*$")) return b;
var name = Regex.Replace(b, @"[^\w.\-]", "_");
if (!Regex.IsMatch(name, @"^[A-Za-z_]")) name = "_" + name;
if (name.StartsWith("xml", StringComparison.OrdinalIgnoreCase)) name = "_" + name;
return name;
}
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
Delimiter = ",",
HasHeaderRecord = true,
BadDataFound = ctx => Console.Error.WriteLine($"bad quoting on row {ctx.RawRecord}"),
};
using var reader = new StreamReader(args[0], Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
using var csv = new CsvReader(reader, config);
csv.Read();
csv.ReadHeader();
var names = csv.HeaderRecord!.Select(XmlName).ToArray();
var settings = new XmlWriterSettings
{
Indent = true,
Encoding = new UTF8Encoding(false),
// Entitize is the default and is what keeps a newline inside an
// attribute value from being normalised away on the next parse.
NewLineHandling = NewLineHandling.Entitize,
};
using var writer = XmlWriter.Create("out.xml", settings);
writer.WriteStartDocument();
writer.WriteStartElement("rows");
while (csv.Read())
{
writer.WriteStartElement("row");
for (var i = 0; i < names.Length; i++)
{
// WriteElementString escapes the value for you.
writer.WriteElementString(names[i], csv.TryGetField<string>(i, out var v) ? v : "");
}
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteEndDocument();<?php
// fgetcsv implements RFC 4180 quoting, and DOMDocument escapes text nodes,
// so the two halves that usually break are both handled for you.
function xml_name(string $heading, int $index): string {
$base = trim($heading);
if ($base === '') return 'column' . ($index + 1);
if (preg_match('/^[A-Za-z_][\w.\-]*$/', $base)) return $base;
$name = preg_replace('/[^\w.\-]/', '_', $base);
if (!preg_match('/^[A-Za-z_]/', $name)) $name = '_' . $name;
if (stripos($name, 'xml') === 0) $name = '_' . $name;
return $name;
}
$handle = fopen($argv[1], 'r');
$header = fgetcsv($handle);
// Strip a UTF-8 BOM from the first heading; Excel writes one.
$header[0] = preg_replace('/^\xEF\xBB\xBF/', '', $header[0]);
$names = [];
foreach ($header as $i => $h) $names[] = xml_name($h, $i);
$doc = new DOMDocument('1.0', 'UTF-8');
$doc->formatOutput = true;
$root = $doc->createElement('rows');
$doc->appendChild($root);
$line = 1;
while (($fields = fgetcsv($handle)) !== false) {
$line++;
if ($fields === [null] || $fields === ['']) continue; // blank line
if (count($fields) !== count($names)) {
fprintf(STDERR, "line %d has %d fields, header has %d\n",
$line, count($fields), count($names));
}
$row = $doc->createElement('row');
foreach ($names as $i => $name) {
// createTextNode escapes; createElement($name, $value) does not.
$el = $doc->createElement($name);
$el->appendChild($doc->createTextNode($fields[$i] ?? ''));
$row->appendChild($el);
}
$root->appendChild($row);
}
fclose($handle);
echo $doc->saveXML();# Import-Csv parses RFC 4180 quoting, including embedded newlines.
# ConvertTo-Xml escapes the values.
Import-Csv -Path .\data.csv -Delimiter ',' |
ConvertTo-Xml -As String -NoTypeInformation |
Out-File -FilePath .\out.xml -Encoding utf8
# Note the shape it produces. Headings become Name attributes, not
# element names, so nothing needs sanitising and nothing round-trips
# into the schema you probably wanted:
#
# <Objects>
# <Object>
# <Property Name="unit price">12.50</Property>
# </Object>
# </Objects>
#
# For heading-as-element-name output, build it explicitly:
$rows = Import-Csv -Path .\data.csv
$doc = New-Object System.Xml.XmlDocument
$root = $doc.AppendChild($doc.CreateElement('rows'))
foreach ($r in $rows) {
$row = $root.AppendChild($doc.CreateElement('row'))
foreach ($p in $r.PSObject.Properties) {
$name = $p.Name -replace '[^\w.\-]', '_'
if ($name -notmatch '^[A-Za-z_]') { $name = "_$name" }
$el = $row.AppendChild($doc.CreateElement($name))
$el.InnerText = $p.Value # InnerText escapes, InnerXml does not
}
}
$doc.Save((Join-Path $PWD 'out.xml'))Die Regel, die alle sechs teilen: Lassen Sie den Writer maskieren. XML durch Aneinanderhängen von Zeichenketten zu bauen funktioniert bis zum ersten Lieferantennamen mit einem Kaufmanns-Und und erzeugt dann ein Dokument, das kein Parser annimmt.
Häufige Fragen
Wird mein CSV irgendwohin hochgeladen?
Nein. CSV-Parser, Namensbereiniger und XML-Serialisierer laufen in einem Web Worker in diesem Tab, und es gibt kein Backend, an das etwas gehen könnte. Öffnen Sie Ihr Netzwerkpanel und wandeln Sie etwas um: Die Seitenassets laden einmal, danach nichts.
CSV ist das Format, das die heiklen Exporte trägt: Eine Tabelle, die in einen Konverter eingefügt wird, ist typischerweise eine Kundenliste, ein Lohnauszug oder eine Preisdatei unter Geheimhaltung. Ihre Eingabe wird im localStorage dieses Browsers gehalten, damit ein Neuladen sie nicht verliert, und „Leeren“ entfernt sie.
Was passiert mit einer Spaltenüberschrift wie „unit price“ oder „2024 total“?
Beide sind unzulässige XML-Elementnamen, beide werden also bereinigt. Zeichen außerhalb der zulässigen Menge werden Unterstriche, was unit_price ergibt; ein Name, der mit einer Ziffer beginnen würde, bekommt einen Unterstrich davor, was _2024_total ergibt. Eine Überschrift, die in beliebiger Schreibung mit den Buchstaben xml beginnt, bekommt ebenfalls einen, denn XML 1.0 reserviert dieses Präfix.
Jede Umbenennung steht im Ergebnis-Panel mit dem Original neben dem neuen Namen, damit Sie die echten Elementnamen in das XPath oder XSD kopieren können, das die Datei verarbeitet. Wollen Sie die Namen selbst wählen, bearbeiten Sie die Kopfzeile vor dem Umwandeln.
Meine Werte enthalten Kommata. Stimmen die Spalten trotzdem?
Ja, solange diese Werte in Anführungszeichen stehen, was jeder Tabellen- oder Datenbankexport automatisch tut. Der Parser folgt der RFC 4180: Ein Feld in Anführungszeichen darf Kommata, Zeilenumbrüche und Anführungszeichen enthalten, und zwei Anführungszeichen hintereinander darin bedeuten ein wörtliches Anführungszeichen.
Ein Zeilenumbruch in einem gequoteten Feld bleibt erhalten, eine mehrzeilige Adresse bleibt also ein Wert. Unter „Spalten als Attribute“ wird er als geschrieben statt als wörtlicher Umbruch, denn XML normalisiert wörtliche Umbrüche in Attributwerten zu Leerzeichen, und die Zeichenreferenz ist die einzige Form, die überlebt.
Was, wenn manche Zeilen mehr oder weniger Felder als der Kopf haben?
Man sagt es Ihnen. Das Panel meldet, wie viele Zeilen nicht zur Kopfbreite passen, denn ein ungleiches CSV heißt fast immer, dass stromaufwärts etwas kaputt ist: ein nicht maskiertes Trennzeichen, ein abgeschnittener Export, oder zwei aneinandergehängte Dateien.
Die Umwandlung läuft trotzdem. Fehlende Werte werden als leere Elemente geschrieben, damit jeder Datensatz dieselbe Gestalt behält, und zusätzliche Werte werden unter erzeugten Namen columnN behalten statt verworfen. Beides lässt die Auffälligkeit in der Ausgabe sichtbar, statt sie zu verstecken.
Sollen die Werte Elemente oder Attribute sein?
Elemente, es sei denn, etwas Nachgelagertes verlangt Attribute. Elemente können sich wiederholen, mehrzeiligen Text ohne Kodierungstricks tragen, lassen sich in einem XSD leichter einschränken und unterscheiden einen leeren Wert von einem fehlenden. Attribute sind kompakter und sind, was manche älteren Importer erwarten.
Wenn Sie umschalten: Eine doppelte Überschrift wird ein doppeltes Attribut an demselben Element, und das ist kein wohlgeformtes XML. Elemente haben diese Einschränkung nicht.
Mein Export benutzt Semikolons, keine Kommata. Geht das?
Ja. Stellen Sie die Trennzeichen-Einstellung auf Semikolon. Excel schreibt in den meisten europäischen Locales semikolongetrennte Dateien und nennt sie weiterhin CSV, weil dort das Komma das Dezimaltrennzeichen ist, und ein Konverter, der Kommata annimmt, macht aus 12,50 zwei Spalten.
Tabulatorgetrennte Dateien gehen genauso. Ein senkrechter Strich oder ein Zirkumflex als Trennzeichen wird nicht angeboten und braucht vorher ein Suchen und Ersetzen. Eine UTF-8-Byte-Order-Mark, die Excel am Dateianfang schreibt, wird von der ersten Überschrift abgeschnitten, statt Teil des Elementnamens zu werden.