Om factuurgegevens uit een PDF te halen met Python, lees je de tekst uit het bestand en haal je vervolgens benoemde velden uit die tekst. Twee stappen, misschien veertig regels code, en het werkt prachtig op de eerste factuur die je probeert. Dan verplaatst leverancier nummer zeven het factuurnummer drie regels naar boven en begint jouw parser om twee uur 's nachts None terug te geven.
Dat tweede deel is het eigenlijke onderwerp van dit artikel. Het lezen is makkelijk. Het correct blijven is het echte werk.
Belangrijkste punten
- Python haalt factuurtekst er in een paar regels uit. Het correct houden over honderden leverancierslay-outs is wat je daadwerkelijk tijd en geld kost.
- Een PDF is geen dataformaat. Het is een typografische beschrijving van een geprinte pagina, en daarom is een regex die is geschreven voor de lay-out van één leverancier, een erg kwetsbaar iets.
- Reguliere expressies en sjablonen per leverancier schalen lineair mee met je leverancierslijst. Vision-modellen doen dat niet, omdat ze de pagina lezen in plaats van de string.
- Wat de gegevens ook extraheert, jouw eigen code moet de berekeningen controleren. Line items die niet optellen tot het subtotaal zijn de goedkoopste bugdetector die je ooit zult schrijven.
- De extractie is zelden de reden waarom mensen stoppen met zelf bouwen. De uitzonderingenwachtrij, de leveranciersmatching en de boekhoudintegratie zijn dat wel.
Het PDF-formaat
Het PDF-formaat is veelzijdig en zorgt voor een nauwkeurige weergave van papieren documenten, zoals facturen, zonder beperkingen qua ontwerp. De oorsprong ligt in de papieren printwereld en het is bedoeld als digitale versie van een geprinte pagina. Deze flexibiliteit biedt veel vrijheid, zodat PDF-makers zich kunnen uitdrukken en kunnen voldoen aan verschillende standaarden en regelgeving.
De uitdaging ontstaat echter wanneer gegevens opgesloten zitten in een PDF. De vrije en complexe aard van het formaat kan botsen met de gestructureerde en consistente aanpak die nodig is om de enorme hoeveelheid gegevens te beheren die een bedrijf dagelijks verwerkt.

Een PDF slaat op waar elke glyph (teken) op de pagina staat. Het slaat niet het feit op dat het getal rechtsonder het totaal is. Die relatie leeft in jouw hoofd, en elke extractiemethode in dit artikel is een poging om dat te coderen.
Wat zijn de stappen om data uit een factuur te halen?
Een factuur wordt meestal als PDF aangeleverd. Een factuur formaliseert een transactie tussen een leverancier en een klant, waarbij een product of dienst wordt geleverd voor een nauwkeurig bedrag. Dit zijn de stappen die nodig zijn om gegevens uit dit document te halen:
- Definieer een schema voor de gegevens die je uit je facturen wilt halen
- Zet je factuur om van afbeelding naar tekst
- Haal de tekst uit je factuur op basis van je gegevenenschema
- Verzamel de geëxtraheerde gegevens

