How do I extract tables from scanned PDFs with Python?
1 November 2026
Rasterise each page to an image, clean it up (deskew, reduce noise, improve contrast), detect the table's rows and columns, run OCR on each cell, and write the results to a CSV or spreadsheet. Scanned pages have no text layer, so ordinary PDF table extractors return nothing until OCR has been applied.
Why do normal PDF table extractors return nothing from a scan?
A scanned PDF is a picture of a page. Libraries that read tables from digital PDFs rely on a text layer: characters with coordinates, plus ruling lines stored as vector drawing instructions. A scan contains neither, only pixels, so those tools find no words and return empty tables.
You can check quickly. Try selecting text in a PDF viewer, or extract the text with a PDF library and see whether anything comes back. If it is empty, you have an image-only file.
Some scans already carry a hidden OCR layer added by the scanner software. These are mixed cases: the text exists but may be misread or misplaced, so compare a few values against the page before trusting it.
For image-only files you need two extra stages before any table logic:
- Rasterisation: turning each page into an image at a suitable resolution.
- OCR: recognising characters and returning their positions on the page.
Once you have words with bounding boxes, table extraction becomes a geometry problem: group words into rows by vertical position and into columns by horizontal position. That is the core idea behind everything that follows, and it works whether or not the table has visible grid lines.
How do I prepare scanned pages so OCR reads them accurately?
Preparation often matters more than the choice of OCR engine. A sensible pipeline looks like this:
- Rasterise at around 300 DPI. Lower resolution blurs small digits, while much higher slows processing for little gain.
- Convert to greyscale.
- Deskew. Even a one or two degree tilt makes rows slope and breaks row grouping. Estimate the angle from the dominant line or text direction and rotate to correct it.
- Remove speckle with a light median blur.
- Binarise with an adaptive threshold rather than a single global value, which copes better with shadows near the spine or uneven scanner lighting.
- Crop margins and remove punched-hole marks or page borders.
Do not over-process. Aggressive thresholding can erase thin decimal points and faint ruling lines, which in financial tables changes the meaning of a figure. Keep the original image as well, so you can crop from it later if the cleaned version loses detail.
If pages arrive in mixed orientations, correct rotation in 90 degree steps first. A short loop that tries each orientation and keeps the one with the highest average recognition confidence is usually enough for batches. Save intermediate images while developing, so you can see exactly what the OCR engine receives.
How do I find the rows and columns of a table in an image?
There are two approaches, depending on the table.
Ruled tables (visible grid lines). Use morphological operations on the binarised image. A long horizontal kernel isolates horizontal lines and a long vertical kernel isolates vertical ones. Combine the two masks, find the intersections, and you have the cell corners. Sort them top to bottom and left to right to build a grid, then crop each cell.
Unruled tables (whitespace only). Run OCR over the whole table region and use the word positions. Cluster word centres by vertical coordinate to form rows, using a tolerance of roughly half the median text height. For columns, project the words onto the horizontal axis and look for gaps that run the full height of the table. Those gaps are your column boundaries.
Practical tips:
- Merged and multi-line cells are the usual failure points. Treat a row as a continuation of the one above if its first column is empty.
- Broken or faint lines can be bridged by dilating the line masks slightly.
- Locate the table first, for example by finding the largest connected line structure, so headers and footers are excluded.
Draw the detected boxes over the page image and inspect them before you run a whole batch.
How do I turn the recognised cells into a clean spreadsheet?
Once you have a grid, OCR each cell crop separately using a single-line or single-block mode. This stops the engine reordering text across columns. For unruled tables, assign each recognised word to a cell using its coordinates instead.
Load the rows into a dataframe, then write to CSV or a spreadsheet file. Before writing:
- Strip whitespace and normalise odd characters such as unusual dashes and non-breaking spaces.
- Fix common digit confusions in numeric columns: letter O for zero, lowercase l or capital I for one, S for five. Apply these only to columns you know are numeric.
- Parse numbers explicitly, handling thousands separators and bracketed negatives. For UK documents, watch for pound signs and dd/mm/yyyy dates, and parse dates with an explicit format so day and month are never swapped.
- Promote the first row to headers only after checking it was read correctly.
- Keep identifiers such as account numbers or postcodes as text so leading zeros survive.
Add a confidence column, or a flag, for any cell whose OCR confidence falls below a threshold you choose. That lets a person review only the doubtful cells rather than the whole file.
How can I check the extracted data is right before relying on it?
OCR errors are quiet, so build checks into the script rather than relying on a glance.
- Arithmetic checks: if the table has totals, sum the columns and compare with the printed figure. A mismatch points to a misread digit.
- Shape checks: every row should have the same number of columns. Rows that differ often indicate a split or merged cell.
- Type checks: flag non-numeric text in numeric columns and dates that fail to parse.
- Spot checks: save a cropped image beside each low-confidence cell so you can compare it with the output in seconds.
- Sample audits: for a new document layout, compare a handful of rows by hand before running the full batch.
Log the file name, page number and table index for every output so any problem can be traced back to its source. Keep the raw OCR output as well as the cleaned version; if you later improve your cleaning rules, you can rerun them without reprocessing the images.
Finally, record your settings (resolution, thresholds, row tolerance). Different scanners and layouts often need different values, and writing them down makes results repeatable.
Frequently asked questions
What resolution should I scan at for table OCR?
Around 300 DPI is a sensible starting point for typical printed tables. Very small print or faint photocopies may benefit from a higher setting, so test on a sample.
Can Python read handwritten tables from scans?
Handwriting is far less reliable than print with standard OCR, so expect many errors and plan for manual review of every cell.
Do I need to detect grid lines to extract a table?
No. Tables without lines can be rebuilt from word positions by clustering them into rows and finding consistent vertical gaps for columns.
Related on B.A.I.S.
Our Scan-to-Spreadsheet script turns scanned tables into spreadsheets on your own machine, as a script you run yourself. It suits regular conversions where you would rather keep your documents local than upload them anywhere.
Need this done for you? Scan-to-Spreadsheet script