Extract tables from a PDF
Updated August 2026 · 5 minute read
First check whether the PDF has embedded text: open it and try to select a few words. If text selects, pull the tables out directly with a library like pdfplumber and skip OCR entirely ; if the pages are scans, OCR is required, and because engines like Tesseract return words with coordinates rather than tables, rebuilding the rows and columns becomes your problem.
Digital PDF or scanned PDF: which path are you on?
The selectable-text test decides everything downstream. Zooming in works as a second check, since real text stays sharp at any magnification while a scan goes blocky.
| Digital PDF | Scanned PDF or image | |
|---|---|---|
| What the file contains | Characters with exact positions, placed by the software that produced it | A picture of a page, nothing more |
| The right tools | pdfplumber or PyMuPDF, reading the embedded text | An OCR engine such as Tesseract, then your own row and column logic |
| Reliability | Coordinates are exact, so a clean table comes out clean | Every later step inherits the noise of the image underneath |
| Common mistake | Running OCR anyway, trading exact characters for a lossy guess | Expecting the engine's output to arrive already shaped like a table |
Mixed documents exist: a digital PDF with a scanned exhibit pasted into one page. Test per page, not per file.
The DIY path: Tesseract, pytesseract, and pandas
For a scanned table, the standard Python route runs through pytesseract and lands in a DataFrame. It is genuinely workable for clean grids.
-
Prepare the image
Render each PDF page to an image at a generous resolution, then clean it up: deskew so the lines run horizontal, and apply a binarization threshold when contrast is weak. These preprocessing moves shift results more than any recognition setting will.
-
Get word boxes, not plain text
Call pytesseract.image_to_data with output_type set to pytesseract.Output.DATAFRAME instead of reaching for image_to_string. The result is a pandas DataFrame with one row per detected word: its text, the left, top, width, and height of its bounding box, and a confidence value.
-
Cluster words into rows
Drop empty and low-confidence entries, then group words whose top coordinates land within a small tolerance of one another. Each cluster is one visual line of the table.
-
Assign columns by x-position
Within each line, sort words left to right and bin them against column boundaries inferred from the header or from gaps in the x-distribution. If the default segmentation carves the region up strangely, try a page segmentation mode like --psm 6, which treats the input as a single uniform block of text.
-
Validate before trusting anything
Confirm every row has the expected cell count and that numeric columns actually parse as numbers. For financial tables, sum each column and compare against the printed totals: arithmetic is the one check that catches a misread digit.
Where the DIY approach collapses
Tesseract has no native concept of a table. It emits text with positions, and everything grid-shaped is geometry you write yourself, which is why the clustering above holds up right until real-world documents arrive.
Merged cells break the column logic, because a single value now legitimately spans two bins. Wrapped text breaks the row logic, since a description continuing onto a second line clusters as its own row with most cells empty. Multi-line records combine both failures at once.
Financial statements add a quieter trap. Amount columns are right-aligned, so the left edges that column inference leans on drift with the digit count, and a long figure can slide into the neighboring bin. A misassigned amount on a statement does not look broken; it looks like a deposit.
When to build it, and when not to
The script is the right call when the documents are uniform, the volume justifies the engineering time, and a wrong cell is cheap. One recurring report from one source, parsed once and rerun forever, is exactly what a hand-tuned pipeline is for.
The calculus flips when layouts vary or the numbers carry weight. Logic tuned to one bank's statement meets a second bank's format and starts producing plausible garbage, and plausible is the operative word: table extraction rarely fails with a crash, it fails with a value in the wrong column that survives into the books. When a silently wrong number would cost more than tooling does, stop building.
What LedgerBox does with tables
LedgerBox covers both branches of the decision tree. Digital PDFs and scanned images alike come back as structured rows, with the recognition, the geometry, and the layout handling already done, for generic tables as well as the financial documents where the stakes concentrate: bank and brokerage statements, invoices, receipts, purchase orders, and merchant statements.
Validation ships inside the output instead of being left as an exercise. Statement rows have to reconcile against their running balances, uncertain values surface as flagged rather than guessed, and the finished rows export to XLSX, CSV, QuickBooks CSV and QBO, and JSON. The same extraction runs from code through the REST API and the open source TypeScript SDK.
Questions people ask
Can Tesseract extract tables from a PDF?
Not directly. Tesseract reads text and reports where each word sits, but it has no notion of rows or columns, so the workflow is: convert the page to an image, run recognition, then reconstruct the grid from coordinates yourself.
How do I get pytesseract output into a pandas DataFrame?
Use pytesseract.image_to_data with output_type=pytesseract.Output.DATAFRAME. Every detected word becomes one DataFrame row carrying its text, bounding box, and confidence, ready for the grouping that turns loose words into table rows.
How do I extract a table from a scanned PDF in Python?
Render the page to an image, deskew and threshold it, run pytesseract.image_to_data for word boxes, then cluster by y-coordinate into rows and by x-position into columns. If the PDF turns out to have embedded text, use pdfplumber instead and skip all of that.
Why are my extracted table columns misaligned?
Usually geometry rather than recognition. Right-aligned numbers move their left edge as digit counts change, wrapped cells spawn phantom rows, and a tilted scan shifts every coordinate, so fix skew and column inference before blaming the engine.
Should I run OCR on a PDF that already has text?
No. Recognition on a digital PDF rasterizes exact characters into pixels and then guesses them back, so the output can only match or degrade the original; extract the embedded text with pdfplumber or PyMuPDF.
Can I extract a table from an image or screenshot?
Yes, and it is the scanned-PDF path minus the rendering step, since the screenshot already is the image. Quality still dominates, so a full-resolution capture beats a compressed or resized copy.
Where to go next
- Extract PDF tables with LedgerBox The product page for exactly this job, both branches handled.
- What is OCR? The recognition layer underneath all of this, explained plainly.
- REST API and TypeScript SDK Run the same extraction from your own pipeline.
- CSV export What the finished rows look like on the way out.
- Excel export When the destination is a spreadsheet, not a script.