# %% [markdown] # # Analyse von PDF-Formularen # # ## Zweck dieses Skripts # # Dieses Skript Skript extrahiert Aufbau und Inhalt eines PDF-Formulars in tabellarischer Form. # Die Tabelle kann man als Ausgangspunkt für den Entwurf eines Web-Formulars nutzen, das dieselbe Fachlichkeit wie das PDF-Formular abdecken soll. # # ## Funktionsumfang # # Dieses Skript kann Folgendes leisten: # # - Text zeilenweise extrahieren # - Tabellen-Inhalte in Zeilen und Spalten extrahieren # - Text und Tabellen-Inhalte in der richtigen Reihenfolge den jeweiligen Seiten des Formulars zuordnen # - Überschriften mit vorangestelltem Großbuchstaben oder vorangestellter Ziffer erkennen # - Feldnamen mit vorangestellter laufender Nummer erkennen # # Als Ergebnis liefert das Skript eine Excel-Datei. # %% [markdown] # ## Text und Tabellen aus der PDF-Datei extrahieren # %% 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] # Ermitteln, welche Wörter nicht in Tabellen stehen 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) # %% [markdown] # ## Extrakt als Tabelle aufbereiten # %% import pandas as pd def Text_zu_Tabelle(pages): """ Wandelt den extrahierten Text in eine Tabelle um. IN: Ergebnis von Text_extrahieren RETURN: Liste mit dem Dateninhalt der Tabelle Liste mit den Spalten-Namen der Tabelle Text Textinhalt Seite Seitenzahl Textzeile Nummer der Textzeile einer Seite (ohne Tabellenzeilen) Tabellenzeile Zeilennummer innerhalb einer Tabelle Zellzeile Zeilennummer innerhalb einer Tabellenzelle """ # Spalten definieren columns = [ 'Text', 'Seite', 'Textzeile', 'Tabellenzeile', 'Zellzeile' ] # Daten initialisieren data = [] text = "" for seite, page in enumerate(pages, start = 1): for textzeile, line in enumerate(page, start = 1): if isinstance(line, str): # String repräsentiert eine Zeile mit Text außerhalb einer Tabelle text = line tabellenzeile = 0 zellzeile = 0 data.append([text, seite, textzeile, tabellenzeile, zellzeile ]) if isinstance(line, list): # Liste repräsentiert eine Tabelle for tabellenzeile, row in enumerate(line, start = 1): textzeile_in_tab = textzeile + tabellenzeile - 1 for cell in row: # Leere Zellen und solche mit Inhalt "," unterdrücken if cell and cell != ",": # Zellen mit Zeilenumbruch auf in mehreren Zeilen ausgeben split_cell = cell.split("\n") for zellzeile, cell_line in enumerate(split_cell, start = 1): text = cell_line data.append([text, seite, textzeile_in_tab, tabellenzeile, zellzeile ]) return(data, columns) # %% [markdown] # ## Felder identifizieren anhand Nummerierung # %% import re def Felder_identifizieren(data, columns): """" Identifiziert Felder anhand ihrer fortlaufenden Nummerierung IN: Ergebnis aus Text_zu_Tabelle RETURN: Dataframe aus den übergebenen Daten plus den neuen Spalten Feldnr und Feld Text Textinhalt Seite Seitenzahl Textzeile Nummer der Textzeile einer Seite (ohne Tabellenzeilen) Tabellenzeile Zeilennummer innerhalb einer Tabelle Zellzeile Zeilennummer innerhalb einer Tabellenzelle Feldnr Nummer des Feldes im Formular Feld Name des Feldes laut Formular """ feld_pattern = "(\d+) (\D+)" data_feld = [] columns_feld = [] row_feld = [] letzte_nr = 0 gefunden = False for row in data: nr = 0 # Zeile kopieren und zwei leere Spalten ergänzen row_feld = row.copy() + ['', ''] text = row_feld[0] fundliste = re.findall(feld_pattern, text) gefunden = len(fundliste) > 0 if gefunden: # Feldkandidaten gefunden for fund in fundliste: nr = int(fund[0]) if (richtige_nr := nr == letzte_nr + 1): # nr passt zur letzten Feldnummer letzte_nr = nr feld = fund[1].rstrip() # etwaige Leerzeichen am Ende entfernen row_feld[5] = nr row_feld[6] = feld data_feld.append(row_feld.copy()) if gefunden == False or richtige_nr == False: # Keine Felder enthalten oder Feldnummer passt nicht # Originale Inhalte übernehmen data_feld.append(row_feld) columns_feld = columns + ["Feldnr", "Feld"] return (pd.DataFrame(data_feld, columns = columns_feld)) # %% [markdown] # ## Abschnitte identifizieren anhand ihres Präfix # %% def Abschnitte_identifizieren(df): """" Identifiziert Abschnitte anhand ihrer Überschriften mit führendem "A.", "B." usw. oder "1.", "2." usw. IN: Ergebnis aus Felder_identifzieren RETURN: Dataframe aus den übergebenen Daten plus den neuen Spalten Abschnittszeile und Abschnittstitel Text Textinhalt Seite Seitenzahl Textzeile Nummer der Textzeile einer Seite (ohne Tabellenzeilen) Tabellenzeile Zeilennummer innerhalb einer Tabelle Zellzeile Zeilennummer innerhalb einer Tabellenzelle Feldnr Nummer des Feldes im Formular Feld Name des Feldes laut Formular Abschnittszeile Buchstabe oder Ziffer, welche der Überschrift vorangestellt ist Abschnittstitel Text der Überschrift ohne Buchstabe/Ziffer """ df[['Abschnittszeile', 'Abschnittstitel']] = [ (fund[0][0], fund[0][1]) if (fund := re.findall("^([A-Z]|[0-9])\. (\D+)", x)) else ("", "") for x in df["Text"]] return(df) # %% [markdown] # ## Zeilen nach Art und Inhalt klassifizieren # %% def Zeilen_klassifizieren(df): """" IN: Ergebnis aus Abschnitt_identifizieren RETURN: Übergebenes Dataframe mit zusätzlichen Spalten "Art" und "Inhalt" als führende Spalten Text Textinhalt Seite Seitenzahl Textzeile Nummer der Textzeile einer Seite (ohne Tabellenzeilen) Tabellenzeile Zeilennummer innerhalb einer Tabelle Zellzeile Zeilennummer innerhalb einer Tabellenzelle Feldnr Nummer des Feldes im Formular Feld Name des Feldes laut Formular Abschnittszeile Buchstabe oder Ziffer, welche der Überschrift vorangestellt ist Abschnittstitel Text der Überschrift ohne Buchstabe/Ziffer Art "Abschnitt", "Eingabe", "Sonstiges" Inhalt Überschrift bei "Art" = "Abschnitt" Feldbezeichnung bei "Art" = "Eingabe" sonstiger Text bei "Art" = "Sonstiges" """ # Spalte "Art" anlegen und füllen df['Art'] = 'Sonstiges' df['Art'] = ['Abschnitt' if x != "" else y for x, y in zip(df['Abschnittszeile'], df['Art'])] df['Art'] = ['Eingabe' if x != "" else y for x, y in zip(df['Feld'], df['Art'])] # Spalte "Inhalt" anlegen und füllen df['Inhalt'] = df['Text'] df['Inhalt'] = [y if x == 'Abschnitt' else z for x, y, z in zip(df['Art'], df['Abschnittstitel'], df['Inhalt'])] df['Inhalt'] = [y if x == 'Eingabe' else z for x, y, z in zip(df['Art'], df['Feld'], df['Inhalt'])] # Spalten neu ordnen: Art und Inhalt zuerst column_names = ['Art', 'Inhalt', 'Text', 'Seite', 'Textzeile', 'Tabellenzeile', 'Zellzeile', 'Feldnr', 'Feld', 'Abschnittszeile', 'Abschnittstitel'] df = df.loc[:, column_names] return(df) # %% [markdown] # ## Ergebnisse als Excel-Datei ausgeben # %% import xlsxwriter def Dataframe_schreiben(df, excel_datei): """" Schreibt das übergebene Dataframe als Excel-Datei. IN: df aus Zeilen_klassifizieren excel_datei Pfad der zu schreibenden Excel-Datei OUT: Excel-Datei mit - Autofilter eingeschaltet - Spalten "Inhalt" und "Text" mit reduzierter Breite und automatischem Umbruch - Spaltenüberschriften eingefroren """ # Dataframe in Excel schreiben sheet_name = "Extrahierte Texte" writer = pd.ExcelWriter(excel_datei, engine = 'xlsxwriter') df.to_excel(writer, sheet_name = sheet_name, index = False) # Excel-Tabelle formatieren workbook = writer.book worksheet = writer.sheets[sheet_name] # Autofilter einschalten (max_row, max_col) = df.shape worksheet.autofilter(0, 0, max_row, max_col - 1) # Spaltenbreiten festlegen # Alle Spalten: automatische Breite worksheet.autofit() # Spalten "Inhalt" und "Text" automatisch umbrechen wrap_format = workbook.add_format() wrap_format.set_text_wrap() worksheet.set_column(1, 2, wrap_format) # Spalten "Inhalt" und "Text": Breite reduzieren worksheet.set_column(1, 2, 150) # Spaltenüberschriften einfrieren worksheet.freeze_panes(1, 0) workbook.close() # %% [markdown] # ## Ablaufsteuerung # %% 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)