Skip to content
How To

How to Extract Invoice Data from PDF to Excel Automatically with Python

How-To · Python · Automation

How to Extract Invoice Data from PDF Files and Save It to Excel Automatically with Python

If you process invoices regularly, you already know the pain — open a PDF, read the number, type it into a spreadsheet, repeat forty times. This guide shows you how to automate that entire process using Python, so a script does the reading and the spreadsheet fills itself.

Published July 5 2026 By Mr Wangdoo 8 min read Skill level: Beginner

What this guide produces

Column in ExcelExtracted from PDF
Invoice Numbere.g. INV-2026-0041
Invoice Datee.g. 01 July 2026
Due Datee.g. 31 July 2026
Vendor Namee.g. Acme Supplies Ltd
Line ItemsDescription of goods/services
Quantitye.g. 5
Unit Pricee.g. €120.00
VAT / Taxe.g. €120.00
Total Amounte.g. €720.00

Before you start — one important check

This guide works with text-based PDFs — invoices generated by accounting software like Xero, QuickBooks, FreeAgent, Sage, or any PDF exported from a web browser. If your invoices are scanned paper documents (photographed or faxed and saved as PDF), this method will not work as written — scanned PDFs require OCR software to read the text first. Not sure which type you have? Open the PDF, try to highlight and copy some text. If you can select the text, it is text-based. If you cannot, it is a scanned image.

What You Need

A general walkthrough of extracting PDF data to Excel using pdfplumber — the same library used in this guide. Independent production, not a Wangdoo production.

Just the python_portable folder and your invoice PDF files. Python and all required libraries — pdfplumber, openpyxl, pandas — are already bundled inside python_portable. There is nothing to install and no internet connection required.

  • The python_portable folder — self-contained, already set up
  • Your invoice PDF files — text-based, not scanned (see the check below)
  • Five minutes — this is a practical AI-era automation that replaces repetitive manual data entry permanently

Step 1 — Save the Python Script

1Open Notepad (Windows) or TextEdit (Mac) and paste the script below

On Mac, go to Format → Make Plain Text before pasting. Save the file as invoice_extractor.py — the .py extension is important, not .txt.

Windows users — important: when saving in Notepad, the Save As dialog has a “Save as type” dropdown that defaults to “Text Documents (*.txt)”. Change it to “All Files (*.*)” before saving, then type the full filename including the extension: invoice_extractor.py. If you skip this step, Notepad silently saves the file as invoice_extractor.py.txt and the script will not run. To check: in File Explorer, look for the file — if it shows as invoice_extractor.py.txt, delete it and repeat with the “All Files” option selected.

Python — invoice_extractor.py
import pdfplumber
import openpyxl
import os
import re

# -----------------------------------------------
# CONFIGURATION — edit these two lines only
# -----------------------------------------------
PDF_FOLDER = "input_pdfs"                      # folder containing your PDF invoices
OUTPUT_FILE = "output_excels/invoices.xlsx"  # Excel file saved in output_excels folder

# -----------------------------------------------
# Helper: extract a value that follows a label
# e.g. find "Invoice Number:" and return what is after it
# -----------------------------------------------
def find_field(text, patterns):
    for pattern in patterns:
        match = re.search(pattern, text, re.IGNORECASE)
        if match:
            return match.group(1).strip()
    return ""

