Gegevens uit facturen halen met PDF en Python

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.

A screen capture of PDF file format layers
PDF file format layers

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:

  1. Definieer een schema voor de gegevens die je uit je facturen wilt halen
  2. Zet je factuur om van afbeelding naar tekst
  3. Haal de tekst uit je factuur op basis van je gegevenenschema
  4. Verzamel de geëxtraheerde gegevens

A screen capture of Invoice data extraction process
Invoice data extraction process

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

A screen capture of an invoice taken from a smartphone
Picture of an invoice taken from a smartphone

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 zegt Invoice #, of zet de waarde op de volgende regel in plaats van op dezelfde.
  • extract_table retourneert None. 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 8 als een 3 en niets verderop in het proces merkt dit op, omdat 3 een 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:

  1. subtotal + tax + shipping - discount komt exact uit op total, tot op een cent nauwkeurig.
  2. 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.
  3. Elke quantity * unit_price is gelijk aan zijn eigen amount.
  4. De datum kan worden geparst en ligt niet in de toekomst.
  5. Het factuurnummer is nog niet betaald voor die leverancier. Dubbele betalingen zijn de duurste bug in crediteurenbeheer (accounts payable).
  6. De naam van de leverancier verwijst naar een record in jouw leveranciersbestand.
  7. De bankgegevens komen overeen met de gegevens die al in het systeem staan voor die leverancier. Een wijziging hier is een fraudecontrole, geen datacontrole.
  8. De valuta is er daadwerkelijk een waarin je handelt.
  9. Het inkoopordernummer bestaat en de hoeveelheden en prijzen komen overeen als je two-way of three-way matching uitvoert.
  10. 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.

Maak een gratis account aan
Bespaar tijd en moeite met Parseur. Automatiseer je documenten.

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

Aan de slag

Klaar om je data-extractie
uit documenten te automatiseren?

Start gratis in een paar minuten en ontdek hoe Parseur in jouw workflow past.

Geen modeltraining nodig
Automatiseert data-invoer uit elk document
Schaalbaar van point-and-click tot API

Veelgestelde Vragen

De vragen die developers daadwerkelijk stellen zodra het eerste factuurscript draait en de tweede leverancier het kapot heeft gemaakt.

Lees de tekst uit de PDF met een bibliotheek zoals pdfplumber of pdftotext en haal vervolgens benoemde velden uit die tekst. Voor 'born-digital' (digitaal gecreëerde) PDF's is dat een taak van twee stappen. Voor scans heb je eerst een OCR-stap nodig om de afbeelding om te zetten in tekst. Het deel dat bepaalt of het in productie werkt, is niet het lezen, het is hoe je van een muur van tekst naar factuurnummer, datum, leverancier en line items gaat wanneer elke leverancier ze anders opmaakt.

invoice2data is een open-source command line tool en Python-bibliotheek die velden uit facturen extraheert met behulp van YAML-sjablonen die je per leverancier schrijft. Het is een goede keuze wanneer je een kleine, stabiele set leveranciers hebt en je de matchinglogica in versiebeheer wilt hebben in plaats van in je code. Het is niet langer een goede keuze op het moment dat het schrijven en onderhouden van één sjabloon per leverancier meer kost dan het handmatige overtypen dat het heeft vervangen.

Behandel de tabel met line items als een afzonderlijke extractie van de headervelden, en reconstrueer deze over de pagina's heen voordat je deze parst. extract_table van pdfplumber werkt op strak gelinieerde tabellen en retourneert niets bij randloze tabellen, wat bij de meeste leveranciersfacturen het geval is. Het betrouwbare patroon is om de kolomgrenzen één keer per leverancierslay-out te detecteren, deze door te trekken over pagina-overgangen, herhaalde rijen met koppen te laten vallen, en vervolgens te controleren of de geëxtraheerde rijen optellen tot het subtotaal. Doen ze dat niet, dan heb je een rij gemist.

Met deterministische controles in je eigen code, nooit alleen met de betrouwbaarheidsscore van de extractor. De kernset is rekenwerk en identiteit. Bevestig dat subtotaal plus belasting plus verzending minus korting exact uitkomt op het totaal, dat de line items optellen tot het subtotaal, dat de datum klopt en niet in de toekomst ligt, dat het factuurnummer nog niet is betaald voor die leverancier, dat de leverancier in jouw leveranciersbestand staat en dat de valuta er een is die je verwachtte. Alles wat mislukt, gaat naar een mens in plaats van naar de boekhouding.

