""" Extracts IT-relevant text passages from FuBen PDFs (IWF 2-8) using PyMuPDF. Outputs a markdown table with OE, IT-relevant passage, and assessment. Version 2: Better sentence extraction and more focused passages. """ import fitz # PyMuPDF import os import re from pathlib import Path # Paths DATA_DIR = Path(r"c:\Users\AndreKnie\OneDrive - Deutsche Bahn\Coden\Orchestrator\Analyse-O2C-C2S\data\FuBen") OUTPUT_FILE = Path(r"c:\Users\AndreKnie\OneDrive - Deutsche Bahn\Coden\Orchestrator\Analyse-O2C-C2S\analysis\2026-06-18-fuben-it-analyse.md") # Keywords to search for (case-insensitive) KEYWORDS = [ r'\bIT[-\s]', r'\bIT\b', r'\bSoftware\b', r'\bAnwendung(?:en)?\b', r'\bSystem(?:e|en|s)?\b', r'\bDigital(?:e|er|es|en|isierung)?\b', r'\bTool(?:s)?\b', r'\bPlattform(?:en)?\b', r'\bPortal(?:e|s)?\b', r'\bDatenbank(?:en)?\b', r'\bAutomatisier(?:ung|t|en)?\b', r'\bDaten(?:management|pflege|qualität|bereitstellung|verarbeitung|modell)?\b', r'\bSchnittstelle(?:n)?\b', r'\bSAP\b', r'\bERP\b', r'\bApplikation(?:en)?\b', r'\belektronisch(?:e|er|es|en)?\b', ] # Compile combined pattern KEYWORD_PATTERN = re.compile('|'.join(KEYWORDS), re.IGNORECASE) # IWF number extraction from filename IWF_PATTERN = re.compile(r'V\.IWF\s+(\d+)') def get_iwf_number(filename: str) -> str: """Extract IWF number from filename.""" match = IWF_PATTERN.search(filename) if match: return match.group(1) return "" def is_relevant_iwf(iwf_num: str) -> bool: """Check if IWF is in range 2-8 (including sub-units like 21, 221, etc.).""" if not iwf_num: return False first_digit = iwf_num[0] return first_digit in '2345678' def extract_text_from_pdf(pdf_path: Path) -> str: """Extract full text from a PDF using PyMuPDF.""" text = "" try: doc = fitz.open(str(pdf_path)) for page in doc: text += page.get_text() doc.close() except Exception as e: print(f"Error reading {pdf_path.name}: {e}") return text def split_into_items(text: str) -> list[str]: """Split FuBen text into individual task items (■ bullet points and sentences).""" items = [] # Split by ■ bullet points (common in FuBen) - these are the primary structure if '■' in text: parts = text.split('■') for part in parts: cleaned = part.strip() cleaned = re.sub(r'\s+', ' ', cleaned) if len(cleaned) > 15: items.append(cleaned) else: # Fallback: split by newlines and combine short lines lines = text.split('\n') current = "" for line in lines: line = line.strip() if not line or len(line) < 3: if current and len(current) > 15: items.append(re.sub(r'\s+', ' ', current)) current = "" continue if current: current += " " + line else: current = line if current and len(current) > 15: items.append(re.sub(r'\s+', ' ', current)) return items def find_it_passages(text: str) -> list[str]: """Find sentences/bullet points containing IT-relevant keywords.""" passages = [] seen = set() items = split_into_items(text) for item in items: if KEYWORD_PATTERN.search(item): # Skip header/metadata lines if item.startswith('Funktionsbeschreibung V.IWF'): continue if 'Seite' in item and 'von insgesamt' in item: continue if item.startswith('ETO2024'): continue # Truncate to a reasonable length showing the relevant part if len(item) > 300: # Find where the keyword is and show context around it match = KEYWORD_PATTERN.search(item) if match: start = max(0, match.start() - 80) end = min(len(item), match.end() + 220) item = ("..." if start > 0 else "") + item[start:end] + ("..." if end < len(item) else "") if item not in seen: seen.add(item) passages.append(item) return passages def assess_passage(passage: str) -> str: """Assess whether a passage describes a task that should move to IT department.""" passage_lower = passage.lower() # Strong indicators for IT responsibility it_strong = [ 'software', 'anwendung', 'programmier', 'datenbank', 'portal', 'plattform', 'digital', 'it-system', 'it-lösung', 'it-infrastruktur', 'it-anforder', 'it-anwend', 'it-unterstütz', 'schnittstelle', 'automatisier', 'tool', 'applikation', 'datenmanagement', 'datenpflege', 'datenbereitstellung', 'datenverarbeitung', 'datenmodell', 'datenqualität', 'elektronisch', 'server', 'cloud', 'api', 'webseite', 'website', 'netzwerk', 'berechtig', 'zugriff', 'login', 'passwort', 'it-sicherheit', 'cybersecurity', 'datenschutz', 'konfiguration', 'implementierung', 'migration', 'sap', 'erp', 'crm', 'testing', 'abnahme', 'eingabeunterstützung', 'nutzerführung', ] # Indicators that suggest operational/business ownership (not pure IT) business_indicators = [ 'fachlich', 'prozesssteuerung', 'steuerung der', 'strateg', 'führung', 'koordin', 'qualitätssicherung', 'anforderungsmanagement', ] strong_count = sum(1 for term in it_strong if term in passage_lower) business_count = sum(1 for term in business_indicators if term in passage_lower) if strong_count >= 2: return "✅ Ja – klare IT-Aufgabe" elif strong_count >= 1 and business_count == 0: return "✅ Ja – IT-Aufgabe" elif strong_count >= 1 and business_count >= 1: return "⚠️ Teilweise – IT-Unterstützung nötig, fachliche Verantwortung bleibt" elif 'system' in passage_lower and business_count == 0: return "🔍 Prüfen – möglicherweise IT-relevant (Systembezug)" elif 'system' in passage_lower and business_count >= 1: return "⚠️ Teilweise – Systemnutzung mit fachlichem Kontext" else: return "🔍 Prüfen – Kontext unklar" def get_oe_label(iwf_num: str, text: str) -> str: """Extract OE label from PDF text.""" # Look for the title line after "Funktionsbeschreibung V.IWF XX" pattern = re.compile(rf'Funktionsbeschreibung\s+V\.IWF\s+{re.escape(iwf_num)}\s*\n(.+?)(?:\n|Zielsetzung)', re.DOTALL) match = pattern.search(text) if match: name = match.group(1).strip() name = re.sub(r'\s+', ' ', name) if len(name) > 5 and len(name) < 100: return f"IWF {iwf_num} – {name}" # Fallback: look for title after IWF number lines = text.split('\n') for i, line in enumerate(lines): if f'V.IWF {iwf_num}' in line and i + 1 < len(lines): next_line = lines[i + 1].strip() if next_line and len(next_line) > 3 and not next_line.startswith(('Seite', 'ETO', 'Vorstand')): return f"IWF {iwf_num} – {next_line}" return f"IWF {iwf_num}" def main(): results = [] # Process all PDFs pdf_files = sorted(DATA_DIR.glob("*.pdf")) for pdf_path in pdf_files: iwf_num = get_iwf_number(pdf_path.name) if not is_relevant_iwf(iwf_num): continue print(f"Processing: {pdf_path.name} (IWF {iwf_num})") # Extract text text = extract_text_from_pdf(pdf_path) if not text: print(f" No text extracted from {pdf_path.name}") continue # Get OE label oe_label = get_oe_label(iwf_num, text) # Find IT-relevant passages passages = find_it_passages(text) if passages: print(f" Found {len(passages)} IT-relevant passages") for passage in passages: assessment = assess_passage(passage) # Escape pipe characters for markdown table clean_passage = passage.replace('|', '\\|').replace('\n', ' ') results.append((oe_label, clean_passage, assessment)) else: print(f" No IT-relevant passages found") # Sort results by OE results.sort(key=lambda x: x[0]) # Write output OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True) with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: f.write("# IT-Relevanz-Analyse der Funktionsbeschreibungen (FuBen)\n\n") f.write("**Datum:** 2026-06-18\n\n") f.write("**Scope:** IWF 2, 3, 4, 5, 6, 7, 8 und ihre Untereinheiten\n\n") f.write("**Ausgeschlossen:** IWF 1 (Strategie/Steuerung) und IWF 9 (Prozesse und IT)\n\n") f.write("**Suchbegriffe:** IT, Software, Anwendungen, Systeme, Digital, Tools, Plattform, Portal, Datenbank, Automatisierung, Datenmanagement, Schnittstelle, SAP, ERP, Applikation, elektronisch\n\n") f.write("## Legende Bewertung\n\n") f.write("| Symbol | Bedeutung |\n") f.write("|---|---|\n") f.write("| ✅ Ja | Aufgabe sollte in die IT-Abteilung wandern |\n") f.write("| ⚠️ Teilweise | IT-Unterstützung nötig, fachliche Verantwortung bleibt in der OE |\n") f.write("| 🔍 Prüfen | Kontext unklar, weitere Analyse nötig |\n\n") f.write("---\n\n") f.write("## Ergebnisse\n\n") f.write("| OE | IT-relevante Textstelle | Bewertung |\n") f.write("|---|---|---|\n") for oe, passage, assessment in results: f.write(f"| {oe} | {passage} | {assessment} |\n") # Summary section f.write(f"\n\n---\n\n") f.write("## Zusammenfassung\n\n") # Count by assessment type ja_count = sum(1 for _, _, a in results if '✅' in a) teilweise_count = sum(1 for _, _, a in results if '⚠️' in a) prüfen_count = sum(1 for _, _, a in results if '🔍' in a) f.write(f"| Kategorie | Anzahl |\n") f.write(f"|---|---|\n") f.write(f"| ✅ Klare IT-Aufgaben | {ja_count} |\n") f.write(f"| ⚠️ Teilweise IT-relevant | {teilweise_count} |\n") f.write(f"| 🔍 Zu prüfen | {prüfen_count} |\n") f.write(f"| **Gesamt** | **{len(results)}** |\n\n") # Count by OE oe_counts = {} for oe, _, _ in results: oe_counts[oe] = oe_counts.get(oe, 0) + 1 f.write(f"### Verteilung nach Organisationseinheit\n\n") f.write(f"| OE | Anzahl IT-Stellen |\n") f.write(f"|---|---|\n") for oe, count in sorted(oe_counts.items()): f.write(f"| {oe} | {count} |\n") f.write(f"\n\n---\n\n") f.write("## Fazit\n\n") f.write("Die Analyse zeigt, dass IT-bezogene Aufgaben über die gesamte Organisation verteilt sind. ") f.write("Besonders in den Bereichen Kapazitätsmanagement (IWF 2), Netzfahrplan (IWF 3), ") f.write("Kapazitätssteuerung (IWF 5) und Produkt-/Preismanagement (IWF 8) finden sich ") f.write("Aufgaben mit starkem IT-Bezug, die potenziell in eine zentrale IT-Abteilung ") f.write("wandern könnten.\n\n") f.write("**Empfehlung:** Die mit ✅ markierten Aufgaben sollten prioritär geprüft werden, ") f.write("ob sie in IWF 9 (Prozesse und IT) zentralisiert werden können.\n") print(f"\nDone! Output written to: {OUTPUT_FILE}") print(f"Total IT-relevant passages found: {len(results)}") print(f" ✅ Klare IT-Aufgaben: {ja_count}") print(f" ⚠️ Teilweise: {teilweise_count}") print(f" 🔍 Zu prüfen: {prüfen_count}") if __name__ == "__main__": main()