PDF parsen

  Ralf Hersel   Lesezeit: 7 Minuten Auf Mastodon ansehen

Manchmal muss man PDF-Dateien auslesen. Dieser Artikel zeigt, wie man das mit einem Python-Skript macht.

pdf parsen

Am liebsten schreibe ich Artikel aus dem prallen Leben, also über persönliche oder berufliche Erfahrungen. So auch bei diesem Beitrag. Wir erhalten Pfarraufträge vom Bestattungsamt der Stadt Zürich im PDF-Format. Obwohl wir eine digitale Schnittstelle zum Bestattungsamt haben, werden darüber nicht alle Aufträge übertragen. Ich erspare euch die Details. Für unsere neue Kasualienverwaltung (Taufe, Segnung, Konfirmation, Trauung, Abdankung) müssen die Daten vollständig und detailliert übertragen werden, damit die Register (Kirchenbücher) korrekt generiert werden können. Diese Register sind wichtig, weil sie über Jahrhunderte hinweg Auskunft über familiäre Verläufe belegen. Die Einsicht in die Kirchenbücher wird von der Bevölkerung oft angefragt.

Da eine verbesserte API zwischen der Stadt und der Reformierten Kirchgemeinde Zürich in Arbeit ist (die Stadt braucht jahrelang dafür), brauche ich eine Zwischenlösung. Niemand möchte Daten manuell von einer PDF-Datei in eine Anwendungsmaske übertragen. Deshalb habe ich einen Parser geschrieben, der die PDF-Dateien ausliest und in die Anwendung einliest.

Wer meine Artikel liest, weiss, dass das PDF-Format zu meinen "Lieblingen" gehört: PDF - das Format aus der Hölle, was sich beim Schreiben des Parsers bestätigt hat. Zur Verdeutlichung sei gesagt, dass es PDF-Formulare gibt, aus denen man einfach strukturierte Daten auslesen kann. Doch die normalen PDF-Dateien bestehen aus einer unstrukturierten Bleiwüste.

Aus Datenschutzgründen kann ich nur dieses Dokument zeigen, in dem alle kritischen Daten gelöscht sind. Ein Pfarrauftrag (Bestattungsauftrag) sieht so aus:

Den Parser habe ich in Python geschrieben. Das Skript verwendet die Library pypdf, mit der eine PDF-Datei in Text umgewandelt wird. Das Grundgerüst des Python-Skripts sieht so aus:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Name:           pfarrauftrag.py
Description:    Extract text from Pfarrauftrag PDF
Author:         Ralf Hersel
License:        GPL3
Date:           23.04.2025
Version:        0.02

