Stop Copy-Pasting Invoices: Extract PDF Data to CSV with 100 Lines of Python
Every month, somebody on every finance team does the same soul-crushing job: open an invoice PDF, copy the invoice number into a spreadsheet, copy the date, copy the total, repeat forty times. It's slow, it's boring, and it's exactly the kind of work where humans make quiet mistakes — a transposed digit in a total doesn't announce itself until the books don't balance.
The good news: if your invoices are text-based PDFs (the kind exported by accounting software, not scanned paper), this is a solved problem with embarrassingly little code. In this post we'll build a batch extractor in Python: point it at a folder of PDFs, get a CSV out. Full working demo, MIT licensed, is on GitHub — link at the end.
Step 1: Get the text out of the PDF
We'll use pdfplumber, which wraps pdfminer and gives you clean page text:
import pdfplumber
def pdf_text(path):
with pdfplumber.open(path) as pdf:
return "\n".join(page.extract_text() or "" for page in pdf.pages)
That's the whole "PDF parsing engine." For born-digital invoices, the text layer is already there — we just need to mine it.
Step 2: Extract fields with anchored regexes
Invoice layouts vary, but the labels are remarkably stable. Three patterns cover our three fields:
FIELDS = {
"invoice_number": re.compile(
r"Invoice\s*(?:No\.?|Number|#)\s*[:#]?\s*([A-Za-z0-9\-]+)", re.IGNORECASE),
"invoice_date": re.compile(
r"\bDate\s*[:\-]?\s*(\d{4}-\d{2}-\d{2})", re.IGNORECASE),
"total_amount": re.compile(
r"^\s*Total\s*:\s*\$?\s*([\d,]+\.\d{2})", re.IGNORECASE | re.MULTILINE),
}
One detail matters here: the total_amount pattern is anchored to the start of a line. Without that anchor, Subtotal: $1,000.00 happily matches a search for Total: — and your CSV fills up with subtotals wearing a fake moustache. Ask me how I know.
Amounts get a light normalization (1,100.00 → 1100.00) so the CSV is spreadsheet- and diff-friendly.
Step 3: Never drop a row silently
Here's the design decision I'd defend in a code review: when a field isn't found, we still write the row, leave the field empty, and name the gap in a status column:
filename,invoice_number,invoice_date,total_amount,status
partial.pdf,INV-2026-099,2026-03-01,,missing: total_amount
A batch tool that quietly skips files is a batch tool that creates reconciliation mysteries. Forty invoices in, thirty-nine rows out, and no clue which one vanished? No thanks. The status column turns "the numbers don't add up" into a ten-second lookup.
Step 4: Batch it and test it
The main loop is unglamorous: glob *.pdf, extract each, write rows with csv.DictWriter, print a per-file status. The part most tutorials skip is the test: the demo repo generates two synthetic invoices with ReportLab, ships an expected_output.csv with the known-correct answers, and the smoke test is literally:
python make_sample.py
python extract.py --input ./samples --out results.csv
diff results.csv expected_output.csv # silence means success
Synthetic samples matter more than they look. You can't put real client invoices in a public repo, and "it worked on my machine once" isn't a test. Generate your fixtures, check them in, and let the diff be the judge.
Where the demo stops — and production starts
This demo extracts three hardcoded fields from well-behaved PDFs. Real invoice work is messier, and three problems show up immediately:
- Every vendor has a different layout. Hardcoded regexes become a maintenance hobby. You want the fields — names, patterns, types — defined in a config file, so a new vendor is a new YAML block, not a code change.
-
Extraction without validation is a liability. Regex found a number after
Total:— but is it right? The check that catches real money errors is cross-field arithmetic: doestotal == subtotal + tax? I once generated a test invoice with a deliberately wrong total (500 + 50 ≠ 560) specifically to watch a validator flag it. That's the feature that pays for itself the first time it catches a fat-fingered invoice before payment, not after. - Someone has to review the weird ones. In production you want a validation report and a "needs human review" list — the 5% of files that deserve eyes — instead of a human re-checking 100% of them.
Add per-field confidence scores, Excel output, and an OCR path for scanned PDFs, and the demo grows into the tool I actually ship to clients.
Try it / get the full version
- Free demo (MIT): https://github.com/mahuan2928/pdf-extraction-demo — samples, expected output, and the whole ~100-line extractor.
-
Full version ($39): PDF & Invoice Data Extraction Pipeline — fields defined in
fields.yaml(any fields, not just three), validation including cross-field totals checks, a validation report plus human-review list, Excel outputs, per-field confidence scores, and an OCR path for scanned PDFs.
If you adapt the demo to your own invoice layouts, I'd love to hear which pattern broke first — vendor creativity in invoice design is truly boundless.
— Marty Ma, Senior AI & Data Engineer (Tokyo)
Top comments (0)