#!/usr/bin/env python3 """ parse_text_pdf.py - DETERMINISTISCHER Parser fuer die Text-PDFs (kein LLM). Nutzt die Struktur digital ausgefuellter BizMatch-Formulare: - Die Info-Seite und die CA-Seite werden ueber ANKER-Texte gefunden (positionsunabhaengig - koennen auf beliebigen Seiten liegen). - Die eingegebenen Werte stehen als Block; wir ordnen sie den Feldern zu. Checkbox 'interested_in_updates' steht NICHT im Text (eingebettetes Bild) -> bleibt vorerst null, Marker _checkbox_pending=true fuer spaeteres Crop-Vision. Aufruf: python3 parse_text_pdf.py --pdf ~/data/S/"Sturgill, Garett 040126.pdf" python3 parse_text_pdf.py --pdf ... --debug # zeigt Zwischenschritte """ import argparse, re, json import pdfplumber ANCHOR_INFO = "BUYER INFORMATION SHEET" ANCHOR_CA = "PROSPECTIVE BUYER AGREES TO KEEP AND HOLD CONFIDENTIAL" # Label-Zeilen des leeren Templates (Info-Seite), in Reihenfolge. # Nach diesen Labels kommt (weiter unten) der Werte-Block. INFO_LABELS = [ "NAME / COMPANY", "PHONE", "ADDRESS", "EMAIL ADDRESS", "HOW DID YOU HEAR ABOUT US", "TYPES OF BUSINESSES", "BACKGROUND", "TOTAL PURCHASE PRICE", "DOWN PAYMENT", "INCOME REQUIREMENTS", "ACCOUNTANT", "ATTORNEY", "BANK", ] def is_empty_marker(s): """'n', leere/whitespace, oder nur Platzhalter -> gilt als leer.""" if s is None: return True t = s.strip() if not t: return True # einzelne 'n' oder Folgen von 'n' (Platzhalter fuer leere Felder) if re.fullmatch(r"[nN]( +[nN])*", t): return True return False def clean(s): return None if is_empty_marker(s) else s.strip() def find_pages(pdf): """Findet Info- und CA-Seite ueber Anker. Gibt (info_text, ca_text).""" info_txt = ca_txt = None for page in pdf.pages: t = page.extract_text() or "" if info_txt is None and ANCHOR_INFO in t: info_txt = t if ca_txt is None and ANCHOR_CA in t.replace("\n", " "): ca_txt = t return info_txt, ca_txt def parse_info_values(info_txt, debug=False): """ Extrahiert den Werte-Block der Info-Seite. Struktur (aus Analyse): nach der Label-Vorlage (endet mit 'BANK:') folgt der Werte-Block. Die Werte stehen in fester Reihenfolge: name, phone-zeile, address, email, how_heard, types, background(1-2 zeilen), purchase_price, down_payment, income, accountant, attorney, bank Da leere Felder als 'n' erscheinen, gehen wir zeilenweise vor. """ lines = [l.rstrip() for l in info_txt.splitlines()] # Werte-Block beginnt nach der letzten Label-Zeile ("BANK:") bank_idx = None for i, l in enumerate(lines): if l.strip().upper().startswith("BANK"): bank_idx = i if bank_idx is None: return None block = [l for l in lines[bank_idx+1:] if l.strip()] # letzte Zeile ist oft "Doc ID: ..." -> entfernen block = [l for l in block if not l.strip().lower().startswith("doc id")] if debug: print(" --- Werte-Block ---") for j, l in enumerate(block): print(f" [{j}] {l!r}") return block def main(): ap = argparse.ArgumentParser() ap.add_argument("--pdf", required=True) ap.add_argument("--debug", action="store_true") args = ap.parse_args() with pdfplumber.open(args.pdf) as pdf: info_txt, ca_txt = find_pages(pdf) result = { "is_buyer_sheet": info_txt is not None, "name_company": None, "prospective_buyer": None, "company": None, "phone": None, "cell": None, "email": None, "address": None, "state": None, "how_did_you_hear": None, "interested_in_updates": None, "types_of_business_raw": None, "background_experience": None, "total_purchase_price": None, "down_payment": None, "date_of_introduction": None, "_checkbox_pending": True, # Checkbox spaeter per Crop-Vision "_parser": "deterministic", } if info_txt: block = parse_info_values(info_txt, debug=args.debug) if block: # Heuristische Zuordnung nach Reihenfolge. # Wir mappen defensiv: bekannte Anker im Block suchen. # block[0] = name, block[1] = phone-zeile, ... def get(i): return clean(block[i]) if i < len(block) else None result["name_company"] = get(0) # phone-zeile kann "(PHONE) n n" o.ae. sein result["phone"] = None # wird unten aus phone-zeile gezogen # Rest positionsbasiert - ACHTUNG: haengt von Leer-Feld-Verhalten ab # Wir geben den Block auch roh mit, zum Debuggen der Zuordnung result["_value_block"] = block if ca_txt: # CA-Seite: Name + Datum aus dem Werte-Block nach dem Vertragstext ca_lines = [l.strip() for l in ca_txt.splitlines() if l.strip()] # Datum finden (MM / DD / YYYY) for l in ca_lines: m = re.search(r"(\d{1,2})\s*/\s*(\d{1,2})\s*/\s*(\d{4})", l) if m: mm, dd, yy = m.groups() result["date_of_introduction"] = f"{yy}-{int(mm):02d}-{int(dd):02d}" break # Name: erste nicht-leere Zeile nach dem Anker-Block, die nicht 'n' ist # (aus Analyse: direkt nach dem langen Vertragstext-Block) print(json.dumps(result, indent=2, ensure_ascii=False)) if __name__ == "__main__": main()