Skip to content

Repository files navigation

pdf-table-extractor

tests License: MIT Python

Extract tables from technical PDFs into clean Excel / CSV / JSON — and get told exactly which cells to double-check.

Most "PDF to Excel" tools hand you a clean-looking sheet and hide their own uncertainty. This one does the opposite: it normalises what it can (EU/US numbers, currencies, dates), and for anything it couldn't confidently parse it keeps the original text, lowers the cell's confidence, and records why — so a human reviews a handful of cells instead of re-checking the whole document.

It handles three kinds of source: ruled tables, borderless tables (recovered by text alignment), and scanned/image-only pages (optional OCR).


What you get

Every run produces, alongside the data, a quality report — the trust feature:

  • .xlsx — one sheet per table, header styled, low-confidence cells highlighted amber with a comment explaining the flag, plus a Quality Report sheet (overall confidence, scope advisory, per-table strategy, skipped pages).
  • .csv — one file per table (UTF-8 BOM, opens cleanly in Excel).
  • .json — fully structured: every cell carries value, type, confidence, flags, raw; every table carries its extraction strategy.
  • *_quality.md — a human-readable summary.

This covers both freelance delivery models out of the box: hand the client the output files (done-for-you), or hand them this repo + CLI to run on their own recurring PDFs (build-a-tool).


Quick start

pip install -r requirements.txt
make sample          # generate the bundled sample PDF
make run             # extract it to ./out

Output:

Extracted 2 table(s) — overall confidence 0.99
  1 cell(s) flagged low-confidence — see the quality report
Wrote: out/sample_quotation.{json,xlsx,csv} + out/sample_quotation_quality.md

The sample deliberately contains the cases real client PDFs have:

Cell in the PDF Extracted Note
€1.234,50 (EU) 1234.5 thousands/decimal resolved
€1,500.00 (US) 1500.0 same column, US format — still correct
14/03/2026 2026-03-14 day-first → ISO
Mounting bracket\n(stainless) Mounting bracket (stainless) multi-line cell joined
(blank Qty) null flagged: "empty in an otherwise-filled column"

CLI

pdf-table-extractor <pdf> [options]

  -o, --out DIR        output directory (default: ./out)
  -f, --format LIST    comma-separated: xlsx,csv,json (default: all three)
  -p, --pages RANGE    pages to extract, e.g. '1,3,5-8' (default: all)
  --ocr                OCR scanned/image-only pages (needs the [ocr] extra)
  --ocr-dpi N          OCR render resolution (default: 300)
  -q, --quiet          suppress the summary

Scanned PDFs (OCR)

Scanned pages have no text layer. Install the optional engine and pass --ocr:

pip install '.[ocr]'                       # rapidocr-onnxruntime
pdf-table-extractor scanned_invoice.pdf --ocr

OCR text is reconstructed into a table grid from the recognised boxes, and the OCR engine confidence is folded into each cell's confidence — so low-quality reads are flagged exactly like everything else. (Adapted from the OCR fallback in my schematic-extractor project.)

Measured accuracy

Accuracy is checked by an offline, deterministic benchmark with known ground truth (benchmarks/, reusing the evaluation approach from my docchat-rag project): cell-level precision/recall/F1, no LLM-as-judge.

make bench

The extractor is an ensemble — for each page it gathers candidates from pdfplumber (ruling lines, text alignment) and, when installed, Camelot (lattice, stream), then keeps the best one (segmentation-aware, weighted by Camelot's own reported accuracy). It also stitches tables that continue across a page break. Measured lift on the hard-PDF set:

Stage Overall cell-F1
Baseline (pdfplumber only) 0.83
+ stitching + heterogeneous-column normalization 0.90
+ Camelot ensemble 0.93
+ order-insensitive scoring + number recovery 0.95

Read this honestly: it's an internal hard-set benchmark, not a public leaderboard (PubTabNet/FinTabNet can't be fetched here). Perfect extraction from PDF is an unsolved problem; even commercial services err. The remaining ~5% is documented, not hidden — see benchmarks/README.md: the ultra-dense 30-column header degrades in every engine (and is flagged out-of-scope rather than trusted), and a couple of genuinely ambiguous numbers are flagged, not guessed. The path beyond this (Microsoft Table Transformer) is noted there as an optional, heavy dependency whose real gains are on visual/scanned tables.

Library

from pdf_table_extractor import extract_pdf
from pdf_table_extractor.export import export_document

doc = extract_pdf("invoice.pdf")          # add ocr=engine for scanned pages
print(doc.confidence, doc.scope.reason)
export_document(doc, "out", formats=["xlsx", "json"], stem="invoice")

How it works

PDF page
  -> has a text layer?
       yes -> ruled tables (pdfplumber lines)            strategy = "lines"
              none found? -> text-alignment fallback      strategy = "text"  (borderless)
       no  -> optional OCR -> grid from boxes             strategy = "ocr"   (scanned)
  -> per-column type inference (EU/US numbers, currency, %, dates)
  -> per-cell normalize + confidence + flags
  -> scope advisory (is this PDF inside the supported envelope?)
  -> export: xlsx / csv / json + quality report

Confidence is a structural trust signal — does this cell look like clean, unambiguous data of its column's type — not an OCR probability. Penalties: type mismatch −0.5, ambiguous separator −0.25, blank-in-filled-column −0.4, multi-line −0.1; OCR cells are additionally capped at the engine's confidence. Cells below 0.7 are flagged in every output.

Run the tests

pip install -r requirements-dev.txt
make test     # 31 tests; make lint for ruff

The parsing, confidence, OCR-reconstruction, scope and metric logic is all pure Python and unit-tested with no PDF and no ONNX runtime needed (tests/).

Known limitations

Found and documented by stress-testing on hard PDFs (see make eval and the test suite). The tool degrades honestly rather than failing silently:

  • Scanned PDFs need --ocr; OCR quality varies with scan resolution, and OCR'd cells are flagged with the engine's confidence rather than trusted blindly.
  • Heterogeneous columns (a column mixing numbers, N/A, and odd formats) fall back to text, so numbers inside them are kept verbatim, not normalized. This is deliberate — better than coercing a column the tool isn't sure about — but it means mixed columns need a human pass.
  • Ultra-dense / very wide tables (tens of narrow columns) can misalign cells; per-cell confidence stays high because each value parses, so the scope advisory is what flags these up front (in_scope: false). Trust the scope flag, not just per-cell confidence, on dense matrices.
  • Multi-page tables are stitched when a table continues onto the next page (same column count, header only on page 1). Unusual layouts where the continuation can't be confidently matched are left as separate tables rather than merged on a guess.
  • Heavily merged / nested cells produce placeholder headers (col_2, …) and ragged-row flags rather than a silent guess.
  • Separator ambiguity: a lone 1.234 is genuinely ambiguous (1234 vs 1.234); parsed conservatively and flagged rather than guessed.

Built on proven components

Two pieces are adapted from my other repositories, which is why they're already hardened: the OCR fallback pattern from schematic-extractor and the offline evaluation harness from docchat-rag.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages