gateway/modules/chat_content_extraction.py
2025-04-21 17:44:28 +02:00

779 lines
No EOL
29 KiB
Python

"""
Modul zur Extraktion von Inhalten aus verschiedenen Dateiformaten.
Bietet spezialisierte Funktionen für die Verarbeitung von Text, PDF, Office-Dokumenten, Bildern usw.
"""
import logging
import os
import io
from typing import Dict, Any, List, Optional, Union, Tuple
import base64
# Logger konfigurieren
logger = logging.getLogger(__name__)
# Optional imports - only loaded when needed
pdf_extractor_loaded = False
office_extractor_loaded = False
image_processor_loaded = False
def get_document_contents(file_metadata: Dict[str, Any], file_content: bytes) -> List[Dict[str, Any]]:
"""
Hauptfunktion zur Extraktion von Inhalten aus einer Datei basierend auf dem MIME-Typ.
Delegiert an spezialisierte Extraktionsfunktionen.
Args:
file_metadata: Metadaten der Datei (Name, MIME-Typ, etc.)
file_content: Binärdaten der Datei
Returns:
Liste von Document-Content-Objekten mit metadata und is_text Flag
"""
try:
mime_type = file_metadata.get("mime_type", "application/octet-stream")
file_name = file_metadata.get("name", "unknown")
logger.info(f"Extrahiere Inhalte aus Datei '{file_name}' (MIME-Typ: {mime_type})")
# Inhalte basierend auf MIME-Typ extrahieren
contents = []
# Text-basierte Formate
if mime_type.startswith("text/") or mime_type in [
"application/json",
"application/xml",
"application/javascript",
"application/x-python"
]:
contents.extend(extract_text_content(file_name, file_content, mime_type))
# CSV Format
elif mime_type == "text/csv":
contents.extend(extract_csv_content(file_name, file_content))
# Bilder
elif mime_type.startswith("image/"):
contents.extend(extract_image_content(file_name, file_content, mime_type))
# PDF Dokumente
elif mime_type == "application/pdf":
contents.extend(extract_pdf_content(file_name, file_content))
# Word-Dokumente
elif mime_type in [
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword"
]:
contents.extend(extract_word_content(file_name, file_content, mime_type))
# Excel-Dokumente
elif mime_type in [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel"
]:
contents.extend(extract_excel_content(file_name, file_content, mime_type))
# PowerPoint-Dokumente
elif mime_type in [
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint"
]:
contents.extend(extract_powerpoint_content(file_name, file_content, mime_type))
# Binärdaten als Fallback für unbekannte Formate
else:
contents.extend(extract_binary_content(file_name, file_content, mime_type))
# Fallback, wenn keine Inhalte extrahiert werden konnten
if not contents:
logger.warning(f"Keine Inhalte aus Datei '{file_name}' extrahiert, verwende Binär-Fallback")
contents.append({
"sequence_nr": 1,
"name": '1_undefined',
"ext": os.path.splitext(file_name)[1][1:] if os.path.splitext(file_name)[1] else "bin",
"content_type": mime_type,
"data": file_content,
"metadata": {
"is_text": False
}
})
# Add generic attributes for all documents
for content in contents:
if isinstance(content.get("data"), bytes):
content["data"] = base64.b64encode(content["data"]).decode('utf-8')
# Add base64 flag
if "metadata" not in content:
content["metadata"] = {}
content["metadata"]["base64_encoded"] = True
logger.info(f"Erfolgreich {len(contents)} Inhalte aus Datei '{file_name}' extrahiert")
return contents
except Exception as e:
logger.error(f"Fehler bei der Inhaltsextraktion: {str(e)}")
# Fallback bei Fehler - Originaldaten zurückgeben
return [{
"sequence_nr": 1,
"name": file_metadata.get("name", "unknown"),
"ext": os.path.splitext(file_metadata.get("name", ""))[1][1:] if os.path.splitext(file_metadata.get("name", ""))[1] else "bin",
"content_type": file_metadata.get("mime_type", "application/octet-stream"),
"data": file_content,
"metadata": {
"is_text": False
}
}]
def _load_pdf_extractor():
"""Lädt die PDF-Extraktions-Bibliotheken bei Bedarf"""
global pdf_extractor_loaded
if not pdf_extractor_loaded:
try:
global PyPDF2, fitz
import PyPDF2
import fitz # PyMuPDF für umfangreichere PDF-Verarbeitung
pdf_extractor_loaded = True
logger.info("PDF-Extraktions-Bibliotheken erfolgreich geladen")
except ImportError as e:
logger.warning(f"PDF-Extraktions-Bibliotheken konnten nicht geladen werden: {e}")
def _load_office_extractor():
"""Lädt die Office-Dokument-Extraktions-Bibliotheken bei Bedarf"""
global office_extractor_loaded
if not office_extractor_loaded:
try:
global docx, openpyxl
import docx # python-docx für Word-Dokumente
import openpyxl # für Excel-Dateien
office_extractor_loaded = True
logger.info("Office-Extraktions-Bibliotheken erfolgreich geladen")
except ImportError as e:
logger.warning(f"Office-Extraktions-Bibliotheken konnten nicht geladen werden: {e}")
def _load_image_processor():
"""Lädt die Bild-Verarbeitungs-Bibliotheken bei Bedarf"""
global image_processor_loaded
if not image_processor_loaded:
try:
global PIL, Image
from PIL import Image
image_processor_loaded = True
logger.info("Bild-Verarbeitungs-Bibliotheken erfolgreich geladen")
except ImportError as e:
logger.warning(f"Bild-Verarbeitungs-Bibliotheken konnten nicht geladen werden: {e}")
def extract_text_content(file_name: str, file_content: bytes, mime_type: str) -> List[Dict[str, Any]]:
"""
Extrahiert Text aus Textdateien.
Args:
file_name: Name der Datei
file_content: Binärdaten der Datei
mime_type: MIME-Typ der Datei
Returns:
Liste von Text-Content-Objekten mit metadata.is_text = True
"""
try:
# Originaldateiendung beibehalten
file_extension = os.path.splitext(file_name)[1][1:] if os.path.splitext(file_name)[1] else "txt"
# Text-Inhalt extrahieren
text_content = file_content.decode('utf-8')
return [{
"sequence_nr": 1,
"name": "1_text", # Simplified naming
"ext": file_extension,
"content_type": "text",
"data": text_content,
"metadata": {
"is_text": True
}
}]
except UnicodeDecodeError:
logger.warning(f"Konnte Text aus Datei '{file_name}' nicht als UTF-8 decodieren, versuche andere Kodierungen")
try:
# Versuche alternative Kodierungen
for encoding in ['latin-1', 'cp1252', 'iso-8859-1']:
try:
text_content = file_content.decode(encoding)
logger.info(f"Text erfolgreich mit Kodierung {encoding} decodiert")
return [{
"sequence_nr": 1,
"name": "1_text", # Simplified naming
"ext": file_extension,
"content_type": "text",
"data": text_content,
"metadata": {
"is_text": True,
"encoding": encoding
}
}]
except UnicodeDecodeError:
continue
# Fallback auf Binärdaten, wenn keine Kodierung funktioniert
logger.warning(f"Konnte Text nicht decodieren, verwende Binärdaten")
return [{
"sequence_nr": 1,
"name": "1_binary", # Simplified naming
"ext": file_extension,
"content_type": mime_type,
"data": file_content,
"metadata": {
"is_text": False
}
}]
except Exception as e:
logger.error(f"Fehler bei der alternativen Textdekodierung: {str(e)}")
# Binärdaten als Fallback zurückgeben
return [{
"sequence_nr": 1,
"name": "1_binary", # Simplified naming
"ext": file_extension,
"content_type": mime_type,
"data": file_content,
"metadata": {
"is_text": False
}
}]
def extract_csv_content(file_name: str, file_content: bytes) -> List[Dict[str, Any]]:
"""
Extrahiert Inhalt aus CSV-Dateien.
Args:
file_name: Name der Datei
file_content: Binärdaten der Datei
Returns:
Liste von CSV-Content-Objekten mit metadata.is_text = True
"""
try:
# Text-Inhalt extrahieren
csv_content = file_content.decode('utf-8')
return [{
"sequence_nr": 1,
"name": "1_csv", # Simplified naming
"ext": "csv",
"content_type": "csv",
"data": csv_content,
"metadata": {
"is_text": True,
"format": "csv"
}
}]
except UnicodeDecodeError:
logger.warning(f"Konnte CSV aus Datei '{file_name}' nicht als UTF-8 decodieren, versuche andere Kodierungen")
try:
# Versuche alternative Kodierungen für CSV
for encoding in ['latin-1', 'cp1252', 'iso-8859-1']:
try:
csv_content = file_content.decode(encoding)
logger.info(f"CSV erfolgreich mit Kodierung {encoding} decodiert")
return [{
"sequence_nr": 1,
"name": "1_csv", # Simplified naming
"ext": "csv",
"content_type": "csv",
"data": csv_content,
"metadata": {
"is_text": True,
"encoding": encoding,
"format": "csv"
}
}]
except UnicodeDecodeError:
continue
# Fallback auf Binärdaten
return [{
"sequence_nr": 1,
"name": "1_binary", # Simplified naming
"ext": "csv",
"content_type": "text/csv",
"data": file_content,
"metadata": {
"is_text": False
}
}]
except Exception as e:
logger.error(f"Fehler bei der alternativen CSV-Dekodierung: {str(e)}")
return [{
"sequence_nr": 1,
"name": "1_binary", # Simplified naming
"ext": "csv",
"content_type": "text/csv",
"data": file_content,
"metadata": {
"is_text": False
}
}]
def extract_image_content(file_name: str, file_content: bytes, mime_type: str) -> List[Dict[str, Any]]:
"""
Extrahiert Inhalt aus Bilddateien und erzeugt ggf. Metadaten-Beschreibungen.
Args:
file_name: Name der Datei
file_content: Binärdaten der Datei
mime_type: MIME-Typ der Datei
Returns:
Liste von Image-Content-Objekten mit metadata.is_text = False
"""
# Dateiendung aus MIME-Typ oder Dateinamen extrahieren
file_extension = mime_type.split('/')[-1]
if file_extension == "jpeg":
file_extension = "jpg"
# Wenn möglich, Bild analysieren und Metadaten extrahieren
image_metadata = {
"is_text": False,
"format": "image"
}
image_description = None
try:
_load_image_processor()
if image_processor_loaded and file_content and len(file_content) > 0:
with io.BytesIO(file_content) as img_stream:
try:
img = Image.open(img_stream)
# Überprüfe, ob das Bild tatsächlich geladen wurde
img.verify()
# Um sicher weiterzuarbeiten, neu laden
img_stream.seek(0)
img = Image.open(img_stream)
image_metadata.update({
"format": img.format,
"mode": img.mode,
"width": img.width,
"height": img.height
})
# Extrahiere EXIF-Daten, falls vorhanden
if hasattr(img, '_getexif') and callable(img._getexif):
exif = img._getexif()
if exif:
exif_data = {}
for tag_id, value in exif.items():
exif_data[f"tag_{tag_id}"] = str(value)
image_metadata["exif"] = exif_data
# Erzeuge Bildbeschreibung
image_description = f"Image ({img.width}x{img.height}, {img.format}, {img.mode})"
except Exception as inner_e:
logger.warning(f"Fehler beim Verarbeiten des Bildes: {str(inner_e)}")
image_metadata["error"] = str(inner_e)
image_description = f"Image (unable to process: {str(inner_e)})"
except Exception as e:
logger.warning(f"Konnte Bildmetadaten nicht extrahieren: {str(e)}")
image_metadata["error"] = str(e)
# Bild-Inhalt zurückgeben
contents = [{
"sequence_nr": 1,
"name": "1_image", # Simplified naming
"ext": file_extension,
"content_type": "image",
"data": file_content,
"metadata": image_metadata
}]
# Falls Bildbeschreibung vorhanden, als zusätzlichen Text-Content hinzufügen
if image_description:
contents.append({
"sequence_nr": 2,
"name": "2_text_image_info", # Simplified naming with label
"ext": "txt",
"content_type": "text",
"data": image_description,
"metadata": {
"is_text": True,
"image_description": True
}
})
return contents
def extract_pdf_content(file_name: str, file_content: bytes) -> List[Dict[str, Any]]:
"""
Extrahiert Text und Bilder aus PDF-Dateien.
Args:
file_name: Name der Datei
file_content: Binärdaten der Datei
Returns:
Liste von PDF-Content-Objekten (Text und Bilder) mit metadata.is_text Flag
"""
contents = []
extracted_content_found = False
try:
# PDF-Extraktions-Bibliotheken laden
_load_pdf_extractor()
if not pdf_extractor_loaded:
logger.warning("PDF-Extraktion nicht möglich: Bibliotheken nicht verfügbar")
# Originaldatei als binären Inhalt hinzufügen
contents.append({
"sequence_nr": 1,
"name": "1_pdf", # Simplified naming
"ext": "pdf",
"content_type": "application/pdf",
"data": file_content,
"metadata": {
"is_text": False,
"format": "pdf"
}
})
return contents
# Text mit PyPDF2 extrahieren
extracted_text = ""
pdf_metadata = {}
with io.BytesIO(file_content) as pdf_stream:
pdf_reader = PyPDF2.PdfReader(pdf_stream)
# Metadaten extrahieren
pdf_info = pdf_reader.metadata or {}
for key, value in pdf_info.items():
if key.startswith('/'):
pdf_metadata[key[1:]] = value
else:
pdf_metadata[key] = value
# Text aus allen Seiten extrahieren
for page_num in range(len(pdf_reader.pages)):
page = pdf_reader.pages[page_num]
page_text = page.extract_text()
if page_text:
extracted_text += f"--- Seite {page_num + 1} ---\n{page_text}\n\n"
# Wenn Text gefunden wurde, als eigenen Content hinzufügen
if extracted_text.strip():
extracted_content_found = True
contents.append({
"sequence_nr": len(contents) + 1,
"name": f"{len(contents) + 1}_text", # Simplified naming
"ext": "txt",
"content_type": "text",
"data": extracted_text,
"metadata": {
"is_text": True,
"source": "pdf",
"pages": len(pdf_reader.pages),
"pdf_metadata": pdf_metadata
}
})
# Bilder mit PyMuPDF (fitz) extrahieren
try:
with io.BytesIO(file_content) as pdf_stream:
doc = fitz.open(stream=pdf_stream, filetype="pdf")
image_count = 0
for page_num in range(len(doc)):
page = doc[page_num]
image_list = page.get_images(full=True)
for img_index, img_info in enumerate(image_list):
try:
image_count += 1
xref = img_info[0]
base_image = doc.extract_image(xref)
image_bytes = base_image["image"]
image_ext = base_image["ext"]
# Bild als Content hinzufügen
extracted_content_found = True
contents.append({
"sequence_nr": len(contents) + 1,
"name": f"{len(contents) + 1}_image_page{page_num+1}_{img_index+1}", # Simplified naming with label
"ext": image_ext,
"content_type": f"image/{image_ext}",
"data": image_bytes,
"metadata": {
"is_text": False,
"source": "pdf",
"page": page_num + 1,
"index": img_index
}
})
except Exception as img_e:
logger.warning(f"Fehler bei der Extraktion von Bild {img_index} auf Seite {page_num + 1}: {str(img_e)}")
# Dokument schließen
doc.close()
except Exception as img_extract_e:
logger.warning(f"Fehler bei der Bildextraktion aus PDF: {str(img_extract_e)}")
except Exception as e:
logger.error(f"Fehler bei der PDF-Extraktion: {str(e)}")
# Wenn keine Inhalte extrahiert wurden, füge das Original-PDF hinzu
if not extracted_content_found:
contents.append({
"sequence_nr": 1,
"name": "1_pdf", # Simplified naming
"ext": "pdf",
"content_type": "application/pdf",
"data": file_content,
"metadata": {
"is_text": False,
"format": "pdf"
}
})
return contents
def extract_word_content(file_name: str, file_content: bytes, mime_type: str) -> List[Dict[str, Any]]:
"""
Extrahiert Text und Bilder aus Word-Dokumenten.
Args:
file_name: Name der Datei
file_content: Binärdaten der Datei
mime_type: MIME-Typ der Datei
Returns:
Liste von Word-Content-Objekten (Text und ggf. Bilder) mit metadata.is_text Flag
"""
contents = []
extracted_content_found = False
# Dateiendung bestimmen
file_extension = "docx" if mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" else "doc"
try:
# Office-Extraktions-Bibliotheken laden
_load_office_extractor()
if not office_extractor_loaded:
logger.warning("Word-Extraktion nicht möglich: Bibliotheken nicht verfügbar")
# Originaldatei als binären Inhalt hinzufügen
contents.append({
"sequence_nr": 1,
"name": "1_word", # Simplified naming
"ext": file_extension,
"content_type": mime_type,
"data": file_content,
"metadata": {
"is_text": False,
"format": "word"
}
})
return contents
# Unterstützt nur DOCX (neueres Format)
if mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
with io.BytesIO(file_content) as docx_stream:
doc = docx.Document(docx_stream)
# Text extrahieren
full_text = []
for para in doc.paragraphs:
full_text.append(para.text)
# Tabellen extrahieren
for table in doc.tables:
for row in table.rows:
row_text = []
for cell in row.cells:
row_text.append(cell.text)
full_text.append(" | ".join(row_text))
extracted_text = "\n\n".join(full_text)
# Extrahierten Text als Content hinzufügen
if extracted_text.strip():
extracted_content_found = True
contents.append({
"sequence_nr": 1,
"name": "1_text", # Simplified naming
"ext": "txt",
"content_type": "text",
"data": extracted_text,
"metadata": {
"is_text": True,
"source": "docx",
"paragraph_count": len(doc.paragraphs),
"table_count": len(doc.tables)
}
})
else:
logger.warning(f"Extraktion aus altem Word-Format (DOC) nicht unterstützt")
except Exception as e:
logger.error(f"Fehler bei der Word-Extraktion: {str(e)}")
# Wenn keine Inhalte extrahiert wurden, füge das Original-Dokument hinzu
if not extracted_content_found:
contents.append({
"sequence_nr": 1,
"name": "1_word", # Simplified naming
"ext": file_extension,
"content_type": mime_type,
"data": file_content,
"metadata": {
"is_text": False,
"format": "word"
}
})
return contents
def extract_excel_content(file_name: str, file_content: bytes, mime_type: str) -> List[Dict[str, Any]]:
"""
Extrahiert Tabellendaten aus Excel-Dateien.
Args:
file_name: Name der Datei
file_content: Binärdaten der Datei
mime_type: MIME-Typ der Datei
Returns:
Liste von Excel-Content-Objekten mit metadata.is_text Flag
"""
contents = []
extracted_content_found = False
# Dateiendung bestimmen
file_extension = "xlsx" if mime_type == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" else "xls"
try:
# Office-Extraktions-Bibliotheken laden
_load_office_extractor()
if not office_extractor_loaded:
logger.warning("Excel-Extraktion nicht möglich: Bibliotheken nicht verfügbar")
# Originaldatei als binären Inhalt hinzufügen
contents.append({
"sequence_nr": 1,
"name": "1_excel", # Simplified naming
"ext": file_extension,
"content_type": mime_type,
"data": file_content,
"metadata": {
"is_text": False,
"format": "excel"
}
})
return contents
# Unterstützt nur XLSX (neueres Format)
if mime_type == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
with io.BytesIO(file_content) as xlsx_stream:
workbook = openpyxl.load_workbook(xlsx_stream, data_only=True)
# Jedes Arbeitsblatt als separaten CSV-Content extrahieren
for sheet_index, sheet_name in enumerate(workbook.sheetnames):
sheet = workbook[sheet_name]
# Daten als CSV formatieren
csv_rows = []
for row in sheet.iter_rows():
csv_row = []
for cell in row:
value = cell.value
if value is None:
csv_row.append("")
else:
csv_row.append(str(value).replace('"', '""'))
csv_rows.append(','.join(f'"{cell}"' for cell in csv_row))
csv_content = "\n".join(csv_rows)
# Als CSV-Content hinzufügen
if csv_content.strip():
extracted_content_found = True
sheet_safe_name = sheet_name.replace(" ", "_").replace("/", "_").replace("\\", "_")
contents.append({
"sequence_nr": len(contents) + 1,
"name": f"{len(contents) + 1}_csv_{sheet_safe_name}", # Simplified naming with sheet label
"ext": "csv",
"content_type": "csv",
"data": csv_content,
"metadata": {
"is_text": True,
"source": "xlsx",
"sheet": sheet_name,
"format": "csv"
}
})
else:
logger.warning(f"Extraktion aus altem Excel-Format (XLS) nicht unterstützt")
except Exception as e:
logger.error(f"Fehler bei der Excel-Extraktion: {str(e)}")
# Wenn keine Inhalte extrahiert wurden, füge das Original-Dokument hinzu
if not extracted_content_found:
contents.append({
"sequence_nr": 1,
"name": "1_excel", # Simplified naming
"ext": file_extension,
"content_type": mime_type,
"data": file_content,
"metadata": {
"is_text": False,
"format": "excel"
}
})
return contents
def extract_powerpoint_content(file_name: str, file_content: bytes, mime_type: str) -> List[Dict[str, Any]]:
"""
Extrahiert Inhalte aus PowerPoint-Präsentationen.
Args:
file_name: Name der Datei
file_content: Binärdaten der Datei
mime_type: MIME-Typ der Datei
Returns:
Liste von PowerPoint-Content-Objekten mit metadata.is_text = False
"""
# Für PowerPoint geben wir aktuell nur die originale Binärdatei zurück
# Eine vollständige Extraktion würde mehr spezialisierte Bibliotheken erfordern
file_extension = "pptx" if mime_type == "application/vnd.openxmlformats-officedocument.presentationml.presentation" else "ppt"
return [{
"sequence_nr": 1,
"name": "1_powerpoint", # Simplified naming
"ext": file_extension,
"content_type": mime_type,
"data": file_content,
"metadata": {
"is_text": False,
"format": "powerpoint"
}
}]
def extract_binary_content(file_name: str, file_content: bytes, mime_type: str) -> List[Dict[str, Any]]:
"""
Fallback für binäre Dateien, bei denen keine spezifische Extraktion möglich ist.
Args:
file_name: Name der Datei
file_content: Binärdaten der Datei
mime_type: MIME-Typ der Datei
Returns:
Liste mit einem binären Content-Objekt mit metadata.is_text = False
"""
file_extension = os.path.splitext(file_name)[1][1:] if os.path.splitext(file_name)[1] else "bin"
return [{
"sequence_nr": 1,
"name": "1_binary", # Simplified naming
"ext": file_extension,
"content_type": mime_type,
"data": file_content,
"metadata": {
"is_text": False,
"format": "binary"
}
}]