Wanneer de extractie niet langer het moeilijkste deel is. Het aanroepen van een vision-model is goedkoop en snel om te prototypen, dus als je een handvol vaste leveranciers hebt en iemand die de pipeline in de gaten kan houden, bouw het dan. De rekening komt later, in de wachtrij- en retry-logica, het scherm voor uitzonderingsbeoordeling dat jouw AP-team (crediteuren) nodig heeft, de duplicaatdetectie, de leveranciersmatching, het controlespoor (audit trail) en de boekhoudintegratie. Tel die op voordat je prijzen per pagina vergelijkt.

Ja, maar een scan heeft een OCR-stap nodig voordat de tekstbibliotheken iets kunnen zien. Een gefotografeerde of gescande factuur is een afbeelding verpakt in een PDF, dus pdfplumber en pdftotext retourneren hierbij een lege string. De gebruikelijke open-source route is Tesseract via de pytesseract-binding, na het rechtzetten (deskewing) en opschonen van de afbeelding. Moderne vision-modellen slaan die stap volledig over en lezen de afbeelding direct.

Er is geen specifieke factuurbibliotheek, alleen PDF-bibliotheken plus jouw eigen logica. pdfplumber is de gebruikelijke eerste keuze omdat het woorden met hun coördinaten blootlegt en een tabelextractor heeft. PyMuPDF is sneller bij grote batches. pdftotext is het kleinste wat werkt als je alleen de onbewerkte tekst nodig hebt. pytesseract behandelt scans. Elk van hen geeft je tekst of boxen. Geen van hen vertelt je welk getal het totaal is.

Omdat een regex een string matcht en een factuur een afbeelding is. Jouw patroon is verankerd aan een label, een regelafbreking of een kolompositie die door de nieuwe sjabloon van de leverancier is verplaatst. Facturen zijn semi-gestructureerde visuele documenten, dus de dingen die jouw proces breken zijn lay-outproblemen in plaats van stringproblemen. Tabellen die over meerdere pagina's worden gesplitst, herhaalde kolomkoppen, samengevoegde cellen en het verschil tussen subtotaal, totaal en verschuldigd bedrag zijn niet op te lossen met een betere reguliere expressie.

Ja, en het is nu de kortste weg van een PDF naar gestructureerde JSON, maar alleen met een schema en een validator eromheen. Een vision-model leest de factuur zoals een mens dat doet, dus een leverancier die zijn lay-out verandert, is niet langer een incident. Beperk de output met een JSON-schema, met zoiets als OpenAI structured outputs of een Pydantic model, zodat je elke keer dezelfde sleutels (keys) krijgt. Controleer daarna zelf de berekeningen. Een model dat het factuurnummer niet kan vinden, zal er soms een verzinnen dat aannemelijk lijkt.

Nauwkeurigheid hangt af van het veld en de scan, niet van het product, dus beschouw elk enkelvoudig percentage in krantenkoppen als marketing. Gedrukte totalen en factuurnummers op een schone, digitaal gecreëerde PDF komen vrijwel perfect terug. Dezelfde velden op een gefotografeerde, scheve scan met weinig contrast zijn de plekken waar cijfers door elkaar worden gehaald en waar een verkeerd teken echt geld kost. Het getal dat de moeite waard is om te meten, is niet de teken-nauwkeurigheid, maar hoeveel facturen jouw boekhoudsysteem bereiken zonder dat een mens ze hoeft aan te raken.

Je stopt met het schrijven van iets per leverancier. Elke aanpak die een sjabloon, een regex-set of een coördinatenkaart nodig heeft voor elke leverancier, groeit lineair met jouw leverancierslijst en verlaagt de werkdruk van niemand na ongeveer twintig tot dertig actieve lay-outs. Een model dat het document visueel leest, is door het ontwerp onafhankelijk van het formaat, en daarom is 'lay-out drift' (verschuiving in lay-out) geen onderhoudsincident meer. Wat wel nog jouw aandacht nodig heeft is de wachtrij met uitzonderingen, en dat is een workflowprobleem in plaats van een extractieprobleem.

Een CSV schrijven is een paar regels Python en het is de makkelijke helft. De moeilijke helft is het krijgen van de gegevens in het systeem dat de rekening betaalt, wat betekent dat je jouw veldnamen in kaart brengt naar het schema ervan, leveranciers matcht met bestaande records, omgaat met de rijen die het systeem afwijst en de mislukte API-calls opnieuw probeert. Plan voor die helft vanaf het begin, want een map met CSV-bestanden die nog steeds door iemand met de hand wordt geïmporteerd, heeft niemand een middag bespaard.