""" 

# === Import ===================================================================

from pypdf import PdfReader                                 # pip install pypdf

# === Constants ================================================================

TAB = 30

Neben dem Shebang und der Coding-Angabe, gibt es ein paar Meta-Infos zum Skript. Danach importiere ich die Python-Bibliothek pypdf und setze die Konstante TAB, mit der die Distanz für die Textausgabe bestimmt wird. Der Main-Teil sieht so aus:

# === Main =====================================================================

def main(args):
    try:
        pdf_file = args[1]
    except IndexError:
        print("Missing PDF filename")
        exit(1)
    pdf = read_pdf(pdf_file)
    txt = get_text(pdf)
    print(txt)
    print('-------------------------------------------------------------------')
    parse_text(txt)
    return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))

Das ist überwiegend Boilerplate-Code. Am Anfang prüfe ich, ob eine PDF-Datei als Parameter beim Aufruf des Skripts mitgegeben wurde; falls nicht, bricht das Skript mit einer Fehlermeldung ab. Dann importiere ich die PDF-Datei. Dieser Code lautet:

def read_pdf(pdf_file):
    pdf = PdfReader(pdf_file)
    return pdf

Ja, es ist nur eine Zeile Code, weshalb ich mir die Funktion hätte sparen können. Dann extrahiere ich den Text aus der PDF-Datei:

def get_text(pdf):
    txt = ''
    for page in pdf.pages:
        txt += page.extract_text()
    return txt

Hier muss man über die Seiten des PDF-Dokuments iterieren. Die Inhalte werden in der Variablen txt zusammengefasst. Nachdem der gesamte Text der PDF-Datei jetzt in der Variable txt vorliegt, kann der Parser loslegen. Es gibt verschiedene Verfahren dafür; ich habe mich für eine Schlüsselwortsuche entschieden. Dabei wird der Text nach Start- bzw. Stopp-Wörtern durchsucht. Ein zeilenbasierter Ansatz funktioniert hier nicht, weil Anzahl und Position der Zeilen im PDF nicht eindeutig sind. Ich beginne mit einem einfachen Beispiel:

field = "Bestattungstermin"
search = "Termin der Bestattung"
start = txt.find(search) + len(search) + 1
stop = txt.find("\n", start)
value = txt[start:stop]

Hierbei wird das Datum der Bestattung gesucht. Der Suchbegriff lautet "Termin der Bestattung". Zu der gefundenen Startposition wird die Länge des Suchbegriffs addiert, weil dieser nicht zum Inhalt gehört. Als Stopp-Wort dient das Zeilenende. Den Wert ermittle ich durch Slicing: txt[start:stop]. Hier ist ein Textausschnitt, damit ihr es euch besser vorstellen könnt:

Friedhof, Adresse Friedhof Rehalp, Forchstrasse 384, 8008 Zürich
Termin der Bestattung Freitag, 12.07.2024, 13.30
Abdankungsort Friedhofkapelle Enzenbühl, Forchstrasse 384, 8008 Zürich

Der gefundene Wert lautet "Freitag, 12.07.2024, 13.30". Manchmal verteilt sich der Wert über mehrere Zeilen, wie man am Abdankungsort sehen kann:

Termin der Bestattung Freitag, 15.01.2021, 14.00
Abdankungsort Friedhofkapelle Hönggerberg, Notzenschürlistr. 30, 8049 Zü-
rich
Datum/Zeit Freitag, 15.01.2021, 14.00

Hier ist der Code für diesen Fall:

field = "Abdankungsort"
search = "Abdankungsort"
start = txt.find(search) + len(search) + 1
stop = txt.find("Datum/Zeit", start)
value = txt[start:stop]
value = value.replace('\n', '')[:-2]

Dabei wird "Datum/Zeit" als Stopp-Wort verwendet, also der Feldname des darauffolgenden Feldes. Ausserdem werden die Zeilenumbrüche im gefundenen Wert entfernt. Schwieriger wird die Sache, wenn der Suchbegriff mehrmals im Text vorkommt, aber nur einer davon der richtige ist. Das ist beim Geburtsdatum der Fall. Der Feldname lautet "geb.". Leider kann dieser Text auch an anderen Stellen vorkommen, z. B. bei "Meier, geb. Müller". Deshalb ist eine iterierende Suche mit Überprüfung des gefundenen Wertes erforderlich:

field = "Geboren"
search = "geb."
found = False
last_start = 0
while not found and last_start >= 0:     
    start = txt.find(search, last_start) + len(search) + 1
    stop = txt.find("gest.", start)
    value = txt[start:stop]
    if value[0].isnumeric():
        found = True
    else:
        last_start = start

Mittels einer while-Schleife wird die Suche so lange wiederholt, bis der richtige Wert gefunden wurde. Die Variable found dient als Abbruchkriterium. Ausserdem wird die letzte Startposition behalten, damit die Suche nicht jedes Mal am Anfang des Textes beginnt. Ob der richtige Wert gefunden wurde, überprüfe ich mit dem Test, ob das erste Zeichen des Wertes numerisch ist. Um eine Endlosschleife zu vermeiden, wird in der Bedingung der while-Schleife auch geprüft (last_start >= 0), ob die Suche überhaupt etwas gefunden hat (find liefert -1, falls nichts gefunden wird).

Mit diesen Funktionen und leichten Abwandlungen konnte ich alle 23 Felder der PDF-Datei auslesen. Vielleicht hilft euch dieser Artikel, falls ihr selbst einmal eine unstrukturierte PDF-Datei auslesen möchtet.

Titelbild (verändert): https://pixabay.com/photos/sorrow-grief-mourn-sad-funeral-4900424/

Quelle: keine

Tags

PDF, Parser, parsen, Auslesen, pypdf

Es wurden noch keine Kommentare verfasst, sei der erste!