Mostrando entradas con la etiqueta odt. Mostrar todas las entradas
Mostrando entradas con la etiqueta odt. Mostrar todas las entradas

miércoles, 6 de mayo de 2026

DATOS. Archivos de texto

Archivos de texto

Acceso a contenidos específicos

No tengo muy claro si esta entrada debe situarse en esta subsección (Acceso a datos) o si debería ubicarse en la que sigue (Limpieza de datos), entendida en sentido amplio, pero posiblemente esto no es lo más importante. Lo que interesa es el concepto que se trata en ella y lo que implica. Antes de decir algo más al respecto, es necesario advertir que, en cualquier caso, ahora sólo trataremos estas cuestiones en términos muy básicos y limitados, ya que será necesario retomar el tema cuando hablemos de organización y limpieza de los datos no estructurados y, como no puede ser de otro modo, también en la subsección Uso de datos.

En las entradas anteriores hemos estado tratando cuestiones relativas al acceso a los datos de los textos como un todo, diferenciando si este contenido se encuentra ubicado en tablas o si conforma párrafos más o menos extensos. Esta diferención lo es también de datos vs. textos, o si se preferie (y yo sí lo prefiero) entre datos semi-estructurados frente a datos no estructurados. En sentido estricto, ningun documento contiene datos estructurados, así que lo más parecido a éstos son los datos semi-estructurados que encontramos en las tablas y en las tablas-formulario de los documentos. Pero ahora nos hemos alejado de estos entornos y nos hemos enfrentado al texto en sentido estricto, lo que equivale a decir a los datos no estructurados; y ante estos el mero acceso (y "captura") sólo puede ser una primera (y muy necesaria) fase de un proceso considerablemente complejo que requiere varios subprocesos. Lo que ahora abordamos sería un intento de concretar una opción de posible segunda fase.

Y es que de poco sirve capturar un texto si no obtenemos información de él. Pasa lo mismo cuando hablamos del tratamiento analógico de los textos, pero es mucho más definitorio (y difícil de llevar a cabo) cuando el tratamiento es digital. Podemos concretarlo de muchas formas, así que la que ahora voy a proponer es sólo una de ellas, y no precisamente de las de mayor complejidad.

En el marco del acceso al contenido de los textos de una colección de informes psicopedagógicos, deseamos identificar si en ellos se hace alguna mención al uso del test (batería) WISC. Esto implica interés por saber si este recurso se ha empleado, con qué peso cuantitativo en la muestra documental y qué información se asocia con ese uso.

El objetivo simple, identificar uso vs. no-uso del WISC, no parece ofrecer mayor dificultad, pero acceder al contenido textual asociado a esta etiqueta conlleva cierto nivel de complejidad, que nos proponemos abordar de la forma más directa y simple posible, lo que no garantiza que sea la mejor, aunque según cual sea nuestro objetivo puede ser suficiente. Sí lo es para el objetivo que nos planteamos en esta entrada.



import os
import csv
from odf.opendocument import load
from odf import text

# --- Función secundaria 1: Extracción lineal de párrafos ignorando tablas. ---

def extraer_parrafos_no_tablas(ruta_archivo):

    try:
        doc = load(ruta_archivo)
        parrafos_limpios = []
        for p in doc.getElementsByType(text.P):
            parent = p.parentNode					 # Filtro de tablas por ancestros
            dentro_de_tabla = False
            while parent is not None:
                if parent.tagName == "table:table":
                    dentro_de_tabla = True
                    break
                parent = parent.parentNode
            
            if not dentro_de_tabla:
                contenido = "".join(node.data for node in p.childNodes if node.nodeType == 3)	 # Unimos todos los nodos de texto
                if contenido.strip():
                    parrafos_limpios.append(contenido.strip())
        return parrafos_limpios
    except Exception as e:
        print(f"   [!] Error de lectura en {os.path.basename(ruta_archivo)}: {e}")
        return []

