#!/usr/bin/env python3 # -*- coding: utf-8 -*- # -------------------------------------------------- # chunking_utils.py # # Enthält robuste Text-Chunking-Logik: # 1. Absatzbasiertes Chunking # 2. Satzbasiertes Chunking per Regex (kein NLTK) # 3. Satz-Overlap-Chunking # -------------------------------------------------- import re # -------------------------------------------------- # Hilfsfunktion: split_sentences # Zweck: # - Teilt Text in Sätze auf, basierend auf Punkt, Ausrufe- und Fragezeichen # - Trennt bei ".!? " (Satzzeichen gefolgt von Leerraum) # Parameter: # text : Volltext als String # Rückgabe: # Liste von Satz-Strings # -------------------------------------------------- def split_sentences(text: str) -> list[str]: # Regex: lookbehind für . ! oder ?, dann ein oder mehrere Whitespace-Zeichen return re.split(r'(?<=[\.!?])\s+', text.strip()) # -------------------------------------------------- # Funktion: chunk_text_paragraphs # Zweck: # - Trennt Text absatzweise in Chunks mit bis zu max_length Zeichen # - Absätze werden an doppelten Zeilenumbrüchen getrennt # - Zu große Absätze werden hart in max_length-Teile gesplittet # Parameter: # text : Volltext als String # max_length : Maximale Länge eines Chunks (Standard 500) # Rückgabe: # Liste von Strings (Chunks) # -------------------------------------------------- def chunk_text_paragraphs(text: str, max_length: int = 500) -> list[str]: paragraphs = re.split(r'\n\s*\n', text.strip()) chunks: list[str] = [] current_chunk = "" for para in paragraphs: para = para.strip() if not para: continue # Prüfen, ob Absatz noch in aktuellen Chunk passt (+2 für "\n\n") if len(current_chunk) + len(para) + 2 <= max_length: if current_chunk: current_chunk += "\n\n" + para else: current_chunk = para else: # Bislang gesammelten Chunk speichern if current_chunk: chunks.append(current_chunk) # Absatz hart splitten, wenn er allein zu groß ist if len(para) > max_length: for i in range(0, len(para), max_length): part = para[i:i + max_length] chunks.append(part) current_chunk = "" else: # Neuer Chunk beginnt mit diesem Absatz current_chunk = para # Letzten Chunk nicht vergessen if current_chunk: chunks.append(current_chunk) return chunks # -------------------------------------------------- # Funktion: chunk_by_sentences # Zweck: # - Zerlegt Text in Sätze per Regex-Split # - Baut daraus Chunks mit ganzen Sätzen bis max_length # Parameter: # text : Volltext als String # max_length : Maximale Länge eines Chunks (Standard 500) # Rückgabe: # Liste von Strings (Chunks) # -------------------------------------------------- def chunk_by_sentences(text: str, max_length: int = 500) -> list[str]: sentences = split_sentences(text) chunks: list[str] = [] current_chunk = "" for sent in sentences: sent = sent.strip() if not sent: continue # Prüfen, ob Satz noch in aktuellen Chunk passt (+1 für Leerzeichen) if len(current_chunk) + len(sent) + 1 <= max_length: if current_chunk: current_chunk += " " + sent else: current_chunk = sent else: # Bisher gesammelten Chunk speichern if current_chunk: chunks.append(current_chunk) # Einzelnen Satz hart splitten, falls er zu lang ist if len(sent) > max_length: for i in range(0, len(sent), max_length): chunks.append(sent[i:i + max_length]) current_chunk = "" else: current_chunk = sent # Letzten Chunk nicht vergessen if current_chunk: chunks.append(current_chunk) return chunks # -------------------------------------------------- # Funktion: chunk_with_sentence_overlap # Zweck: # - Baut zunächst sentence-basierte Chunks # - Fügt vom vorherigen Chunk overlap_sents Sätze vorne an # Parameter: # text : Volltext als String # max_length : Maximale Länge eines Chunks (Standard 500) # overlap_sents : Anzahl Sätze, die überlappend übernommen werden (Standard 1) # Rückgabe: # Liste von Strings (Chunks mit Kontext-Overlap) # -------------------------------------------------- def chunk_with_sentence_overlap( text: str, max_length: int = 500, overlap_sents: int = 1 ) -> list[str]: base_chunks = chunk_by_sentences(text, max_length) overlapped: list[str] = [] for idx, chunk in enumerate(base_chunks): if idx == 0 or overlap_sents <= 0: overlapped.append(chunk) else: prev = base_chunks[idx - 1] prev_sents = split_sentences(prev) context = " ".join(prev_sents[-overlap_sents:]) overlapped.append((context + " " + chunk).strip()) return overlapped