LlmFiles
Converter to Markdown
LlmFiles/Guides/CSV to Markdown

CSV to Markdown

CSV is the simplest input a converter can get, and the one that breaks most quietly: there is no markup to argue about, so every defect shows up as a row that changed shape without saying anything. Below is what LlmFiles returns for real .csv files — the values that survive untouched, the four ways a table still gets distorted, and how a file that is not UTF-8 is read.

Updated 7 Nov 2026 · LlmFiles · Russian version

The whole pipeline, in one paragraph

The file is decoded, split on commas with Python's csv module, and printed as a pipe table: row 1 becomes the header, a separator line follows, every other row is one line of cells. No dialect sniffing, no type inference, no row cap. A four-row file in, five lines out:

| region | quarter | revenue | orders |
| --- | --- | --- | --- |
| EMEA | Q1 | 48200 | 340 |
| APAC | Q1 | 12700 | 96 |
| Americas | Q1 | 91450 | 512 |

Three consequences follow, and they explain almost everything on this page: the first row is always taken as the header, every value is a string, and every row is bent to the width of that first row.

Values arrive exactly as written

A CSV is not opened by a spreadsheet engine here, so nothing is cast, normalised or reformatted. Measured on a file built to catch exactly that:

Value in the CSVIn the MarkdownWhat a spreadsheet reader would do
007, a product code0077 — leading zeros lost
$48,200.00$48,200.0048200 — currency gone
9.0% and 0.09both unchanged0.09 for both
2026-03-14, 14/03/2026, 2026-3-4three different strings, unchangednormalised to one format, or to a number
true, TRUE, falseunchangedtrue / false
empty cellempty cellstays empty; a parser that casts invents NaN

That is why CSV output from this converter is safe to hand to a parser: nothing was guessed, nothing was lost. Only decoration could be lost, and a CSV has none. For the opposite case, XLSX to Markdown lists every value a spreadsheet engine rewrites on the way out.

Pipes and line breaks come out escaped

Two characters are legal inside a CSV field and both would break a pipe table: | is the cell separator, a newline is the end of the row. A quoted field may hold either, so the converter escapes instead of dropping: | becomes \| and a line break becomes <br>. Measured on a file with both in the third column:

| id | format | note |
| --- | --- | --- |
| 1 | PDF\|DOCX\|XLSX | ok |
| 2 | csv | step one<br>step two |
| 3 | md | plain |

Every row still has three cells, and the value is intact rather than split. On a 300-record export where 200 of the notes contained one of the two characters, the output was 302 lines — header, separator, 300 records — with 100 escaped pipes, 100 <br>, and 0 rows whose cell count differed from the header. Before this rule the same file came out as 402 lines with every record shifted.

One caveat, and it is on the reading side: a tool that splits rows on | without respecting the backslash will still see three cells in PDF\|DOCX\|XLSX. Any Markdown renderer handles it; a hand-written line.split('|') does not.

Rows that don't match the header

The header decides the width and every other row is bent to it: short rows are padded with empty cells, long rows are cut. Measured on a,b,c followed by 6,7,8,9 and 1,2 — the 9 simply vanishes, and the short row gains an empty cell:

| a | b | c |
| --- | --- | --- |
| 6 | 7 | 8 |
| 1 | 2 |  |

The same mechanism does real damage when a title line sits above the header, as bank statements and 1C exports do. Measured on a statement that opens with Account statement 2026-10-01..2026-10-27: that single line becomes the header, so the file is a one-column table and every data row keeps only its first field.

| Account statement 2026-10-01..2026-10-27 |
| --- |
| id |
| 1 |

The id, the date and the amount are all gone; what is left is a list of ids. Deleting the preamble before converting takes ten seconds and is the whole fix.

Dialects: comma, and only comma

The reader is configured for commas, with no fallback, so everything else becomes a value or a surprise:

In the fileWhat comes out
Semicolon-separated, as Excel saves it in a comma-decimal localeone column: | EMEA;Q1;48200 |
sep=, hint line that some Excel locales writethat line becomes the header: | sep= | |
1234,56 with an unquoted decimal comma in a comma filethe row gains a cell, so the amount is cut to 1234
Tabs, in a file named .csvone column, with literal tab characters inside it
A real .tsvno table at all: the file returns as a fenced code block
Blank line between rowsa row of empty cells
Header row onlyheader and separator, no data rows
A zero-byte file# name.csv and an empty code block

Swap the delimiter before uploading and the result is a normal three-column table (checked on the semicolon sample):

python3 -c "import csv,sys; r=csv.reader(open(sys.argv[1],encoding='utf-8-sig'),delimiter=';'); w=csv.writer(open(sys.argv[2],'w',encoding='utf-8',newline='')); [w.writerow(x) for x in r]" semi.csv fixed.csv

Encoding: UTF-8 first, then a guess

