Extracting tables from PDFs and spreadsheets for RAG

Jun 2, 2026 · 4 min read · rag, tables, pdf, office-docs

Ask any RAG pipeline builder which content type causes the most retrieval failures. A lot of them will say tables. A paragraph survives bad extraction gracefully. A jumbled sentence is still mostly readable. A table doesn’t survive as well. Strip out the column alignment and you’re left with a wall of numbers and labels. The order means nothing without the grid they came from.

Why tables break naive extraction

A table’s meaning lives entirely in its two-dimensional layout. Which cell is in which row, and which row belongs under which column header, both matter. Most “extract the text” libraries only understand one-dimensional text: a stream of characters in reading order. Fed a table, they have to guess how to flatten two dimensions into one. The guess is often wrong. Cells read top-to-bottom instead of left-to-right. Headers get separated from the rows they label by hundreds of characters. Merged cells get duplicated or dropped entirely.

The result is number-soup, and that’s worse than an outright error. It looks like extracted text. It gets chunked and embedded like extracted text. It only fails visibly later. That happens when someone asks a question whose answer was in row 14 of a table that got scrambled into row 3’s neighborhood.

What extraction for a table means here

Be careful with what txtfetch does and does not give you. It depends on the file format and on the output you ask for. We tested one small table in seven formats. Tables has the raw responses.

The default output is extracted_text. It has no table in it. A Word or HTML table comes back with one cell on each line. An Excel or PowerPoint table comes back as tab-separated rows.

Add format=markdown and an HTML, Word, Excel, PowerPoint or ODT table comes back as a Markdown pipe table. A merged ODT cell is the one exception. That table comes back as HTML with a colspan. Add format=json and the same table comes back as a table element with a cells grid. Each cell holds its text, its colspan and its rowspan.

curl -X POST "https://api.txtfetch.com/v1/extract?format=markdown" \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@tables-plain.docx
{
  "status": "success",
  "markdown": "Sales by quarter.\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 1.5 | 2.5 |\n| South | 1.0 | 2.0 |\n"
}

A PDF is different. In our test, the PDF table came back as one line for each row, with the values split by spaces. The Markdown output had no pipe table. The JSON output had no table element. This is that response, without its metadata field:

{
  "status": "success",
  "extracted_text": "\nSales by quarter.\n\nRegion Q1 Q2\n\nNorth 1.5 2.5\n\nSouth 1.0 2.0\n\n\n"
}

The same holds for a table in a scan. No cell model exists in the file. Your pipeline needs a tool that reads the page layout for those tables.

Spreadsheets give the best result. An Excel file comes back as a pipe table under a heading for each sheet. Fetch one from a URL:

curl -X POST "https://api.txtfetch.com/v1/extract?format=markdown&url=https://example.com/models/forecast.xlsx" \
  -H "Authorization: Bearer $TXTFETCH_KEY"

Merged cells

Check merged cells before you trust a table. Excel and PowerPoint keep every value in its column. ODT keeps the merge as a colspan. HTML and Word lose it. The cells after the merge move one column left. In Markdown, that puts a number under the wrong header. The cells grid in format=json shows the problem. A row with a lost merge has fewer cells than the header row. The Tables page has a short script that finds those rows.

Chunking tabular text so retrieval survives

Once a table is text, the chunking decision matters more than usual. Splitting a table in the middle of its rows, the same fixed-size cut you’d apply to prose, divorces a row from its header. That makes the chunk unanswerable on its own. Two things help:

  • Keep small tables whole. A table under a few hundred tokens should usually be its own chunk rather than getting split, so the header stays attached to every row.
  • Repeat the header row into each chunk of a large table. For tables too big to keep whole, prefix every chunk with the header row’s text. That way each chunk is self-describing, instead of assuming the header survived from a previous chunk.

See the chunking strategies guide for the general reasoning. Tables are the case where getting this specific decision right matters most.

Tables are common enough inside Office documents that it’s also worth reading the DOCX/PPTX/XLSX parsing guide. Read it if spreadsheets and embedded tables inside Word docs are a big share of your document set.

Wrestling with a document set that’s mostly tabular? Get in touch and we’ll set you up with an API key.

Try it on your own file.

The free reader runs in your browser. Nothing gets uploaded.

Open the file reader →

Get an API key →