Definieer een schema voor je factuurgegevens
Facturen komen van verschillende leveranciers en iedere leverancier past de lay-out van zijn facturen vaak aan. Ondanks deze diversiteit in vorm is de inhoud van alle facturen in de basis hetzelfde: je hebt een leverancier, een klant, een factuurreferentie, een datum en een lijst van items met een bijbehorende hoeveelheid, beschrijving en prijs. Een geweldige manier om te beginnen met het definiëren van je factuurformaat is jouw boekhoudsoftware, aangezien je daar uiteindelijk hoogstwaarschijnlijk je geëxtraheerde factuurgegevens wilt opslaan, toch? Als je gewoon een dataformaat wilt dat alle uitzonderingen dekt, laat me dan de schema.org website aanbevelen, die handig een reeks industriestandaard dataformaten definieert voor heel veel dingen, inclusief facturen. Parseur definieert een standaard dataschema voor jouw facturen, maar je kunt dit aanpassen aan jouw use case door de velden in jouw factuurmailbox te hernoemen, zoals hier uitgelegd. Zodra je dataformaat is vastgelegd, kun je de factuur omzetten van afbeelding naar tekst.
Je kunt bijvoorbeeld de volgende velden voor je factuur definiëren in JSON Swagger-formaat:
{
"InvoiceNumber": {
"type": "string",
"description": "Het factuurnummer"
},
"InvoiceIssueDate": {
"type": "string",
"description": "De factuurdatum"
},
"Items": {
"type": "array",
"description": "De lijst met items op de factuur",
"items": {
"type": "object",
"properties": {
"quantity": {
"type": "number",
"description": "De hoeveelheid van het item"
},
"description": {
"type": "string",
"description": "De beschrijving van het item"
},
"unit_price": {
"type": "number",
"description": "De eenheidsprijs van het item"
},
"price": {
"type": "number",
"description": "De totale prijs van het item"
}
}
}
}
}
Schrijf dit op voordat je enige parsing-code schrijft. Het is het contract waaraan elke onderstaande methode moet voldoen, en het is wat je later aan een vision-model zult overhandigen.
Zet je factuur om van afbeelding naar tekst

