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
ich verwende gerne pdfgrep - kann man einfach über den paketmanager installieren. näheres unter: https://pdfgrep.org/
Hi Ralf
Danke für den Einblick. Vielleicht für mich Motivation, doch noch Python reinzuziehen.
Solche Übungen mit PDF haben wir schon vor 30 Jahren veranstaltet. s/w Charts aus einem proprietären System mit Scripts aufgehübscht.
Wir haben auch ESC, Postscript, Prescribe, PCL Datenströme frisiert.
Man gedenke den damals verwendeten Tools: sed, awk, m4, rexx, zip.
Solche wie von Dir beschriebenen Medien-Brüche gibt es immer noch zuhauf. Hotel-Buchungen. Branchenlösungen. Bei letzteren bleibt oft nur "ausdrucken" - obwohl die Anbieter sterben - bis zu 5 hier pro Jahr - und die Datenstruktur nicht rausrücken. Ein ERP ohne SQL Zugang ist ein No-Go.
Vor zwei Jahren habe ich was mitgekriegt. Der letzte und einzige Kunde hätte für die DB-Beschreibung zwei Millionen bezahlen sollen.
Zum Glück gibt es reine Text Exports.
Nächstens scheppert was. Ist zwar MS-SQL... Aber das tu ich mir nicht mehr an.
Take care. Und danke für Deine Arbeit
Dani
Man könnte auch versuchen die PDF-Datei mit einer OCR Software zu konvertieren.
Vorschlag für Tools: Das pdf blattweise in Bitmaps konvertieren (Werkzeug pdftoppm aus dem Paket poppler, dpi Zahl ist einstellbar), Dannach auf die gewünschte Seite die OCR tesseract laufen lassen (dazu ggf. geeignete Sprachpakete für tesseract installieren). Das Ergebnis wäre eine Textdatei. Tesseract bietet auch einiges an Optionen. Ein Vorteil dieser Methode wäre, dass falls das beschriebene Text extrahieren nicht greift, weil ein pdf ja code ist, dass diese Methode eventuell noch Resultate liefern könnte.
Disclaimer: ich kenne tesseract nicht im Speziellen, und kann nicht viel zur Qualität sagen. Ich hab das aber an einem einfachen Beispiel ausprobiert
> txt += page.extract_text()
Wenn du "C" kennst, "+=" verhält sich wie Arrays und "append()" wie Linked List.
Das heißt bei "+=" werden immer zwei Arrays erzeugt, ein Quell-Array und ein Ziel-Array und dann werden die Daten hin- und herkopiert.
Das hast du nicht mit "append()".
(Intern ist die Datenstruktur hinter "+=" und "append()" dieselbe. Mir geht es hierbei auch eher um das Verhalten.)