# -----------------------------------------------
# Main extraction function
# Reads one PDF and returns a list of rows
# (one row per line item on the invoice)
# -----------------------------------------------
def extract_invoice(pdf_path):
    rows = []
    with pdfplumber.open(pdf_path) as pdf:
        full_text = ""
        for page in pdf.pages:
            full_text += page.extract_text() or ""

        # --- Header fields ---
        invoice_number = find_field(full_text, [
            r"Invoice\s*(?:Number|No\.?|#)\s*[:\-]?\s*(\S+)",
            r"INV[-\s]?(\d+)"
        ])
        invoice_date = find_field(full_text, [
            r"Invoice\s*Date\s*[:\-]?\s*([\d]{1,2}[\s/\-][\w]+[\s/\-][\d]{2,4})",
            r"Date\s*[:\-]?\s*([\d]{1,2}[\/\-][\d]{1,2}[\/\-][\d]{2,4})"
        ])
        due_date = find_field(full_text, [
            r"Due\s*Date\s*[:\-]?\s*([\d]{1,2}[\s/\-][\w]+[\s/\-][\d]{2,4})",
            r"Payment\s*Due\s*[:\-]?\s*([\d]{1,2}[\/\-][\d]{1,2}[\/\-][\d]{2,4})"
        ])
        vendor = find_field(full_text, [
            r"From\s*[:\-]?\s*(.+)",
            r"Vendor\s*[:\-]?\s*(.+)",
            r"Supplier\s*[:\-]?\s*(.+)"
        ])
        vat = find_field(full_text, [
            r"VAT\s*[:\-]?\s*([\d,\.]+)",
            r"Tax\s*[:\-]?\s*([\d,\.]+)",
            r"GST\s*[:\-]?\s*([\d,\.]+)"
        ])
        total = find_field(full_text, [
            r"Total\s*(?:Amount\s*)?[:\-]?\s*([\d,\.]+)",
            r"Amount\s*Due\s*[:\-]?\s*([\d,\.]+)",
            r"Grand\s*Total\s*[:\-]?\s*([\d,\.]+)"
        ])

        # --- Line items from tables ---
        # pdfplumber can extract tables directly
        line_items_found = False
        for page in pdf.pages:
            tables = page.extract_tables()
            for table in tables:
                for row in table:
                    # Skip header rows and empty rows
                    if not row or not any(row):
                        continue
                    if row[0] and row[0].lower() in ["description", "item", "service"]:
                        continue
                    description = row[0] if len(row) > 0 else ""
                    quantity    = row[1] if len(row) > 1 else ""
                    unit_price  = row[2] if len(row) > 2 else ""
                    if description:
                        rows.append({
                            "file": os.path.basename(pdf_path),
                            "invoice_number": invoice_number,
                            "invoice_date": invoice_date,
                            "due_date": due_date,
                            "vendor": vendor,
                            "description": description,
                            "quantity": quantity,
                            "unit_price": unit_price,
                            "vat": vat,
                            "total": total,
                        })
                        line_items_found = True

        # Fallback: if no table found, add one row with header fields only
        if not line_items_found:
            rows.append({
                "file": os.path.basename(pdf_path),
                "invoice_number": invoice_number,
                "invoice_date": invoice_date,
                "due_date": due_date,
                "vendor": vendor,
                "description": "",
                "quantity": "",
                "unit_price": "",
                "vat": vat,
                "total": total,
            })
    return rows

# -----------------------------------------------
# Build the Excel file
# -----------------------------------------------
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Invoices"

# Column headers
headers = [
    "File", "Invoice Number", "Invoice Date", "Due Date",
    "Vendor", "Description", "Quantity", "Unit Price", "VAT/Tax", "Total"
]
ws.append(headers)

# Process every PDF in the folder
pdf_files = [f for f in os.listdir(PDF_FOLDER) if f.lower().endswith(".pdf")]

if not pdf_files:
    print(f"No PDF files found in '{PDF_FOLDER}' folder.")
else:
    for filename in sorted(pdf_files):
        path = os.path.join(PDF_FOLDER, filename)
        print(f"Processing: {filename}")
        try:
            rows = extract_invoice(path)
            for row in rows:
                ws.append([
                    row["file"], row["invoice_number"], row["invoice_date"],
                    row["due_date"], row["vendor"], row["description"],
                    row["quantity"], row["unit_price"], row["vat"], row["total"]
                ])
        except Exception as e:
            print(f"  Error reading {filename}: {e}")

    wb.save(OUTPUT_FILE)
    print(f"\nDone. {len(pdf_files)} invoice(s) processed.")
    print(f"Results saved to: {OUTPUT_FILE}")

Step 2 — Set Up Your Folder Structure

2Put everything inside your python_portable folder

Your python_portable folder already contains everything you need. The structure looks like this:

Folder structure
python_portable/
├── RUN.bat              ← double-click this to run the extractor
├── invoice_extractor.py ← the script
├── input_pdfs/          ← put all your invoice PDFs in here
│   ├── invoice-001.pdf
│   ├── invoice-002.pdf
│   └── invoice-003.pdf
└── output_excels/       ← your extracted Excel file appears here
    └── invoices.xlsx

Copy all your invoice PDFs into the input_pdfs folder. The script reads from there and writes results into the output_excels folder automatically. You can drop in as many PDFs as you like — ten, a hundred, it makes no difference.

Step 3 — Double-click RUN.bat to Run the Script

3Double-click RUN.bat to extract your invoices

Open the python_portable folder. You will see an icon called RUN (a .bat file). Double-click it.

A black command prompt window opens automatically, processes every PDF in the input_pdfs folder, and closes when it is finished. You do not need to type anything.

When it is done, open the output_excels folder — your invoices.xlsx file is there, with one row per line item across all your invoices.

What you will see while it runs

Processing: invoice-001.pdf
Processing: invoice-002.pdf
Processing: invoice-003.pdf

Done. 3 invoice(s) processed.
Results saved to: output_excels/invoices.xlsx

Step 4 — If Some Fields Come Back Empty

