Werkzeuge

Hier gebe ich Einblick in meinen Werkzeugkasten. Mit Werkzeugen meine ich Dinge, die einem als Projektleiter und Business Analyst die Arbeit erleichtern. Das reicht von hilfreichen Konzepten bis hin zu Excel-Dateien oder Programmen.

PDF-Formulare analysieren mit Python

Wozu ist das gut?

Wenn der Öffentliche Sektor seine Leistungen online anbieten will, gibt es meist schon entsprechende Antragsformulare in Papierform oder als Datei zum Herunterladen. Gedruckte und heruntergeladene Variante basieren in der Regel auf einer Vorlage im Portable Document Format (PDF). PDF-Dateien sind daher häufig ein Ausgangspunkt beim Entwurf von digitalen Antragsstrecken.

Allerdings ist es mühsam, aus PDF-Dateien diejenigen Informationen herauszuklauben, die man für die Spezifikation einer Antragsstrecke braucht, z.B. :

  • Gliederung des Formulars in Abschnitte
  • Eingabefelder mit ihren Bezeichnungen und erlaubten Eingaben
  • Zusatzinformationen, die beim korrekten Ausfüllen helfen sollen

In diesem Beitrag beschreibe ich ein Python-Skript, das einem langweilige Routineaufgaben bei der Analyse von PDF-Dateien abnimmt.

Die Analyse erfolgt schrittweise

Die Analyse eines PDF-Formulars erfolgt in mehreren Schritten:

import json
from pathlib import Path

# Konfiguration einlesen
with open("config.json", "r") as f:
    config = json.load(f)

pdf_dateien = Path(config['input_dir']).glob('**/*.pdf')
for pdf_datei in pdf_dateien:
    
    print("Analysiere: ", pdf_datei)
    pages = Text_extrahieren(pdf_datei)
    data, columns = Text_zu_Tabelle(pages)
    df = Felder_identifizieren(data, columns)
    df = Abschnitte_identifizieren(df)
    df = Zeilen_klassifizieren(df)

    excel_datei = Path(f'{pdf_datei}.xlsx')
    print("Ergebnis:   ", excel_datei, "\n")
    Dataframe_schreiben(df, excel_datei)

Das Skript erwartet die zu analysierenden PDF-Dateien in einem Verzeichnis, das man in der Konfigurationsdatei config.json als Parameter input_dir angeben kann. Die Python-Bibliothek pathlib ermöglich es, unabhängig von den Konventionen des verwendeten Betriebssystems (Windows, Linux usw.) über die PDF-Dateien zu iterieren und sie in folgenden Schritten zu analysieren:

  1. Texte aus dem PDF-Formular extrahieren und in eine tabellarische Form bringen, wobei die Texte seitenweise von rechts oben nach links unten ausgewertet werden
  2. Bedeutung der Texte analysieren

Beim zweiten Schritt nutzt man aus, dass PDF-Formulare oft eine Struktur aufweisen, die etwas über die Bedeutung der extrahierten Texte aussagt:

  1. Überschriften von Formular-Abschnitten können durchnummeriert sein, entweder mit Ziffern oder mit Buchstaben
  2. Felder können ebenfalls fortlaufend nummeriert sein

Hier ein Beispiel, in dem man Überschriften und Eingabefelder an ihrer fortlaufenden Nummerierung erkennen kann:

Formular mit nummerierten Überschriften und Eingabefeldern

Je PDF-Formular speichert das Skript die Analyse-Ergebnisse in einer Excel-Datei, deren Name sich aus dem der PDF-Datei plus angehängtem Suffix “.xlsx” ergibt:

Excel-Datei mit Ergebnissen der PDF-Analyse

Die erste Spalte gibt an, wie das Skript den Text klassifiziert hat, der in der Spalte “Inhalt” steht:

  • Abschnitt: Überschrift eines Abschnitts
  • Eingabe: Bezeichnung eines Eingabefelds
  • Sonstiges: Text, der nicht genauer klassifiziert werden konnte

Die Spalte “Text” enthält zum Vergleich die originale Zeile, die aus dem PDF-Formular extrahiert wurde. Man kann sie ebenso wie die folgenden Spalten dazu verwenden, Fehlern bei Extraktion und Klassifikation auf die Spur zu kommen.

Beim Extrahieren aus PDF hilft ein Klempner

Für Python gibt es mehrere Bibliotheken zum Auswerten von PDF-Dateien. In diesem Skript habe ich pdfplumber verwendet. So kann man auch ohne PDF-Spezialwissen Text aus PDF-Dateien extrahieren. Dabei wird der extrahierte Text seitenweise ausgelesen, auch der Text, der in Tabellen enthalten ist:

from operator import itemgetter
import pdfplumber


def check_bboxes(word, table_bbox):
    """
    Prüft, ob word innerhalb von table_bbox liegt.
    table_bbox ist der Rahmen (bounding box) um die Tabelle.
    """
    l = word['x0'], word['top'], word['x1'], word['bottom']
    r = table_bbox
    return l[0] > r[0] and 
           l[1] > r[1] and 
           l[2] < r[2] and 
           l[3] < r[3]


