Spaces:
Sleeping
Sleeping
Download pdf_loader.py from FLAFUENTE/optchunkopt: direct link, hf CLI and curl.
- Browser
- Download file 9.93 kB
-
https://huggingface.co/spaces/FLAFUENTE/optchunkopt/resolve/main/pdf_loader.py
- Command line
-
hf download hf://spaces/FLAFUENTE/optchunkopt/pdf_loader.py
-
curl -L -o pdf_loader.py https://huggingface.co/spaces/FLAFUENTE/optchunkopt/resolve/main/pdf_loader.py
9.93 kB
| """ | |
| MIT License | |
| Autor: Fernando Lafuente | |
| Proyecto: Ejercicio opcional NLP - Chunking y Recuperación | |
| Descripción: Extracción y limpieza de texto desde archivos PDF | |
| Este módulo se encarga de extraer el contenido textual de documentos PDF | |
| y aplicar técnicas de limpieza para preparar el texto para el chunking. | |
| Utiliza PyMuPDF (fitz) para la extracción y aplica filtros pedagógicos | |
| que mejoran la calidad del texto para procesamiento posterior. | |
| """ | |
| import logging | |
| from pathlib import Path | |
| from typing import Optional | |
| import fitz # PyMuPDF | |
| import settings | |
| from utils_logging import setup_logging, log_operation_time, clean_text_basic, validate_text_length | |
| # Configurar logger específico para este módulo | |
| logger = setup_logging() | |
| class PDFLoader: | |
| """ | |
| Clase para cargar y procesar documentos PDF. | |
| Esta clase encapsula la lógica de extracción de texto desde archivos PDF, | |
| aplicando técnicas de limpieza y validación que aseguran que el texto | |
| resultante sea apropiado para chunking y generación de embeddings. | |
| La extracción se realiza página por página para permitir un mejor control | |
| sobre la calidad del texto y facilitar el debugging en documentos largos. | |
| """ | |
| def __init__(self): | |
| """ | |
| Inicializa el cargador de PDF con configuración por defecto. | |
| """ | |
| self.logger = logging.getLogger("chunking_system.pdf_loader") | |
| def load_pdf(self, pdf_path: Path) -> str: | |
| """ | |
| Carga un archivo PDF y extrae todo su contenido textual. | |
| Este método abre el PDF, procesa cada página individualmente, | |
| extrae el texto y lo concatena en un único string. Aplica | |
| limpieza básica y validaciones para asegurar calidad del texto. | |
| Args: | |
| pdf_path: Ruta al archivo PDF a procesar | |
| Returns: | |
| str: Texto completo extraído del PDF | |
| Raises: | |
| FileNotFoundError: Si el archivo PDF no existe | |
| ValueError: Si el PDF está corrupto o no contiene texto | |
| Example: | |
| loader = PDFLoader() | |
| texto = loader.load_pdf(Path("documento.pdf")) | |
| """ | |
| # Validar que el archivo existe | |
| if not pdf_path.exists(): | |
| raise FileNotFoundError(f"Archivo PDF no encontrado: {pdf_path}") | |
| self.logger.info(f"Iniciando carga de PDF: {pdf_path.name}") | |
| try: | |
| # Abrir documento PDF con PyMuPDF | |
| pdf_document = fitz.open(str(pdf_path)) | |
| total_pages = pdf_document.page_count | |
| self.logger.info(f"PDF abierto correctamente. Páginas totales: {total_pages}") | |
| # Extraer texto de todas las páginas | |
| all_text = [] | |
| for page_num in range(total_pages): | |
| page_text = self._extract_page_text(pdf_document, page_num) | |
| if page_text: # Solo agregar páginas con contenido | |
| all_text.append(page_text) | |
| self.logger.debug(f"Página {page_num + 1}: {len(page_text)} caracteres extraídos") | |
| # Cerrar documento | |
| pdf_document.close() | |
| # Concatenar todo el texto | |
| full_text = "\n\n".join(all_text) | |
| # Aplicar limpieza final | |
| clean_text = self._clean_extracted_text(full_text) | |
| # Validar que se extrajo contenido útil | |
| if not validate_text_length(clean_text): | |
| raise ValueError(f"El PDF no contiene suficiente texto válido: {pdf_path}") | |
| self.logger.info(f"Extracción completada. Texto final: {len(clean_text)} caracteres") | |
| return clean_text | |
| except Exception as e: | |
| self.logger.error(f"Error procesando PDF {pdf_path}: {str(e)}") | |
| raise | |
| def _extract_page_text(self, pdf_document: fitz.Document, page_num: int) -> str: | |
| """ | |
| Extrae texto de una página específica del PDF. | |
| Este método privado se encarga de extraer el texto de una sola página, | |
| aplicando técnicas específicas de PyMuPDF para obtener la mejor calidad | |
| de texto posible. Maneja casos especiales como páginas vacías o con | |
| elementos gráficos únicamente. | |
| Args: | |
| pdf_document: Documento PDF abierto con PyMuPDF | |
| page_num: Número de página a procesar (base 0) | |
| Returns: | |
| str: Texto extraído de la página, limpio y validado | |
| """ | |
| try: | |
| # Obtener la página | |
| page = pdf_document[page_num] | |
| # Extraer texto usando el método estándar de PyMuPDF | |
| # El método get_text() retorna el texto en el orden de lectura | |
| page_text = page.get_text() | |
| # Aplicar limpieza básica específica para esta página | |
| clean_page_text = clean_text_basic(page_text) | |
| # Validar que la página contiene texto útil | |
| if not validate_text_length(clean_page_text, min_length=10): | |
| self.logger.debug(f"Página {page_num + 1} descartada: contenido insuficiente") | |
| return "" | |
| return clean_page_text | |
| except Exception as e: | |
| self.logger.warning(f"Error extrayendo texto de página {page_num + 1}: {str(e)}") | |
| return "" | |
| def _clean_extracted_text(self, text: str) -> str: | |
| """ | |
| Aplica limpieza específica al texto extraído de PDF. | |
| Esta función implementa técnicas de limpieza especializadas para | |
| texto extraído de PDFs, que suelen contener artefactos específicos | |
| como headers/footers repetidos, numeración de páginas, etc. | |
| Args: | |
| text: Texto en bruto extraído del PDF | |
| Returns: | |
| str: Texto limpio listo para chunking | |
| """ | |
| if not text: | |
| return "" | |
| # Aplicar limpieza básica primero | |
| clean_text = clean_text_basic(text) | |
| # Limpieza específica para PDFs | |
| # Eliminar líneas que son solo números (numeración de páginas) | |
| lines = clean_text.split('\n') | |
| filtered_lines = [] | |
| for line in lines: | |
| line_stripped = line.strip() | |
| # Descartar líneas vacías | |
| if not line_stripped: | |
| continue | |
| # Descartar líneas que son solo números (posibles números de página) | |
| if line_stripped.isdigit() and len(line_stripped) <= 3: | |
| continue | |
| # Descartar líneas muy cortas que probablemente son artefactos | |
| if len(line_stripped) < 5: | |
| continue | |
| filtered_lines.append(line_stripped) | |
| # Reconstruir texto | |
| final_text = ' '.join(filtered_lines) | |
| # Normalizar espacios finales | |
| final_text = ' '.join(final_text.split()) | |
| return final_text | |
| def get_pdf_metadata(self, pdf_path: Path) -> dict: | |
| """ | |
| Extrae metadatos del archivo PDF para información adicional. | |
| Los metadatos pueden ser útiles para logging, debugging o para | |
| personalizar el procesamiento según las características del documento. | |
| Args: | |
| pdf_path: Ruta al archivo PDF | |
| Returns: | |
| dict: Diccionario con metadatos del PDF | |
| Example: | |
| metadata = loader.get_pdf_metadata(pdf_path) | |
| logger.info(f"Procesando: {metadata['title']}") | |
| """ | |
| try: | |
| pdf_document = fitz.open(str(pdf_path)) | |
| metadata = { | |
| 'title': pdf_document.metadata.get('title', 'Sin título'), | |
| 'author': pdf_document.metadata.get('author', 'Desconocido'), | |
| 'subject': pdf_document.metadata.get('subject', ''), | |
| 'creator': pdf_document.metadata.get('creator', ''), | |
| 'pages': pdf_document.page_count, | |
| 'file_size': pdf_path.stat().st_size, | |
| 'encrypted': pdf_document.needs_pass | |
| } | |
| pdf_document.close() | |
| return metadata | |
| except Exception as e: | |
| self.logger.warning(f"Error extrayendo metadatos de {pdf_path}: {str(e)}") | |
| return { | |
| 'title': pdf_path.name, | |
| 'author': 'Desconocido', | |
| 'pages': 0, | |
| 'file_size': 0, | |
| 'encrypted': False | |
| } | |
| def load_pdf_from_settings() -> str: | |
| """ | |
| Función de conveniencia que carga el PDF configurado en settings. | |
| Esta función utiliza la configuración centralizada para cargar | |
| automáticamente el primer PDF encontrado en el directorio especificado. | |
| Returns: | |
| str: Texto extraído del PDF | |
| Raises: | |
| FileNotFoundError: Si no se encuentra ningún PDF | |
| ValueError: Si el PDF no contiene texto válido | |
| Example: | |
| texto_documento = load_pdf_from_settings() | |
| chunks = chunker.create_chunks(texto_documento) | |
| """ | |
| # Obtener ruta del PDF desde configuración | |
| pdf_path = settings.get_pdf_file() | |
| # Crear instancia del loader y procesar | |
| loader = PDFLoader() | |
| # Mostrar metadatos para información | |
| metadata = loader.get_pdf_metadata(pdf_path) | |
| logger.info(f"Cargando PDF: {metadata['title']} " | |
| f"({metadata['pages']} páginas, {metadata['file_size']} bytes)") | |
| # Cargar y retornar texto | |
| return loader.load_pdf(pdf_path) |