XML to CSV Converter
Turn repeating XML records into CSV, TSV or an .xlsx workbook — you pick the row element — entirely on this device.
Press ? for shortcuts
Do it offline:$ python3 -c "import xml.etree.ElementTree as E,csv,sys;w=csv.writer(sys.stdout);w.writerow(['id','title']);[w.writerow([r.get('id'),r.findtext('title')]) for r in E.parse(sys.argv[1]).getroot().iter('book')]" books.xml
How to use it
- Paste XML or drop a file — a chip names the record element that was detected and how many rows it yields.
- Disagree? The record menu lists every repeating path with its row count; pinning one replaces the detected chip.
- Choose whether attributes become columns and what a repeated child does — keep the first, join them, or number them.
- Set delimiter, header row, quoting, BOM and line endings, then copy the text or download .csv, .tsv or .xlsx.
A spreadsheet wants a rectangle; XML gives you a tree. So this converter settles one question first — which repeating element is a row — then names every column after its path below that element: @id for an attribute, addr/city for a nested leaf. Repeated children, mixed text and missing values each have a stated rule, and the lossy ones say so. Nothing you paste leaves this device.
Examples
<library>
<book id="1">
<title>Refactoring</title>
<author>Fowler</author>
<tag>design</tag>
<tag>patterns</tag>
</book>
<book id="2">
<title>Design Patterns: Elements, Reused</title>
<author>Gamma</author>
<tag>patterns</tag>
</book>
</library>@id,title,author,tag
1,Refactoring,Fowler,design; patterns
2,"Design Patterns: Elements, Reused",Gamma,patternslibrary/book is detected because it repeats and carries fields. The two tag children collapse into one cell joined with "; ", and a warning names the path. Only the title holding a comma is quoted — RFC 4180 minimal quoting.
<notes>
<note id="a">Ship <b>today</b> please</note>
<note id="b">Hold</note>
</notes>@id,b,#text
a,today,
b,,HoldMixed content: the words around <b> sit beside a child element, so they get no column and are dropped with a warning. The second record has no child, so its text lands in the record's own #text column instead.
<orders>
<order id="1001">
<line sku="A-1"><qty>2</qty></line>
<line sku="B-7"><qty>1</qty></line>
</order>
<order id="1002">
<line sku="C-3"><qty>5</qty></line>
</order>
</orders>@id,line/@sku,line/qty
1001,A-1; B-7,2; 1
1002,C-3,5Detection picks orders/order/line — three line items outrank two orders — so this pins orders/order: one row per order, and the repeated line children collapse into line/@sku and line/qty under the join policy. Warnings name both.
<hosts>
<host name="web-1" ip="10.0.0.4" role="edge"/>
<host name="db-1" ip="10.0.0.9"/>
</hosts>@name,@ip,@role
web-1,10.0.0.4,edge
db-1,10.0.0.9,Empty elements with attributes only still make a full table. Column order is first appearance across the record set, so @role keeps the third slot the second host never fills; that gap becomes an empty cell, counted by a chip.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Which element becomes a row
A path qualifies when it occurs at least twice and at least one occurrence has an attribute or a child element. Qualifying paths rank by count, then shallower depth, then alphabetically, and rank one is the pick — which is why three line items inside two orders propose the line.
A repeated child has no honest single answer
Two <tag> children cannot both own one cell, so you choose: keep the first, join them with a separator you set, or mint tag[1], tag[2] … columns up to 32 before the tail joins into the last one. Every policy warns and names the path, because each one changes what the file means.
Entity references reach the cell as written
The parser never expands entities, so a cell holding Tom & Jerry arrives in the CSV as the five characters & — not an ampersand. Character references such as A behave the same way, and no external entity is ever fetched.
Namespace prefixes travel; URIs do not
A d:item element with a d:key attribute yields the header @d:key verbatim. Nothing is resolved against the xmlns declaration and no xmlns attribute becomes a column, so two prefixes bound to one URI stay two column names. A note says so whenever prefixes appear.
Whitespace at the edges, line ends inside
Whitespace around a value is trimmed, so an indented element does not carry your indentation into the spreadsheet, while runs between words survive untouched. A CRLF or a lone CR inside a value becomes one LF, as XML 1.0 §2.11 requires of every parser.
The workbook is text, deliberately
Excel export runs with type detection off, so 00501 stays 00501 and a long order number is never rewritten in scientific notation. One sheet, no formatting, no formulas; the .xlsx bytes are assembled on this device when you press download.
Common errors
No repeating element to turn into rows — every element occurs once ("invoice", "invoice/number", "invoice/total"). A table needs an element that occurs at least twice; pick a different document, or convert a one-off document with the XML to JSON converter.- One invoice is not a table. The paths it did see are listed, so a truly single-record document is easy to tell from a file whose repeating part sits deeper than expected.
No element matches the record path "catalog/item" — the paths seen here are "catalog", "catalog/product", "catalog/product/name". Pick one of the detected paths, or clear the field to have the record element detected.- A pinned path is matched literally against the whole path from the root, prefix included. Three known paths come back, so a typo is one click from a fix.
Record "export/row" at line 1, column 9 pushes the table past the 2,048-column limit for this tool — pick a deeper record element, or turn attributes off.- Column count is measured across the whole record set, so one unusually wide record can trip it. Both suggested fixes shrink columns rather than rows — the direction that helps a spreadsheet.
Closing tag '</item>' at line 4, column 3 — '<product>' opened at line 2, column 3 is still open; did you forget '</product>'?- Well-formedness is checked before any flattening, and the tokenizer's wording is shown unedited. Both positions are given because the mistake usually lives at the opening tag, not where the parser noticed.
FAQ
How does it decide which element is a row?
It takes the repeating path with the most occurrences, counting only paths that carry attributes or child elements and breaking ties toward the shallower one. The pick is always shown with its row count and always overridable.
Why did some of my XML not make it into the CSV?
Four things are dropped by design: comments, processing instructions, the DOCTYPE and text sitting beside child elements. The first three are markup, not data; the fourth has nowhere rectangular to go and warns with the record path, so the loss is never silent.
Can I get one row per order and keep the line items?
Not in one table — that is two shapes. Pin the order and let the repeated-child policy join or number the line item values, or pin the line and join the two exports on a shared identifier afterwards.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Convert the XML
- ⌘/Ctrl + S
- Download the pinned target
- ⌘/Ctrl + C
- Copy the output
- ⌘/Ctrl + Shift + C
- Force-copy over a selection
- t
- Switch the theme
- ? or ⌘/Ctrl + /
- Open shortcut help
- Esc
- Close help or the record menu