Een PDF-bestand kan een afbeelding bevatten. Je werknemer kan bijvoorbeeld snel een foto van een factuur maken met diens smartphone. Ze slaan deze vervolgens op als PDF en sturen het naar je administratie. Jouw boekhoudteam is er dan verantwoordelijk voor om de gegevens uit deze factuur te halen en deze op de een of andere manier foutloos in je boekhoudsysteem te krijgen. De volgende stap is om deze afbeelding om te zetten naar tekst met behulp van een Optical Character Recognition systeem. Een van de populairste OCR-systemen is Tesseract. Tesseract is geschreven in C en C++. Om Tesseract vanuit ons Python-programma te gebruiken, hebben we een binding nodig, zoals PyTesseract. Een binding is een manier om een softwarebibliotheek (hier Tesseract) aan te roepen vanuit een taal waarin deze niet is geschreven (hier Python). Er bestaan veel van dergelijke systemen en de resultaten verschillen sterk, afhankelijk van hun onderliggende technologie en de kwaliteit van de scan van het document waaraan ze werken. Parseur detecteert transparant of je document een afbeelding is en zet het intern automatisch om naar tekst. Zodra de documentgegevens in tekstvorm zijn, is het klaar om geëxtraheerd te worden.
Haal de tekst uit je factuur op basis van je gegevenenschema
Zodra je PDF in tekst (of doorzoekbare) vorm is, kun je de pdftotext Python-bibliotheek gebruiken om de tekst uit het PDF-bestand te halen. Hier is een codefragment om tekst uit een PDF-bestand te halen:
import pdftotext
# Laad je factuur
with open("invoice.pdf", "rb") as file_handle:
pdf = pdftotext.PDF(file_handle)
# Itereer over alle pagina's
for page in pdf:
print(page)
Noem dit script convert_pdf_to_text.py en voer het uit, je krijgt de factuur als tekst naar de standaarduitvoer.
Als je de uitvoer naar een bestand wilt omleiden, kun je uitvoeren:
$ python convert_pdf_to_text.py > invoice.txt
pdftotext geeft je een platte string, wat prima is voor headervelden maar kansloos voor tabellen. Als je de line items nodig hebt, grijp dan in plaats daarvan naar pdfplumber, omdat dit de coördinaten van elk woord behoudt en een poging kan doen om de tabel zelf op te halen:
import pdfplumber
with pdfplumber.open("invoice.pdf") as pdf:
page = pdf.pages[0]
# Woorden met hun posities op de pagina
for word in page.extract_words():
print(word["text"], word["x0"], word["top"])
# En een poging om de tabel met line items op te halen
table = page.extract_table()
if table:
for row in table:
print(row)
Voer dat uit op een echte leveranciersfactuur en je zult heel vaak ontdekken dat table de waarde None heeft. Dat is geen bug. Het is het eerste eerlijke signaal dat dit probleem moeilijker is dan het lijkt, en daar komen we hieronder op terug.
Nu je de factuur in tekstvorm hebt, kun je de gewenste gegevens eruit halen met een combinatie van de volgende technieken:
- Je kunt een reguliere expressie gebruiken om de gewenste data te extraheren. Reguliere expressies zijn een krachtige manier om gegevens uit tekst te halen, maar ze zijn ook erg kwetsbaar. Als het factuurformaat verandert, moet je jouw reguliere expressie bijwerken. Ook zijn reguliere expressies niet erg goed in het extraheren van gegevens uit tabellen.
- Je kunt een visueel sjabloonsysteem gebruiken, ideaal gebruikmakend van Dynamische OCR en Zonale OCR. Dit is een meer geavanceerde manier om gegevens uit tekst te halen. Het is robuuster dan reguliere expressies, maar het is ook complexer om te implementeren.
- Je kunt de pagina aan een vision-model geven met een schema en het document laten lezen zoals een mens dat doet. Dit is de aanpak die de afgelopen twee jaar is veranderd, en het krijgt hieronder een eigen sectie.
Laten we gegevens uit je factuur halen met Python's reguliere expressies re module. Hier is een codefragment om het factuurnummer uit je factuur te halen:
import re
# Laad je factuur
with open("invoice.txt", "r") as file_handle:
invoice = file_handle.read()
# Haal het factuurnummer eruit
invoice_number = re.search(r"Invoice number: (\w+)", invoice).group(1)
print(invoice_number)
Noem dit script extract.py en voer het uit, je krijgt het factuurnummer naar de standaarduitvoer:
$ python extract.py
En je zult iets krijgen als:
INV-1234
Waarom jouw regex factuurparser breekt
Een regex matcht een string. Een factuur is een afbeelding. Alles wat fout gaat komt voort uit die mismatch, en het gaat fout op een klein aantal voorspelbare manieren:
- Het label is verplaatst. Jouw patroon is verankerd aan
Invoice number:en het nieuwe sjabloon zegtInvoice #, of zet de waarde op de volgende regel in plaats van op dezelfde. extract_tableretourneertNone. Tabelextractors zoeken naar scheidingslijnen. De meeste leveranciersfacturen lijnen hun kolommen uit met witruimte en tekenen helemaal geen randen.- De tekst komt er in de verkeerde volgorde uit. Tweekoloms lay-outs en zwevende adresblokken lopen door elkaar heen wanneer de pagina wordt afgevlakt naar een string, waardoor jouw rijen met line items door elkaar gehusseld aankomen.
- De tabel beslaat meerdere pagina's. Rijen twee tot en met negen staan op pagina één, tien tot veertien op pagina twee, met de kolomkoppen ertussenin herhaald en een subtotaalregel die zich voordoet als een item.
- Drie getallen zien er allemaal uit als het totaal. Subtotaal, totaal, verschuldigd bedrag en saldo. De grootste kiezen is fout op elke factuur die een tegoed (credit) bevat.
- De scan is een foto. OCR leest een vlekkerige
8als een3en niets verderop in het proces merkt dit op, omdat3een perfect geldig cijfer is. - Samengevoegde cellen en meerregelige beschrijvingen. Eén productbeschrijving loopt door over drie regels en jouw logica voor het splitsen van rijen verandert dit in drie items zonder prijzen.
Geen van deze problemen is op te lossen met een betere reguliere expressie. Het zijn allemaal lay-outproblemen vermomd als een stringprobleem. Dit is het punt waarop de meeste mensen ofwel beginnen met het schrijven van één sjabloon per leverancier, wat eindeloos blijft groeien, ofwel van aanpak veranderen.
De aanpak van 2026 - een vision-model en een schema
De nuttige verschuiving is dat je de pagina niet langer hoeft af te vlakken naar tekst voordat je eruit extraheert. Een vision-model kijkt naar de gerenderde factuur, dus een leverancier die zijn factuurnummer verplaatst is niet langer een incident. Wat jij aanlevert is geen patroon, maar het schema dat je bovenaan dit artikel hebt geschreven.
Beperk de output zodat je elke keer dezelfde sleutels (keys) krijgt. Pydantic plus een structured-output modus, zoals OpenAI's structured outputs, doet dat voor je:
from typing import List
from pydantic import BaseModel
class LineItem(BaseModel):
description: str
quantity: float
unit_price: float
amount: float
class Invoice(BaseModel):
vendor_name: str
invoice_number: str
invoice_date: str # ISO 8601
currency: str
subtotal: float
tax: float
total: float
line_items: List[LineItem]
# Render de PDF-pagina naar een afbeelding, stuur het naar een vision-model,
# en vereis dat de respons overeenkomt met het Invoice schema.
# Het model vult de velden in. Het mag de vorm niet zelf verzinnen.
Daarmee is oprecht het grootste deel van het extractieprobleem opgelost, en daarom heeft deze aanpak zich zo snel verspreid. Het introduceert ook een nieuwe storingsmodus die regex nooit had: een regex die het factuurnummer niet kan vinden retourneert None, terwijl een model dat het niet kan vinden soms een aannemelijk nummer zal verzinnen. De regel die jou veilig houdt is simpel. Het model stelt voor en jouw code verifieert.
Als je het model liever niet zelf draait, wordt dezelfde mogelijkheid als een beheerde service (managed service) verkocht door de cloudproviders, in Azure AI Document Intelligence en Amazon Textract's AnalyzeExpense, die beide headervelden en line items afzonderlijk retourneren. We hebben beschreven hoe de onderliggende aanpak verschilt van op regels gebaseerde parsing in AI versus op regels gebaseerde PDF-parsers, en hoe dit specifiek wordt toegepast op facturen in vision AI factuurverwerking.
De validatielaag die je zelf moet schrijven
Wat jouw JSON ook heeft geproduceerd, deze controles horen thuis in jouw code, niet in de betrouwbaarheidsscore van de extractor. Ze zijn goedkoop, deterministisch en ze vangen de fouten af die geld kosten:
subtotal + tax + shipping - discountkomt exact uit optotal, tot op een cent nauwkeurig.- De bedragen van de line items tellen op tot het subtotaal. Doen ze dat niet, dan heb je een rij gemist of er een verzonnen.
- Elke
quantity * unit_priceis gelijk aan zijn eigenamount. - De datum kan worden geparst en ligt niet in de toekomst.
- Het factuurnummer is nog niet betaald voor die leverancier. Dubbele betalingen zijn de duurste bug in crediteurenbeheer (accounts payable).
- De naam van de leverancier verwijst naar een record in jouw leveranciersbestand.
- De bankgegevens komen overeen met de gegevens die al in het systeem staan voor die leverancier. Een wijziging hier is een fraudecontrole, geen datacontrole.
- De valuta is er daadwerkelijk een waarin je handelt.
- Het inkoopordernummer bestaat en de hoeveelheden en prijzen komen overeen als je two-way of three-way matching uitvoert.
- Elk vereist veld is aanwezig en niet-leeg.
Alles wat mislukt, gaat naar een mens, niet naar de boekhouding:
def validate(invoice):
errors = []
if abs(invoice.subtotal + invoice.tax - invoice.total) > 0.01:
errors.append("totals_do_not_add_up")
line_sum = sum(item.amount for item in invoice.line_items)
if invoice.line_items and abs(line_sum - invoice.subtotal) > 0.01:
errors.append("line_items_do_not_sum_to_subtotal")
if not invoice.invoice_number:
errors.append("missing_invoice_number")
return errors
Tien regels rekenwerk zullen meer echte problemen opsporen dan welke hoeveelheid prompt-tuning dan ook.
Hoe zit het met invoice2data en de andere bibliotheken?
invoice2data verdient een eerlijke vermelding, omdat het het eerste resultaat is waar veel mensen op landen en het echt een goed stuk software is. Het is een command line tool en Python-bibliotheek die facturen matcht met YAML-sjablonen die je per leverancier schrijft, waarbij de matchingregels in versiebeheer staan in plaats van verborgen in jouw code. Als je een dozijn leveranciers hebt die hun lay-out nooit veranderen, zal het je jarenlang goed van dienst zijn.
De beperking zit in het ontwerp, niet in de kwaliteit. Eén sjabloon per leverancier betekent dat jouw onderhoud meegroeit met jouw leverancierslijst, en de sjablonen breken om precies de redenen die hierboven zijn genoemd. Ergens tussen de twintig en dertig actieve lay-outs doet de persoon die de sjablonen onderhoudt meer werk dan de persoon die voorheen de facturen overtypte.
Dezelfde redenering is van toepassing op de bredere plank met bibliotheken. pdfplumber, PyMuPDF, pdftotext en pytesseract zijn allemaal uitstekend in hun eigenlijke werk, namelijk karakters en coördinaten van een pagina halen. Geen van hen was ooit bedoeld om te weten welk nummer het totaal is. We vergelijken de bredere set in ons overzicht van de beste PDF-parsers en de beste data-extractie API's.
Verzamel de geëxtraheerde gegevens
Met Python kun je over de factuurbestanden in een map itereren en de gewenste gegevens extraheren. Laten we zeggen dat we het factuurnummer en het totaalbedrag extraheren en het resultaat als CSV exporteren:
import os
import re
import pdftotext
# Loop over alle PDF-bestanden in de map
for filename in os.listdir("invoices/"):
if not filename.endswith(".pdf"):
continue
# Laad je factuur
with open("invoices/" + filename, "rb") as file_handle:
pdf = pdftotext.PDF(file_handle)
# Print de CSV-kopregel
print("InvoiceNumber,TotalAmount")
# Itereer over alle pagina's
for page in pdf:
# Haal het factuurnummer eruit
invoice_number = re.search(r"Invoice number: (\w+)", page).group(1)
total_amount = re.search(r"Total amount: (\w+)", page).group(1)
print(invoice_number, total_amount, sep=",")
Noem dit script extract_to_csv.py en voer het uit, je krijgt het factuurnummer en totaalbedrag naar de standaarduitvoer,
die je kunt omleiden naar een CSV-bestand dat je later met je favoriete spreadsheetsoftware als Excel kunt openen:
$ python extract_to_csv.py > invoices.csv
Een map met CSV-bestanden is waar de meeste factuurscripts stoppen, en het is ook waar de echte boekhouding begint. Iemand moet ze nog steeds importeren, de leveranciers matchen, achter de rijen aan gaan die door het boekhoudsysteem zijn afgewezen, en uitzoeken wat er moet gebeuren met de factuur die de validatie om 2 uur 's nachts niet heeft doorstaan. Dat werk staat niet in jouw script en het staat ook niet op jouw factuur voor AI-tokens.
Wanneer je moet stoppen met zelf bouwen
Hier is het deel dat de meeste leveranciers overslaan, dus wij zeggen het als eerste. Als je een handvol vaste leveranciers hebt, een engineer die een pipeline in de gaten kan houden en er niemand zit te wachten op de data, schrijf dan het script. Een vision-model plus een schema plus de tien regels validatie hierboven brengen je een heel eind voor heel weinig geld, en je zult elk onderdeel ervan begrijpen.
De rekening voor het bouwen komt later, en nooit in de extractie. Die komt in de onderdelen die niemand prototypeert:
- De uitzonderingenwachtrij. Jouw AP-team heeft een scherm nodig waar het klikken op een veld dit op de factuur markeert, zodat ze het in vier seconden kunnen oplossen in plaats van veertig. Dat is een product, geen script.
- Leveranciersmatching. "ACME Ltd", "Acme Limited" en "ACME LTD." zijn één leverancier, en de grootboekrekening accepteert er geen drie.
- Duplicaatdetectie. Dezelfde factuur komt op dinsdag aan als e-mailbijlage en op vrijdag als een PDF-afschrift.
- De state machine. Wachtrijen, retries (opnieuw proberen), gedeeltelijke fouten en weten welke van de 300 facturen van gisteravond er daadwerkelijk doorheen zijn gekomen.
- De audit trail. Wat er is geëxtraheerd, wat een mens heeft gewijzigd, wie het heeft goedgekeurd en wanneer. Financiën zal ernaar vragen, meestal tijdens een audit.
- Alles ná de JSON. Veldmapping naar het boekhoudsysteem, GL-codering (grootboek), matching van inkooporders, en de rijen die het afwijst.
De duidelijke signalen dat het tijd is om te kopen in plaats van te bouwen zijn deze. Je bent de ruwweg twintig tot dertig actieve leverancierslay-outs gepasseerd. Je hebt line items nodig, niet alleen headervelden. Meer dan een handvol van jouw facturen komt als scans binnen. Jouw AP-team, niet jouw engineeringteam, moet de fouten herstellen. Mislukte extracties vertragen betalingen. Of, en dit komt het meest voor, de persoon die de parser onderhoudt, is gestopt met het opleveren van andere dingen.
Hoe Parseur hiermee omgaat
Parseur is een documentparser die de hele cyclus uitvoert, niet alleen de extractiestap. Facturen komen aan op een speciaal mailboxadres, via de API of vanuit een bewaakte map. De Vision AI engine leest PDF's, scans en foto's, en de Text AI engine leest e-mails en tekstdocumenten. Velden komen er benoemd en getypeerd uit, en line items verschijnen als rijen. Er zijn geen sjablonen om te schrijven en niets om te onderhouden wanneer een leverancier het ontwerp van zijn factuur aanpast.
Wat je bovenop de JSON krijgt, is de helft waar dit artikel je al voor heeft gewaarschuwd. Geëxtraheerde gegevens kunnen ter plekke worden beoordeeld en gecorrigeerd, zodat een medewerker crediteurenbeheer een verkeerd gelezen totaal corrigeert zonder een ticket aan te maken. Correcties worden teruggekoppeld naar de extractie. En de gegevens gaan waar ze heen moeten via directe webhook-integratie, Make, Zapier of Microsoft Power Automate, of rechtstreeks vanuit de API als JSON.
Als je de set velden wilt zien die we standaard uit facturen halen, dan is dat gedocumenteerd op onze factuur OCR pagina, en de bredere workflow wordt behandeld in factuur data capture.
Conclusie
Het extraheren van factuurgegevens uit een PDF met Python is een opgelost probleem voor ongeveer honderd facturen en een onopgelost probleem voor tienduizend. De code is niet wat er verandert tussen die twee getallen. Wat verandert, is hoeveel leverancierslay-outs jij stilletjes belooft te onderhouden, en wie er wordt opgeroepen wanneer een ervan verschuift.
Schrijf het script. Het is oprecht de moeite waard om één keer te doen, al was het maar om er precies achter te komen welke van de zeven bovenstaande foutmodi jou als eerste treft. Beslis daarna eerlijk of de komende zes maanden van jouw tijd het beste besteed zijn aan de achtste. Als het antwoord nee is, doet Parseur dit al sinds 2016 en neemt het de map graag van je over.
Laatst bijgewerkt op