def Text_extrahieren(pdf_datei):
    """
    Extrahiert Text und Tabellen aus einem PDF-Formular.
    
    IN:
        pdf_datei    Pfad der zu analysierenden PDF-Datei

    RETURN:
        Liste von Seiten, die Listen von Zeilen enthalten
           'text'  = Zeile mit Text außerhalb von Tabellen
           'table' = Zeile mit Text innerhalb von Tabellen
    """

    with pdfplumber.open(pdf_datei) as pdf:
        
        # Seiteninhalte extrahieren
        pages = []
        for page in pdf.pages:

            # Tabellen mit ihren Begrenzungen extrahieren
            tables = page.find_tables()
            table_bboxes = [i.bbox for i in tables]
            tables = [{'table': i.extract(), 
                       'top': i.bbox[1]
                      } for i in tables
                     ]

            # Wörter außerhalb von Tabellen ermitteln
            non_table_words = [
                     word for word in page.extract_words() 
                     if not any([
                     check_bboxes(word, table_bbox) 
                     for table_bbox in table_bboxes])]

            # Zeilen der Seite in Liste zusammenstellen
            lines = []
            for cluster in pdfplumber.utils.cluster_objects(
                    non_table_words + tables, 
                    itemgetter('top'), 
                    tolerance = 5):
                     if 'text' in cluster[0]:
                       lines.append(' '.join(
                                    [i.get('text', '') 
                                     for i in cluster
                                    ]))
                     elif 'table' in cluster[0]:
                        lines.append(cluster[0]['table'])
            
            # Zeilen der neuen Seite hinzufügen
            # zur Liste der Seiten
            pages.append(lines)

    return(pages)

Das Ergebnis pages ist eine Liste von Seiten. Für jede Seite enthält es eine Liste der darin gefundenen Zeilen. Diese geschachtelte Liste wird später in eine Tabelle umgewandelt und bei der weiteren Analyse angereichert.

Das vollständige Skript findet man im folgenden Kapitel.

Quelldateien

Das Python-Skript wurde hier mit dem Suffix “.txt” statt “.py” bereitgestellt, um Berechtigungsprobleme zu vermeiden.

Hier requirements.txt mit den verwendeten Bibliotheken:

Die Konfigurationsdatei config.json hat folgenden Inhalt:

{
  "input_dir" : "./pdf_dir"
}

PDF-Formulare analysieren mit Python Read More »

Werkzeuge: Zero-Knowledge Proof

Geht das: Jemanden davon überzeugen, dass man ein Geheimnis kennt, ohne dieses Geheimnis zu verraten?

Ja, das geht tatsächlich, so unmöglich es auf den ersten Blick erscheint. Und zwar mit einem kryptographischen Verfahren, das Mitte der 1980er-Jahre als “Zero-Knowledge Proof” erstmals publiziert wurde und von dem es inzwischen zahlreiche Varianten gibt, nicht nur in der Theorie, sondern auch in der Praxis.

So bietet die ING ein Login-Verfahren für ihr Online-Banking an, bei dem man dazu aufgefordert wird, zwei Ziffern seines sechsstelligen DiBa-Keys einzutippen (den man natürlich geheim halten soll). Welche Ziffern man eingeben soll, würfelt der Login-Prozess bei jeder Anmeldung neu aus.

Weil der DiBa-Key nicht über die Leitung zwischen Kunde und Bank fließt, kann ein Angreifer den Key nicht ermitteln, selbst wenn er in der Lage wäre, die verschlüsselte Kommunikation zu knacken, z.B. mithilfe eines geheimdienstlichen Rechnerparks oder eines gekaperten Botnetzes. (Weil der DiBa-Key über eine virtuelle Tastatur eingegeben wird, hat auch ein Angreifer mit Keylogger keine Chance, die Eingabe mitzulesen.)

Es handelt sich also um ein statistisches Verfahren, denn die Bank kann nur mit einer gewissen Wahrscheinlichkeit darauf schließen, dass der Kunde den DiBa-Key wirklich kennt – schließlich wäre es ja denkbar, dass ein Angreifer mit einer gehörigen Portion Glück zufällig auf die beiden richtigen Ziffern tippt, ohne den vollständigen DiBa-Key zu kennen. Natürlich muss die Bank verhindern, dass man so lange herumprobiert, bis man irgendwann die richtigen Ziffern erwischt.

Es wurden viele Szenarien entwickelt, die das Potential des Zero-Knowledge Proof veranschaulichen, ohne die Wissbegierigen durch verzwickte Theorie abzuschrecken. Mein Lieblings-Szenario geht so:

Angenommen Sie haben zwei Tennisbälle, die bis auf die Farbe völlig gleich sind: Der eine ist rot, der andere grün. Und Sie haben einen Freund, der farbenblind ist, also Rot und Grün nicht unterscheiden kann. Mit dem Zero-Knowledge Proof können Sie Ihren Freund nun davon überzeugen, dass Sie die beiden Bälle unterscheiden können, ohne zu verraten, wie Sie das machen.

Wie geht das?

Sie bitten Ihren Freund, die beiden Bälle hinter seinem Rücken zu vertauschen oder nicht zu vertauschen und dann wieder vorzuzeigen. Sie entscheiden sich für eine der beiden Farben, z.B. für Rot, und zeigen dann auf den roten Ball.

Ihr Freund soll nun die Bälle so oft hinter seinem Rücken vertauschen und Sie zeigen jedes Mal wieder auf den roten Ball, bis Ihr Freund davon überzeugt ist, dass Sie die Bälle unterscheiden können, auch wenn ihm ein Rätsel bleibt, wie zum Teufel Sie das anstellen.

Fazit: Ein Geheimnis verschlüsselt auszutauschen, ist gut. Es nicht zu verraten, aber trotzdem nachzuweisen, dass man es kennt, ist besser. Genau das leistet der Zero-Knowledge Proof.

Werkzeuge: Zero-Knowledge Proof Read More »