The script uses pattern matching to find fields like “Invoice Number” and “Due Date.” It looks for common label formats — “Invoice No:”, “INV-“, “Due Date:”, “Total Amount:” and so on. Most invoices from standard accounting software use these exact labels, which is why the script works without configuration.

If a field comes back blank for a particular supplier’s invoices, it usually means that supplier uses a different label — for example “Reference” instead of “Invoice Number,” or “Payment Terms” instead of “Due Date.” Open that invoice PDF, find exactly what label the field uses, and add it to the matching patterns in the script. The section you need is clearly labelled with comments. Each pattern entry is one line — copy an existing one, change the label text, save, and run again.

Stuck? Use Claude to customise the script for you

If your invoices use unusual layouts, non-standard labels, or you want to extract additional fields the script does not currently cover, Claude (claude.ai) can help you refine and adapt the extractor. Paste the relevant section of the script into Claude, describe what your invoice looks like and what field is coming back blank, and ask it to update the pattern. For example: “My supplier’s invoices use ‘Ref:’ instead of ‘Invoice Number:’ — update this Python script to catch that.” Claude can also help you add entirely new columns, handle currency symbols, or reformat dates into a specific style for your spreadsheet.

Scanned invoices and handwritten invoices — OCR can help, but has real limits

If a field is blank for every invoice — not just some — it usually means the PDFs are scanned images rather than text-based. The test: open the PDF and try to highlight text with your cursor. If you cannot select any text, the file is a scanned image.

For scanned invoices, OCR (Optical Character Recognition) software such as Tesseract via the pytesseract library can attempt to read the text from the image before the extractor processes it. Honest assessment: it works reasonably well on clean, high-resolution scans of typed invoices — but accuracy drops noticeably with low-quality scans, unusual fonts, or poor lighting. For handwritten invoices, OCR accuracy is significantly worse. Handwriting varies so much between individuals that even the best OCR engines struggle, and a misread total or invoice number that ends up in your spreadsheet without you noticing it is worse than no extraction at all. If your invoices are handwritten or low-quality scans, manual entry or a dedicated invoice scanning service designed specifically for this purpose will give more reliable results than a general OCR approach. For clean digital scans, it is worth trying — but verify the output carefully before relying on it.

Customising the Script

Change the folder or output file name

At the top of the script, two lines control where the script looks and where it saves:

Python
PDF_FOLDER = "input_pdfs"                      # change this if your input folder has a different name
OUTPUT_FILE = "output_excels/invoices.xlsx"  # change this if you want a different output filename

You can use full paths if needed — for example “C:/Users/YourName/Documents/invoices” on Windows or “/Users/YourName/Documents/invoices” on Mac.

Add a new supplier label

If a supplier uses “Bill Number” instead of “Invoice Number,” find this section in the script and add a new pattern line:

Python
invoice_number = find_field(full_text, [
    r"Invoice\s*(?:Number|No\.?|#)\s*[:\-]?\s*(\S+)",
    r"INV[-\s]?(\d+)",
    r"Bill\s*Number\s*[:\-]?\s*(\S+)",  # ← add this line
])

Frequently Asked Questions

Will this work on Mac and Windows?

Yes — Python, pdfplumber, and openpyxl all run on Windows, Mac, and Linux. The only difference is which command you use to open the terminal and how you write folder paths. Both are covered in the steps above.

How many PDFs can it process at once?

There is no limit built into the script. It processes every PDF it finds in the invoices folder, one after another. A hundred PDFs takes a few seconds on a modern laptop. The output Excel file gets one row per line item, so a hundred invoices with three line items each produces three hundred rows.

What if my invoices are in subfolders?

The script only looks in the top-level invoices folder, not subfolders inside it. If your PDFs are organised into monthly subfolders, either move them all into the main invoices folder before running, or change the PDF_FOLDER path to point at the specific subfolder you want to process.

Will it overwrite my existing invoices.xlsx if I run it again?

Yes — each run creates a fresh Excel file, overwriting the previous one. If you want to keep previous results, rename the old file before running the script again, or change the OUTPUT_FILE value to a new name.

My invoice has multiple pages — will it read all of them?

Yes. The script loops through every page of every PDF before extracting. Multi-page invoices with line items spread across pages are handled correctly.

Can I run this automatically every day or week?

Yes. On Windows, use Task Scheduler to run python invoice_extractor.py on a schedule. On Mac, use cron or the built-in Automator. Point the script at a folder where new invoices land and it will process whatever is there each time it runs.

Mr Wangdoo

Clayton Samuel (Mr Wangdoo), QFA

Founder & editor, Wangdoo.com. Background in electronics, web development, and cloud infrastructure. The Python script in this guide was written and tested to confirm it runs without errors before publication.