The encoding is decided on the whole file, not on its first few kilobytes: UTF-16 and UTF-32 are recognised by their byte-order mark, then the file is tried as UTF-8 in full, then a single-byte encoding is guessed — and a guess is accepted only if it decodes the file to the end — then cp1252, then latin-1, which cannot fail. A byte-order mark is stripped, so the first header cell is region, not an invisible character glued to it.

Measured on the same table saved five ways — UTF-8, UTF-8 with a BOM, Windows-1252, Latin-1 and UTF-16 — all five came out character for character identical:

| name | city | note |
| --- | --- | --- |
| Aeroport | Zürich | café |
| Müller | Paris | naïve |
| Ångström | Córdoba | füße |

A file whose first 200 records are plain ASCII and whose last one ends in El Peñón converts completely — 201 records out of a 4.7 KB file. Where the first non-ASCII letter sits no longer matters, which is what used to make this failure look random.

What still goes wrong is a double-byte encoding without a mark. A UTF-8 file is easy, and single-byte code pages are guessed correctly on the sample above, but a Shift-JIS Japanese export was decoded as a neighbouring code page: 東京 came out as 뱦떈. Chinese GBK was read correctly in the same test, so this is a guess, not a rule — if a file is Japanese, Korean or Chinese and not UTF-8, re-save it as CSV UTF-8 before uploading. A UTF-16 file without its BOM is refused as binary (its NUL bytes in the first 8 KB), which at least fails loudly instead of producing one wrong column.

A report before the conversion, scripted

Three of the failures above are visible in the source file: a row wider than the header loses values, a short row gains empty cells, and a blank line becomes an empty record. One pass with the same comma dialect finds them before anything is converted:

import csv, sys

def report(path):
    with open(path, encoding='utf-8-sig', newline='') as fh:
        rows = list(csv.reader(fh))
    if not rows:
        print(f'{path}: empty file')
        return
    width = len(rows[0])
    body = rows[1:]
    wide = [(n, len(r) - width) for n, r in enumerate(body, start=2) if len(r) > width]
    short = [n for n, r in enumerate(body, start=2) if 0 < len(r) < width]
    blank = [n for n, r in enumerate(body, start=2) if not any(c.strip() for c in r)]
    print(f'{path}: header {width} column(s), {len(body)} data rows, '
          f'{len(short)} short, {len(blank)} blank, {len(wide)} too wide')
    if short:
        print(f'  first short row: line {short[0]}')
    if blank:
        print(f'  first blank row: line {blank[0]}')
    for line, extra in wide[:5]:
        print(f'  line {line}: {extra} value(s) past the header — they will be cut')

for path in sys.argv[1:]:
    report(path)

Run on the 300-record export above it printed header 3 column(s), 300 data rows, 0 short, 0 blank, 0 too wide — which is the point: a clean export reports nothing, and the pipe escaping is not something you have to fix afterwards. On a,b,c with a five-value row it names the line that will lose a value.

What this does not do

There is no delimiter detection, no header detection and no type casting: the first row is the header because the format says so, and every cell is a string because a CSV has nothing else. If the source needs a title line removed, a delimiter swapped or a ragged row fixed, that is a change to the file — after it, the conversion is a mechanical one-to-one.

Free tier: 25 files a day from one IP, up to 25 MB each, no account. There is no paid plan and nothing to buy; the limits, and the reasons they exist, are on the pricing page. For pipelines: POST /api/convert returns JSON with a markdown field, POST /api/convert/batch takes up to 20 files in one request, and POST /api/convert/zip returns a ZIP of .md files. Examples are in the API documentation; the converter needs no sign-up.

Frequently asked questions

Why is my first row now the header?

Because a table in Markdown starts with a header and a CSV has no header cells, so row 1 is taken verbatim. A title line above the real headings therefore becomes the only column — delete it in the file before converting.

A value contained a pipe or a line break — is it lost?

No. | arrives as \| and a line break as <br>, so the row keeps its cell count and the value stays readable. Only a hand-written splitter that ignores the backslash sees extra separators.

What happens to a row with more or fewer values than the header?

The header sets the width: short rows are padded with empty cells and values past the width are cut. The report script on this page names the lines where that will happen.

The file is not UTF-8 — will it break?

Usually not. UTF-8 is tried first on the whole file, then a single-byte encoding is guessed, then cp1252, then latin-1; a BOM is stripped. The case that still mis-decodes is a Japanese, Korean or Chinese file in a double-byte encoding without a BOM — re-save it as CSV UTF-8.

Semicolon-separated files from Excel?

They are not split: every row arrives as one cell. Convert the delimiter first — the one-line csv command above does it.

What are the limits, and can this run in a pipeline?

Free: 25 files a day from one IP, up to 25 MB each, no sign-up. Nothing is sold here. Automation works via POST /api/convert, /api/convert/batch and /api/convert/zip.

Convert a file

Drag a document into the window and get Markdown you can read, diff and feed to a model. Free: 25 files a day, up to 25 MB.