Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
313 lines
12 KiB
Python
313 lines
12 KiB
Python
"""
|
||
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()
|