PDF to Excel with a neural network: extract tables without copy-paste
A working pipeline: pull a table out of a PDF, ask the model for JSON, write xlsx and reconcile the totals so a silent error does not slip through.
4 min · · автоматизация · development
If you have dozens of invoices, statements or price lists in PDF, copying them into a spreadsheet by hand eats an evening. A neural network can do PDF to Excel in a few lines of code, but only if you do not trust it blindly: a model can skip a row or mix up a decimal comma. Below is a pipeline where the result is checked with arithmetic, plus a cost estimate per page.
Two kinds of PDF and why it matters
First, find out what is inside the file.
- A text layer exists. You can select the text with the mouse. It can be extracted in code and sent to the model as a plain string. This is cheaper and more accurate.
- It is a scan or a photo. You cannot select text. The page has to be rendered to an image and sent to a model that accepts images as input.
A quick check in code: if extract_text() returns an empty string or garbage, the page is most likely an image.
Step 1. Get the text or the image
pdfplumber works for the text layer and PyMuPDF for rendering pages:
pip install pdfplumber pymupdf openpyxl openaiimport base64
import pdfplumber
import fitz # PyMuPDF
def page_text(path: str, page_no: int) -> str:
with pdfplumber.open(path) as pdf:
return pdf.pages[page_no].extract_text() or ""
def page_png_b64(path: str, page_no: int, dpi: int = 150) -> str:
doc = fitz.open(path)
pix = doc[page_no].get_pixmap(dpi=dpi)
return base64.b64encode(pix.tobytes("png")).decode("ascii")150 dpi is usually enough for printed text. Raise it for small fonts, but remember that a bigger image means more input tokens.
Step 2. Ask the model for JSON
The main rule: do not ask to "make a table", ask for a strictly defined structure. Then the result is easy to verify in code.
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.zukko.pro/v1", api_key="fetch_your_key")
PROMPT = """Extract the line-item table from the page. Return JSON only, no commentary:
{"rows": [{"name": "...", "qty": 0, "price": 0.0, "total": 0.0}],
"document_total": 0.0}
Write numbers as numbers with a dot, without spaces or currency signs.
If a value is not in the document, use null. Do not invent anything."""
FENCE = "`" * 3 # markdown fence, in case the model adds one
The text path uses deepseek-v4.1-flash: it has no image input, but it is the cheapest model in our table. Scans need a model with image input, for example claude-sonnet-5-5 or gpt-6.1-sol. Request formats are covered in the chat completions docs.
Step 3. Check the sums before writing the file
This is the most important step. The model may be wrong by a single digit while the table looks convincing.
from decimal import Decimal
def validate(data: dict) -> list[str]:
problems = []
rows = data["rows"]
for i, row in enumerate(rows):
if None in (row["qty"], row["price"], row["total"]):
problems.append(f"row {i}: empty value")
continue
calc = Decimal(str(row["qty"])) * Decimal(str(row["price"]))
if abs(calc - Decimal(str(row["total"]))) > Decimal("0.01"):
problems.append(f"row {i}: {calc} != {row['total']}")
s = sum(Decimal(str(r["total"])) for r in rows if r["total"] is not None)
doc_total = data.get("document_total")
if doc_total is not None and abs(s - Decimal(str(doc_total))) > Decimal("0.01"):
problems.append(f"sum of rows {s} does not match total {doc_total}")
return problemsTwo checks catch most errors: quantity times price equals the row total, and the sum of rows equals the document total. Send pages with warnings for another pass (for example to a stronger model) or to manual review.
Step 4. Write xlsx with openpyxl
from openpyxl import Workbook
def write_xlsx(pages: list[dict], out_path: str) -> None:
wb = Workbook()
ws = wb.active
ws.append(["Name", "Qty", "Price", "Total"])
for data in pages:
for r in data["rows"]:
ws.append([r["name"], r["qty"], r["price"], r["total"]])
last = ws.max_row
ws.append(["Total", None, None, f"=SUM(D2:D{last})"])
wb.save(out_path)The closing SUM formula is a second independent control: compare its result with the total in the PDF by eye.
What it costs
Prices as of 18 October 2026, dollars per 1M tokens. Formula: input × input price + output × output price.
A text page on deepseek-v4.1-flash (0.013 / 0.038). Assume 1500 input tokens and 800 output tokens:
1500 × 0.013 / 1,000,000 + 800 × 0.038 / 1,000,000 = 0.0000195 + 0.0000304 ≈ $0.00005
For 100 pages that is about $0.005.
An image page on claude-sonnet-5-5 (0.4 / 2). How many tokens an image takes depends on its size, so assume 1600 input tokens and 800 output tokens:
1600 × 0.4 / 1,000,000 + 800 × 2 / 1,000,000 = 0.00064 + 0.0016 = $0.00224
For 100 pages that is about $0.22. Read the real token count for an image from the usage field of the response and measure on three to five of your own pages before running a batch. Current prices: GET https://api.zukko.pro/v1/models or the "Models" page in the dashboard.
Practical tips
- Do not send personal data to the model unless the task needs it: templates often contain names and bank details.
- Cut multi-page tables by page and merge them in code instead of asking the model to process 80 pages at once.
- Describe complex multi-level headers and merged cells in the prompt in words: which columns exist and in what order.
- Other document types are covered in the posts on PDF processing via vision and on extracting data from documents.
Create a key in the dashboard; the code above works with it unchanged. A basic Python example is in the docs.
FAQ
Can I do this without code?
Yes, for one file: upload the pages to the browser chat and ask for a table. For regular work, a script with sum checks is more reliable.
What about poor-quality scans?
Raise the dpi, crop the margins and retry. If the model answers null, that is more honest than an invented value: check those rows by hand.
Which model is more accurate?
We do not publish our own quality measurements. Compare two or three models on ten of your own pages and count which one produces fewer mismatches in the sum check.
How many pages can I process at once?
Pages are independent, so they can be sent in parallel. Limits depend on the key limits and the model provider; handle a 429 error with a retry and a pause.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Translating subtitles with a neural network: SRT without broken timecodesThe main risk when translating SRT with a neural network is a broken file structure. A scheme where timecodes never reach the model, and a Python script with validation.
- Editing texts with a neural network: proofreading, style and cuttingDifferent kinds of editing need different prompts. Wording for proofreading, style and cutting, how to get edits with explanations and how not to lose the author's voice.
- A neural network for writing scripts: Bash, PowerShell and PythonA model writes scripts for routine fast, but running them blindly is risky. How to state the task, demand a safe dry-run mode and check the result before launch.
- Dify in Docker: installation, first-run setup and connecting modelsInstall Dify with Docker Compose, create the admin account and connect Claude, GPT and DeepSeek models on one key. Plus what to do when containers will not start.