# --- Función secundaria 2: Captura el párrafo del hallazgo y los N siguientes para asegurar el contexto. . ---

def extraer_segmento_contiguo(lista_parrafos, termino, num_posteriores=4):
   
    segmentos_completos = []
    indices_ya_incluidos = set()

    for i, texto in enumerate(lista_parrafos):
        if termino.lower() in texto.lower() and i not in indices_ya_incluidos:
            rango_fin = min(i + num_posteriores + 1, len(lista_parrafos))		# Iniciamos bloque
            bloque = lista_parrafos[i:rango_fin]
            
            for j in range(i, rango_fin):		 # Marcamos para no repetir si el término aparece en los párrafos de este bloque
                indices_ya_incluidos.add(j)
            
            segmentos_completos.append("\n\n".join(bloque))
            
    return segmentos_completos

# --- Función principal ---

def procesar_informes(dir_entrada, csv_salida, palabra_clave):
    if not os.path.exists(dir_entrada):
        print(f"La ruta {dir_entrada} no existe.")
        return

    archivos = [f for f in os.listdir(dir_entrada) if f.lower().endswith('.odt')]
    resultados = []

    print(f"Procesando {len(archivos)} archivos...\n")

    for nombre in archivos:
        ruta = os.path.join(dir_entrada, nombre)
        parrafos = extraer_parrafos_no_tablas(ruta)		# Llamada a función secundaria 1
        
        hallazgos = extraer_segmento_contiguo(parrafos, palabra_clave, num_posteriores=4)	 # Llamada a función secundaria 2
        
        estado = "PRESENTE" if hallazgos else "AUSENTE"
        print(f"[{estado}] {nombre}")
        
        resultados.append({
            'Archivo': nombre,
            'Hallazgo': estado,
            'Texto': " |SECCIÓN SIGUIENTE| ".join(hallazgos) if hallazgos else "N/A"
        })

    try:
        with open(csv_salida, mode='w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['Archivo', 'Hallazgo', 'Texto'])
            writer.writeheader()
            writer.writerows(resultados)
        print(f"\nCSV generado con éxito en: {csv_salida}")
    except Exception as e:
        print(f"Error al escribir CSV: {e}")

# --- Llamada a función ---

if __name__ == "__main__":
    RUTA_ARCHIVOS = r"ruta_de_directiro_de_documentos"		# Ruta de acceso a la colección de documento
    RUTA_CSV = r"ruta_destino_y_nombre_archivo.csv"			# Ruta para ubicación de csv e identificador del archivo
    TERMINO = "WISC"										# Término objeto de estudio

    procesar_informes(RUTA_ARCHIVOS, RUTA_CSV, TERMINO)


Lo que hacemos con este script es, en primer lugar prescindir de las tablas del documento y de los datos que éstas contienen para centrarnos en la extracción de los textos (párrafos). Dentro de ellos, buscamos la aparición del término-diana (aquí WISC) y lo relacionamos con su contexto (texto inmediatamente próximo al término-diana) para capturar el contenido que se asocia al mismo- Esto nos permite acceder no sólo a información sobre la presencia vs. ausencia del término-diana en el documento, sino también al contenido asociado a dicho término, lo que supone una mayor complejidad del procedmiento de automatización, pero también que el script es capaz de facilitar información que incrementa significativamente nuestra capacidad de análisis de los documentos. No estamos en condiciones para asegurar que esto sea suficiente para objetivos complejos, pero sí para muchos otros más simples. En todo caso sólo estamos ante un procedimiento básico. En otras subsecciones trataremos cómo desarrollar otras.

Presento también el primer script de una serie de tres (el anterior es el tercero) como punto de referencia para comprender la línea de desarrollo que se pretende explictar y que se concreta fundamentalmente como incremento del contexto textual que rodea al término-diana.



import os
import csv
from odf.opendocument import load
from odf import text

#--- Función secundaria. Filtar tablas ---

def extraer_parrafos_no_tablas(ruta_archivo):
    try:
        doc = load(ruta_archivo)
        parrafos_validos = []
        for p in doc.getElementsByType(text.P):
            dentro_de_tabla = False
            parent = p.parentNode
            while parent is not None:
                if parent.tagName == "table:table":
                    dentro_de_tabla = True
                    break
                parent = parent.parentNode
            if not dentro_de_tabla:
                contenido = "".join(node.data for node in p.childNodes if node.nodeType == 3)
                if contenido.strip():
                    parrafos_validos.append(contenido.strip())
        return parrafos_validos
    except Exception as e:
        print(f"Error al leer el archivo {ruta_archivo}: {e}")
        return []
        
# --- Función principal --- 

def procesar_coleccion_odt(directorio_entrada, ruta_csv, termino):
    print(f"Buscando en: {directorio_entrada}...")
    
    if not os.path.exists(directorio_entrada):
        print(f"ERROR: La carpeta no existe: {directorio_entrada}")
        return

    archivos = [f for f in os.listdir(directorio_entrada) if f.lower().endswith('.odt')]
    print(f"Se han encontrado {len(archivos)} archivos .odt")

    if len(archivos) == 0:
        print("No hay nada que procesar. Revisa la extensión de los archivos.")
        return

    resultados = []
    for nombre_archivo in archivos:
        ruta_completa = os.path.join(directorio_entrada, nombre_archivo)
        parrafos = extraer_parrafos_no_tablas(ruta_completa)
        hallazgos = [p for p in parrafos if termino.lower() in p.lower()]
        
        presencia = "PRESENTE" if hallazgos else "AUSENTE"
        print(f"Procesado: {nombre_archivo} -> {presencia}")
        
        resultados.append({
            'Nombre del Archivo': nombre_archivo,
            'Presencia Termino': presencia,
            'Contenido Extraído': " | ".join(hallazgos) if hallazgos else "N/A"
        })

    try:
        with open(ruta_csv, mode='w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['Nombre del Archivo', 'Presencia Termino', 'Contenido Extraído'])
            writer.writeheader()
            writer.writerows(resultados)
        print(f"\nÉXITO: Archivo creado en {ruta_csv}")
    except Exception as e:
        print(f"ERROR al crear el CSV: {e}")

# --- LLAMADA AL SCRIPT ---

if __name__ == "__main__":
    RUTA_DOCS = r"ruta_archivos_a_procesar"				 # Definición de variables de configuración
    RUTA_OUT  = r"ruta_del_archivo.csv"
    OBJETIVO  = "WISC"

    procesar_coleccion_odt(RUTA_DOCS, RUTA_OUT, OBJETIVO)
    

Para comprender lo que conseguimios con estos archivos, nada mejor que un breve estudio de resultados, aunque sin ninguna pretensión de representatividad; tan sólo a nivel de indicio. Analizo a continuación los 14 primeros archivos de una colección significativamente más amplia, pero que ahora no interesa ya que en esta entrada no pretendo realizar ningún estudio (cosa que puede quedar pendiente para otro momento).

De estos 14 archivos, sólo en tres se identifica el término-diana (WISC), lo que ahora es prácticamente irrelevante, pero que dejaría de serlo en un estudio orientado a valorar el grado de uso de un determinado recurso como es la escala WISC en sus diferentes versiones. Lo que sí es relevante ahora es comprender las diferencias que se aprecian en términos de recuperación de la información asociada al término-diana entre el primero y el tercero de los script que se desarrollaron para logar esta meta. De paso, es necesario destacar que existe una diferencia importante entre este modelo de trabajo y lo que sería la mera identificación de la presencia del término. Veamos esa evolución:

Vemos que existen notables diferencias entre los tres archivo, las cuales se deben fundamentalmente al peso real de los datos asociados al término-diana en cada uno de los documento (otra parte se puede entender asociada al mayor o menos éxito del procedimiento de obtención de datos); pero lo que resulta más importante es la evolución entre el primer y el tercer script, que en este gráfico se presenta en términos absolutos (número de palabras por segmento textual y versión del script.

Si lo expresamos en términos porcentuales, algunos datos pueden contribuir a la mejor comprensión del resultado:

  1. Salvo en el primer documento, en los otros dos el incremento obedece al esquema [Tv1 menor que Tv2 menor que Tv3]. En ese primer documento, se aprecia una ligera disminución del número de palabras, pero también es ese documento el que mayor incremento presenta entre el Tv1 y Tv2 (concretamente algo más del 466%)
  2. Esto quiere decir que el script que mejores resultados ofrece (como cabe esperar) el tercero (salvo para el primer documento, que es el segundo script).
  3. La ganancia que se obtiene con este tercer script respecto al primero es evidente: más del 400% en el documento de menos impacto, pero por encima del 700% en los otros dos, concretamente el 788% en uno y algo más del 900% en el otro.

Dado que no se trata de un estudio con intención de ser representativo, estos datos no se deben extrapolar, pero son suficientemente reveladores que lo que interesa mostrar aquí: podemos desarrollar procedimientos que facilitan el acceso selectivo a datos relativos a determinado término, aunque faltan datos que permitan identificar con propiedad la calidad de estos resultados. También esta importante cuestión queda para un análisis específico y para el desarrollo de otros procedimientos alternativos.

DATOS. Archivos de texto

Archivos .odt

Acceso a tablas y textos

Aunque no aporte mucho a lo que ya sabemos, no podemos dejar de exponer los procedimientos que nos permiten acceder al contenido de los archivos .odt, dada la frecuencia con la que trabajamos con estos archivos en este blog. Así vamos a replicar aquí lo que ya hicimos en esta entrada con los archivos .docx.



import os
import sys
import io
from odf import text, table, teletype
from odf.opendocument import load

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

# --- Función ----

def leer_contenido_odt(ruta_archivo):
    if not os.path.exists(ruta_archivo):				    # Controla que existe el archivo
        print(f"Error: El archivo '{ruta_archivo}' no existe.")
        return

    try:
        doc = load(ruta_archivo)				    		# Cargar el documento ODT
    except Exception as e:
        print(f"Error al cargar el archivo: {e}")
        return
    
    print(f"--- INICIO DEL CONTENIDO: {ruta_archivo} ---\n")

    contenido_principal = doc.text				 		 # Accedemos al texto

    for elemento in contenido_principal.childNodes:
        
        if elemento.tagName in ["text:p", "text:h"]:		# 1. Párrafos y Encabezados (h = heading, p = paragraph)
            texto = teletype.extractText(elemento).strip()
            if texto:
                etiqueta = "TÍTULO" if elemento.tagName == "text:h" else "PÁRRAFO"  # Diferenciamos si es título o párrafo
                print(f"[{etiqueta}]: {texto}")
        elif elemento.tagName == "table:table":				# 2. Tablas
            filas = elemento.getElementsByType(table.TableRow)
            nombre_tabla = elemento.getAttribute("name") or "Tabla"
            
            print(f"\n[--- INICIO {nombre_tabla} ---]")
            for fila in filas:
                celdas = fila.getElementsByType(table.TableCell)					# Extraemos el texto de cada celda
                contenido_fila = [teletype.extractText(celda).strip() for celda in celdas]
                if any(contenido_fila):												# Solo imprimimos la fila si tiene algún contenido
                    print(" | ".join(contenido_fila))
            print(f"[--- FIN {nombre_tabla} ---]\n")

    print(f"\n--- FIN DEL DOCUMENTO ---")

# --- Llamada a función ---

if __name__ == "__main__":
    nombre_archivo = r"ruta_de_mi_archivo.odt"  # Aquí el nombre del archivo .odt
    leer_contenido_odt(nombre_archivo)			# Llamada a la función


Ahora toca identificar sólo las tablas...


  
import os
import sys
import io
from odf import table, teletype
from odf.opendocument import load

# Configuración para que la consola de Windows maneje correctamente caracteres especiales
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

# --- Función de extracción de tablas ---

def extraer_tablas_odt(ruta_archivo):

    if not os.path.exists(ruta_archivo):							# Nombre del archivo a procesar
        print(f"Error: El archivo '{ruta_archivo}' no existe.")
        return
    
    try:															# Cargar el documento ODT
        doc = load(ruta_archivo)
    except Exception as e:
        print(f"Error al cargar el archivo: {e}")
        return
    
    print(f"--- EXTRACCIÓN DE TABLAS: {ruta_archivo} ---\n")

    # Buscamos todas las tablas en el documento mediante getElementsByType()
    tablas = doc.getElementsByType(table.Table)

    if not tablas:
        print("No se encontraron tablas en el documento.")
        return

    for i, t in enumerate(tablas):
        nombre_tabla = t.getAttribute("name") or f"Tabla {i+1}"
        print(f"[--- INICIO {nombre_tabla} ---]")
        
        filas = t.getElementsByType(table.TableRow)
        for fila in filas:
            celdas = fila.getElementsByType(table.TableCell)
            
            contenido_fila = [teletype.extractText(celda).strip() for celda in celdas]	# Extraemos el texto de cada celda
            
            if any(contenido_fila):														# Imprimimos la fila solo si contiene algún texto
                print(" | ".join(contenido_fila))
        
        print(f"[--- FIN {nombre_tabla} ---]\n")

    print(f"--- FINALIZADO ---")
    
# --- Llamada a la función ---

if __name__ == "__main__":
    nombre_archivo = r"ruta_de_mi_archivo.odt" 	# Aquí la ruta del archivo
    extraer_tablas_odt(nombre_archivo)			# Llamada a la función
    

Para finalizar vamos a mostrar el script que únicamente accede a los párrafos del documento, omitiendo el contenido de las tablas y de tablas-formulario.



import os
import sys
import io
from odf import text, teletype
from odf.opendocument import load

# Configuración para que la consola de Windows maneje correctamente caracteres especiales
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

# --- Función de extracción de texto (párrafos) ---

def leer_solo_parrafos_odt(ruta_archivo):
  
    if not os.path.exists(ruta_archivo):							 # Verificar si el archivo existe
        print(f"Error: El archivo '{ruta_archivo}' no existe.")
        return

    try:
        doc = load(ruta_archivo)									# Cargar el documento ODT
    except Exception as e:
        print(f"Error al cargar el archivo: {e}")
        return
    
    print(f"--- CONTENIDO TEXTUAL (SIN TABLAS): {ruta_archivo} ---\n")

    contenido_principal = doc.text									# Accedemos al elemento de texto del cuerpo

    for elemento in contenido_principal.childNodes:					# Iteramos sobre los elementos del cuerpo
        
        if elemento.tagName in ["text:p", "text:h"]:				# Filtramos: Solo nos interesan párrafos (p) y encabezados (h)
            texto = teletype.extractText(elemento).strip()
            
            if texto:												# Solo imprimimos si el párrafo no está vacío
                # Opcional: puedes quitar la etiqueta [PÁRRAFO] si quieres el texto limpio
                etiqueta = "TÍTULO" if elemento.tagName == "text:h" else "PÁRRAFO"
                print(f"[{etiqueta}]: {texto}")

    print(f"\n--- FIN DEL DOCUMENTO ---")

# --- Llamada a la función ---

if __name__ == "__main__":
    nombre_archivo = r"ruta_de_mi_archivo.odt" 		# Aquí la ruta del archivo a trabajar 
    leer_solo_parrafos_odt(nombre_archivo)			# Llamada a la función