""" 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" } }]