Table and Structure Extraction
On this page
Updated November 2, 2026 — OCR. OCR 4.0 (
mistral-ocr-4-0) was retired on September 30, 2026. Use OCR 4.1 (mistral-ocr-4-1), generally available since August 31 at the same price, or the aliasmistral-ocr-latest, which has pointed to it since July 16. OCR 4.1 adds confidence scores at the block level.
Tables: The Historical Weakness of OCR
Ask Tesseract to extract the summary table from a financial report, and you’ll get a string of lines where values from the “Total” column get mixed up with those from the “Quantity” column, with no clue linking them back to their original rows. The problem isn’t character recognition, which works fine; it’s the total lack of tabular structure awareness. Traditional solutions read left-to-right, top-to-bottom, losing the relationships between columns and rows along the way.
OCR 4 approaches the page differently: it sees it. This visual understanding lets it restore a table as a table, not as flattened text.
The table_format Parameter
You control table rendering with a single parameter passed to the OCR call:
import os
from mistralai.client import Mistral
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
# Markdown format (recommended for most cases)
ocr_response = client.ocr.process(
model="mistral-ocr-latest",
document={
"type": "document_url",
"document_url": "https://example.com/financial-report.pdf"
},
table_format="markdown"
)
Three values are possible, and the choice isn’t cosmetic: it determines what you can do with the result downstream.
With null, the default value, tables are integrated into the Markdown flow without specific tabular formatting. This setting works when you aren’t processing tabular documents and just want text to index.
With "markdown", each table is rendered in standard pipe format, producing output that is readable as-is and easy to parse:
| Product | Quantity | Unit Price | Total |
|---------|----------|------------|-------|
| Widget A | 100 | €12.50 | €1,250 |
| Widget B | 50 | €25.00 | €1,250 |
| **Total** | **150** | | **€2,500** |
With "html", the table is rendered as full HTML, preserving cell merges and advanced formatting that Markdown can’t express:
<table>
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Unit Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr>
<td>Widget A</td>
<td>100</td>
<td>€12.50</td>
<td>€1,250</td>
</tr>
</tbody>
</table>
In practice, Markdown covers most needs: RAG indexing, search, or display in an editor. Switch to HTML as soon as your tables include merged cells or the result needs to be embedded in a web page. Keep null only when tables don’t matter in your workflow—say, a narrative report or correspondence.
Retrieving Tables as Data
A Markdown table is still text. To turn it into manipulable data, you need to spot it in the flow and split it up. The function below walks through the lines, identifies those belonging to a table, ignores the separator line, and closes the current table as soon as a regular line appears:
import os
import re
import json
from mistralai.client import Mistral
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
def extract_markdown_tables(markdown: str) -> list[list[list[str]]]:
"""Extracts Markdown tables and converts them to lists."""
tables = []
lines = markdown.split("\n")
current_table = []
in_table = False
for line in lines:
if "|" in line and line.strip().startswith("|"):
# Ignore separator line (|---|---|)
if re.match(r"^\|[\s\-:|]+\|$", line.strip()):
continue
cells = [
c.strip() for c in line.strip().strip("|").split("|")
]
current_table.append(cells)
in_table = True
else:
if in_table and current_table:
tables.append(current_table)
current_table = []
in_table = False
if current_table:
tables.append(current_table)
return tables
# OCR extraction with Markdown tables
ocr_response = client.ocr.process(
model="mistral-ocr-latest",
document={
"type": "document_url",
"document_url": "https://example.com/invoice.pdf"
},
table_format="markdown"
)
# Iterate through pages and extract tables
for page in ocr_response.pages:
tables = extract_markdown_tables(page.markdown)
for i, table in enumerate(tables):
print(f"Page {page.index + 1}, Table {i + 1}:")
print(json.dumps(table, indent=2, ensure_ascii=False))
The final check outside the loop isn’t decorative: without it, a table at the end of a page—a common case for an invoice summary—would never be added to the list, since no regular line comes along to close it.
Moving to pandas for Analysis
A list of lists is enough for export, not for calculation. Converting to a DataFrame means promoting the first row to headers:
import pandas as pd
def tables_to_dataframes(markdown: str) -> list[pd.DataFrame]:
"""Converts Markdown tables to pandas DataFrames."""
tables = extract_markdown_tables(markdown)
dataframes = []
for table in tables:
if len(table) < 2:
continue
headers = table[0]
data = table[1:]
df = pd.DataFrame(data, columns=headers)
dataframes.append(df)
return dataframes
# Usage
for page in ocr_response.pages:
dfs = tables_to_dataframes(page.markdown)
for df in dfs:
print(df.to_string())
print()
The len(table) < 2 test filters out false positives: an isolated line containing a pipe, inside a code block or an address, won’t produce a broken, empty DataFrame. Keep in mind that values remain strings; converting “€1,250” to a number is still up to you.
When Tables Get Complicated
Merged cells, two-level headers, or irregular layouts push Markdown beyond its expressive limits. Switch to HTML then, and let BeautifulSoup and pandas handle the analysis:
from bs4 import BeautifulSoup
import pandas as pd
ocr_response = client.ocr.process(
model="mistral-ocr-latest",
document={
"type": "document_url",
"document_url": "https://example.com/complex-report.pdf"
},
table_format="html"
)
for page in ocr_response.pages:
soup = BeautifulSoup(page.markdown, "html.parser")
tables = soup.find_all("table")
for table in tables:
df = pd.read_html(str(table))[0]
print(df)
This approach saves you from writing a parser: pd.read_html handles rowspan and colspan attributes that your manual splitting would miss. The cost is an extra dependency and slightly more verbose output—a trade-off well worth it for financial reports or regulatory tables.
Key Takeaways
- The
table_formatparameter controls table rendering (null, markdown, html) - Markdown format suits most use cases
- HTML format preserves merged cells and advanced formatting
- Extracted tables can be converted to pandas DataFrames for analysis
- OCR 4 faithfully reconstructs tabular structure, even on complex layouts