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

lunes, 4 de mayo de 2026

DATOS. Limpieza de datos

Tablas complejas (.odt) (VI)

Optención automátizada de todos los datos de las tablas del documento

Con esta entrada finalizo la presentación de script de recuperación de datos semi-estructurados de tablas-formulario soportadas sobre archivos .odt. En este caso vamos a generar un procedimiento cuyo objetivo es obtener todos los datos de un conjunto de tablas del mismo documento, en teoría de todos los datos de todas las tablas, aunque en la práctica no es necesario y la mayoría de las veces puede que tampoco conveniente, abarcarlas todas.

Puede considerarse que este script es complementario del expuesto en la entrada anterior, y lo es en cuanto que ahora lo que pretendemos es obtener todos los datos de un único documento, mientras que antes lo que pretendíamos era obtener los mismos datos de muchos documentos diferentes. La cuestión es que este cambio de objetivo tiene varias implicaciones: en lugar de dirigir nuestros esfuerzos al estudio de una realida concreta y su manifestación colectiva en documentos individuales; ahora pretendemos recopilar exhaustivamete los datos de un único documento, aunque estos documentos comparten contener tablas-formulario y no tablas de datos en sentido estricto. De ahí que estemos hablando de datos semi-estructurados, frente a los datos estructurados que presentan las grandes (aunque no necesariamente, sí con frecuencia) bases de datos, como las que se manejan en la minería de datos y en la IA.

Una diferencia fácilmente observable que deriva del nuevo planteamiento es que antes el objetivo era obtener un archivo CSV por cada tabla que se analiza en la que cada registro (fila) es el contenido de dicha tabla en un documento; ahora vamos a generar un archivo .xlsx que contiene varias hojas, una por cada tabla del documento .odt. La elección de este soporte está justificada por nuestro interés por mantener todos los datos en un único archivo, y no tenerlos dispersos en varios, como obligaría el uso del formato csv. Esto facilita el manejo de los archivos, su consulta y posterior manipulación desde el servicio excel o Calc.



# --- 0. BIBLIOTECAS ---
import os
import pandas as pd
from odf.opendocument import load
from odf.table import Table, TableRow, TableCell
from odf import teletype

# --- 1. MOTOR DE LIMPIEZA ---
def limpiar_texto_odt(texto_bruto):
    if not texto_bruto: return ""
    texto = texto_bruto.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ')
    return " ".join(texto.split()).lower()

# --- 2. TRADUCTOR DE COORDENADAS ---
def obtener_referencia_celda(fila_idx, col_idx):
    letras = ""
    temp_col = col_idx
    while temp_col >= 0:
        letras = chr(65 + (temp_col % 26)) + letras
        temp_col = (temp_col // 26) - 1
    return f"{letras}{fila_idx + 1}"

# --- 3. ANALIZADOR DE ESTRUCTURA (MEJORADO) ---
def extraer_datos_tabla(tabla):
    filas_xml = tabla.getElementsByType(TableRow)
    if not filas_xml: return [], [], "0x0"

    max_cols = 0
    for celda in filas_xml[0].getElementsByType(TableCell):
        span = int(celda.getAttribute("numbercolumnsspanned") or 1)
        max_cols += span
    
    total_filas = len(filas_xml)
    # MODIFICACIÓN (1): Generar el dato de estructura
    tipo_tabla = f"{total_filas}x{max_cols}"
    
    ocupada = [[False for _ in range(max_cols)] for _ in range(total_filas)]
    ids, contenidos = [], []

    for r_idx, fila in enumerate(filas_xml):
        celdas_xml = fila.getElementsByType(TableCell)
        c_xml_cursor = 0 
        for c_idx in range(max_cols):
            if ocupada[r_idx][c_idx]: continue
            if c_xml_cursor < len(celdas_xml):
                celda_actual = celdas_xml[c_xml_cursor]
                c_span = int(celda_actual.getAttribute("numbercolumnsspanned") or 1)
                r_span = int(celda_actual.getAttribute("numberrowsspanned") or 1)
                
                texto = teletype.extractText(celda_actual).strip()
                ids.append(obtener_referencia_celda(r_idx, c_idx))
                contenidos.append(texto)
                
                for i in range(r_span):
                    for j in range(c_span):
                        if r_idx + i < total_filas and c_idx + j < max_cols:
                            ocupada[r_idx + i][c_idx + j] = True
                c_xml_cursor += 1
    return ids, contenidos, tipo_tabla

# --- 4. FUNCIÓN MAESTRA EVOLUCIONADA ---
def ejecutar_extraccion_a_excel(ruta_odt, mapa_claves):
    if not os.path.exists(ruta_odt):
        print(f"[!] El archivo no existe en: {ruta_odt}"); return

    directorio_salida = r"C:\PROCESAMIENTO_DATOS_SEO\TABLAS_DOC"
    if not os.path.exists(directorio_salida):
        os.makedirs(directorio_salida, exist_ok=True)

    doc = load(ruta_odt)
    tablas = doc.body.getElementsByType(Table)
    
    nombre_base = os.path.splitext(os.path.basename(ruta_odt))[0]
    ruta_excel = os.path.join(directorio_salida, f"{nombre_base}_EXTRAIDO.xlsx")
    
    # Control de duplicados por clave normalizada
    claves_encontradas = {limpiar_texto_odt(item['termino']): False for item in mapa_claves}
    
    with pd.ExcelWriter(ruta_excel, engine='openpyxl') as writer:
        for t in tablas:
            filas = t.getElementsByType(TableRow)
            if not filas: continue
            celdas_xml = filas[0].getElementsByType(TableCell)
            if not celdas_xml: continue
            
            # MODIFICACIÓN (2): Comprobar contra el mapa de claves y su posición (0 o 1)
            for configuracion in mapa_claves:
                termino_original = configuracion['termino']
                posicion_busqueda = configuracion['posicion'] # 0 o 1
                termino_norm = limpiar_texto_odt(termino_original)
                
                # Seguridad: verificar que la tabla tenga la celda solicitada
                if len(celdas_xml) <= posicion_busqueda: continue
                
                valor_celda_ancla = limpiar_texto_odt(teletype.extractText(celdas_xml[posicion_busqueda]))
                
                if termino_norm in valor_celda_ancla and not claves_encontradas[termino_norm]:
                    ids, valores, tipo_tabla = extraer_datos_tabla(t)
                    
                    # Insertar tipo_tabla como primer campo (Punto 1)
                    final_ids = ["tipo_tabla"] + ids
                    final_valores = [tipo_tabla] + valores
                    
                    df = pd.DataFrame([final_valores], columns=final_ids)
                    
                    nombre_hoja = termino_original[:30]
                    df.to_excel(writer, sheet_name=nombre_hoja, index=False)
                    
                    claves_encontradas[termino_norm] = True
                    print(f"[OK] Tabla '{termino_original}' ({tipo_tabla}) guardada.")
                    break 

    print(f"\nPROCESO COMPLETADO. Ruta: {ruta_excel}")

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

if __name__ == "__main__":

    ARCHIVO_TRABAJO = r""						#Introduce aquí la ruta absoluta del documento a procesar 
    
    MAPA_CLAVES = [								# Lista de diccionarios con término y posición (0=A1, 1=B1). Adáptala a tus necesidades
        {'termino': 'fecha', 'posicion': 0},
        {'termino': 'nie', 'posicion': 0},
        {'termino': 'escolarizado', 'posicion': 1},
        {'termino': 'admisión', 'posicion': 0},
        {'termino': 'padre', 'posicion': 0},
        {'termino': 'anteriores', 'posicion': 0},
        {'termino': 'nueva', 'posicion': 1},
        {'termino': 'discapacidad', 'posicion': 0},
        {'termino': 'ordinario', 'posicion': 1},
        {'termino': 'tipo', 'posicion': 0},
        {'termino': 'personales', 'posicion': 0}
    ]
    
    ejecutar_extraccion_a_excel(ARCHIVO_TRABAJO, MAPA_CLAVES)


Obsérvese que este script comparte la mayor parte de la estructura y la lógica con el precedente, pero presenta una diferencia de mucha importancia para el funcionamiento del script: el uso de la lista de diccionarios MAPA_CLAVES = [] que contiene la colección de términos de referencia para identificar la tabla y el código que establece la celda en la que buscar el término. La lista que aquí se presenta debe ser adaptada a las necesidades concretas que derivan del documento a procesar.

A pesar de que ahora obtenemos un .xlsx, tampoco obtenemos unas tablas que se puedan manejar directamente, siendo necesario procesarlas desde el servicio de hoja de cálculo que se desee o mediante código, aunque presentan una formulación compatible con lo que consideramos datos estructurados, lo que no impide que sea necesario organizarlos y proceder a su limpieza, si se da el caso.

Por último debo repetir aqui lo que dije al presentar el script complementario visto en su momento respecto al uso de la IA y a sus condiciones y limitaciones.

DATOS. Limpieza de datos

Tablas complejas (.odt) (V)

Volcado masivo de datos de una tabla

Además de acceder a una celda concreta, podemos acceder también al conjunto de los datos de una tabla determinada, sea de un documento en concreto o de una collección de documentos.

Sabemos que es posible acceder a una tabla por su identificador en el navegador del procesador de texto, pero este procedimiento tiene una limitación y es que en la colección de documento, todos ellos deben tener la misma estructura de tablas, y la misma denominación, al menos para la tabla que nos interesa, lo que viene a implicar, normalmente, que todos comparten el mismo orden de las tablas. El problema es que todas estas coincidencias no son frecuentes ni fáciles de observar, lo que limita y complica el procedimiento de estracción de datos.

Para sortear estas limitaciones y ampliar el funcionalidad real del script, facilitando la obtención masiva de datos de documentos que pueden no compartir aspectos formales, pero que sí comparten contenidos (tal es el caso de los diferentes modelos de, por ejemplo, informe de evalauación psicopedagógica), una opción es identificar previamente a la extracción de datos y como condición para ella, esa tabla mediante la identificación de, por ejemplo, un concepto que de identidad a la tabla como conjunto.

Para que se entienda mejor lo que quiero decir y sus implicaciones, vamos a valorar su incidencia en un ejemplo real: la obtención de los datos de la que podría ser la Tabla1 que podría identificar el navegador.

Lo que muestra esta imagen es la tabla situada visualmente en primer lugar tanto en uno de los modelos de dictamen y otro de informe. Aunque se trata de modelos concretos, otros comparten la presencia de esta tabla en términos de estructura y de contenido, aunque se puedan observar algunas diferencias que generan (a posteriori) ciertos problemas de compatibilidad. Lo ya desde el principio no comparten es el identificador de la tabla según muestra el navegador, incluyendo que como tales no son necesariamente la Tabla1. Esto complica la viabilidad de la estrategia empleada en el script de la entrada anterior.

Cierto que podríamos utilizar determinadas estrategias para dar cabida a la variedad de identificadores de las tablas en función del documento (tipo y modelo), pero esto exige la revisión detallada de la variedad documental objeto de interés. Pero disponemos de una alternativa que evita esa revisión. La base es la siguiente: si observamos ambas tablas comprobaremos que en su celda A1 (0,0) ambas presentan un texto que es de hecho una etiqueta de la base de datos que es la tabla en su conjunto; ese texto contiene y comparte, en ambos casos, el término 'fecha'. Si, como ya sabemos, recorremos las tablas del documento, accedemos a su celda A1 y a su contenido, y realizamos una identificación en función del texto que nos interesa (en este caso 'fecha') podremos acceder al contenido de cualquiera de las celdas de esta tabla o al conjnuto de ellas. Este es el comentido del script que se muestra a continuación.



# ---0. BIBLIOTECAS NECESARIAS ---

from odf.opendocument import load
from odf.table import Table, TableRow, TableCell
from odf import teletype
import os
import csv
import sys

# --- 1. MOTOR DE LIMPIEZA ---
def limpiar_texto_odt(texto_bruto):
    if not texto_bruto: return ""
    texto = texto_bruto.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ')
    return " ".join(texto.split()).lower()

# --- 2. TRADUCTOR DE COORDENADAS ---
def obtener_referencia_celda(fila_idx, col_idx):
    letras = ""
    temp_col = col_idx
    while temp_col >= 0:
        letras = chr(65 + (temp_col % 26)) + letras
        temp_col = (temp_col // 26) - 1
    return f"{letras}{fila_idx + 1}"

# --- 3. ANALIZADOR DE TABLA ---
def extraer_datos_tabla(tabla):
    filas_xml = tabla.getElementsByType(TableRow)
    if not filas_xml: return [], [], "0x0"

    max_cols = 0
    for celda in filas_xml[0].getElementsByType(TableCell):
        span = int(celda.getAttribute("numbercolumnsspanned") or 1)
        max_cols += span
    
    total_filas = len(filas_xml)
    dato_estructura = f"{total_filas}x{max_cols}"
    
    ocupada = [[False for _ in range(max_cols)] for _ in range(total_filas)]
    ids, contenidos = [], []

    for r_idx, fila in enumerate(filas_xml):
        celdas_xml = fila.getElementsByType(TableCell)
        c_xml_cursor = 0 
        for c_idx in range(max_cols):
            if ocupada[r_idx][c_idx]: continue
            if c_xml_cursor < len(celdas_xml):
                celda_actual = celdas_xml[c_xml_cursor]
                c_span = int(celda_actual.getAttribute("numbercolumnsspanned") or 1)
                r_span = int(celda_actual.getAttribute("numberrowsspanned") or 1)
                
                texto = limpiar_texto_odt(teletype.extractText(celda_actual))
                ids.append(obtener_referencia_celda(r_idx, c_idx))
                contenidos.append(texto)
                
                for i in range(r_span):
                    for j in range(c_span):
                        if r_idx + i < total_filas and c_idx + j < max_cols:
                            ocupada[r_idx + i][c_idx + j] = True
                c_xml_cursor += 1
    return ids, contenidos, dato_estructura

# --- 4. FUNCIÓN PRINCIPAL ---
def procesar_coleccion(directorio_origen, termino_busqueda, col_ancla_idx=0):
    ruta_salida_base = r"" 						# Aquí tu ruta del archivo de salida
    if not os.path.exists(ruta_salida_base):
        os.makedirs(ruta_salida_base)

    csv_maestro = os.path.join(ruta_salida_base, "") #Aquí (entre comillas) el nombre de la tabla CSV de salida
    datos_totales = []
    encabezados_globales = None
    termino = limpiar_texto_odt(termino_busqueda)

    if not os.path.exists(directorio_origen):
        print(f"[ERROR] No existe la carpeta: {directorio_origen}", flush=True)
        return

    archivos = [f for f in os.listdir(directorio_origen) if f.endswith('.odt')]
    
    print(f"\n>>> INICIO: Detectados {len(archivos)} archivos.", flush=True)	 # flush=True fuerza a mostrar el texto

    for nombre_archivo in archivos:
        ruta_completa = os.path.join(directorio_origen, nombre_archivo)
        try:
            doc = load(ruta_completa)
            tablas = doc.body.getElementsByType(Table)
            encontrado = False
            
            for t in tablas:
                filas = t.getElementsByType(TableRow)
                if not filas: continue
                celdas = filas[0].getElementsByType(TableCell)
                
                if len(celdas) <= col_ancla_idx: continue
                
                valor_ancla = limpiar_texto_odt(teletype.extractText(celdas[col_ancla_idx]))
                
                if termino in valor_ancla:
                    ids, valores, estructura = extraer_datos_tabla(t)
                    if encabezados_globales is None:
                        encabezados_globales = ["ARCHIVO_ORIGEN", "ESTRUCTURA"] + ids
                    
                    datos_totales.append([nombre_archivo, estructura] + valores)
                    print(f" [OK] {nombre_archivo} | Estructura: {estructura}", flush=True)
                    encontrado = True
                    break
        except Exception as e:
            print(f" [ERR] Error en {nombre_archivo}: {e}", flush=True)

    if datos_totales:
        with open(csv_maestro, mode='a', newline='', encoding='utf-8-sig') as f:
            escritor = csv.writer(f)
            escritor.writerow(encabezados_globales)
            escritor.writerows(datos_totales)
        print(f"\n>>> ÉXITO: CSV generado en {csv_maestro}", flush=True)
    else:
        print("\n>>> AVISO: No se encontraron coincidencias.", flush=True)

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

if __name__ == "__main__":
    CARPETA = r""		# Aquí el directorio de los archivos a trabajar
    TERMINO = ""		# Aquí el término de identificación
    # 0 = A1, 1 = B1	# Identificadores de celda de referencia
    procesar_coleccion(CARPETA, TERMINO, 0)


Este script es suficientemente complejo como para necesitar una explicación con cierto detalle. Tenemos un script que desarrolla un sistema automatizado de extracción de datos jerarquizado que utiliza la biblioteca odfpy. Consta o requiere unas funciones que identificamos como secundarias, una función principal y el código de llamada a la función.

Las funciones secundarias realizan tareas de limpieza def limpiar_texto_odt(texto_bruto):, cálculo de coordenadas para la identificación de las celdas def obtener_referencia_celda(fila_idx, col_idx): y para la reconstrucción de celdas combinadas def extraer_datos_tabla(tabla):.

def procesar_coleccion(directorio_origen, termino_busqueda, col_ancla_idx=0): es la función principal, la que gestiona el flujo de archivos, la búsqueda del término identificador y el almacenamiento de los datos como CVS. Esta función es la que llama a las funciones secundarias y es llamada, a su vez, por el script principal.

Este script contiene la identificación de variables (ruta de archivos, término de búsqueda y celda de referencia para la búsqueda) y la llamada a la función principal. Sobre la doble opción de celda, decir que se ha implementado para evitar la dificultad que presentan aquellas tablas cuya celda A1 puede resultar inapropiada por diversos motivos, por ejemplo, por estar vacía sin que ello implique algún tipo de irregularidad.

Sin la ayuda de IA-Gemini no habría sido posible desarrollarla, pero incluso con ella me resultó de gran dificultad, dado que fue necesario reformular el procedimiento varias veces, reconstruirlo paso a paso y controlar que el funcionamiento de cada una de las partes y del conjunto se ajustar a lo esperado y se alcanzara objetivo final: obtener un archivo CSV que contenga el texto de cada una de las celdas de la tabla de dos conjuntos de documentos que comparten (como se aprecia en la imagen superior) la estructura y el contenido de esa tabla identificada por el string 'fecha'.

No se prevé, no obstante, que ese documento resultante sea de utilidad inmediata, ya que se precisa cierto tratamiento que puede ejecutarse, eso sí, manualmente sobre el servico de hoja de cálculo o mediante código. Pero este es un proceso que deriva a una segunda fase del tratamiento de datos: el de la organización y limpieza de datos obtenidos mediante procedimientos de extracción.

DATOS. Limpieza de datos

Tablas complejas (.odt) (IV)

Acceso al contenido de celdas

En la entrada anterior conseguimos acceder a una tabla concreta y a una celda determinada. Aprovechando estas funcionalidades, en ésta el objetivo es convertir ese conociento en una utilidad de interés: la obtención de un dato determinado (podrían ser varios) de un grupo de documentos iguales (comparten tipología .odt, tabla y estructura de la tabla).

Este script es el primero de una serie de tres que tratarán sobre la obtención masiva y automatizada de contenido de las tablas de los documentos .odt.



# --- 0. BIBLIOTECAS ---
import os
import csv
from odf import opendocument, table, teletype

# --- 1. CONFIGURACIÓN ---
ruta_directorio_fuente = r"mi_directorio" 			# Carpeta con los .odt
nombre_tabla_objetivo = "mi_tabla"		  			# Tabla elegida
fila_destino = 1						  			# Referencias de la celda
columna_destino = 1

directorio_destino = r"directorio_de_escritura_csv"	# Configuración de salida (Rutas solicitadas)
nombre_archivo_csv = "extraccion_tabla_celda.csv"	# Configuración del nombre del CSV (e. extraccion_tabla4_A1)
ruta_completa_csv = os.path.join(directorio_destino, nombre_archivo_csv)

# --- 2. PREPARACIÓN DEL ENTORNO ---
if not os.path.exists(directorio_destino):		# Crear el directorio de destino si no existe
    os.makedirs(directorio_destino)
    print(f"Directorio creado: {directorio_destino}")

datos_acumulados = []		# Lista para almacenar los resultados de la consulta

# --- 3. PROCESAMIENTO DE ARCHIVOS ---
if not os.path.exists(ruta_directorio_fuente):
    print("Error: El directorio fuente no existe.")
    exit()

archivos = [f for f in os.listdir(ruta_directorio_fuente) if f.endswith(".odt")]

print(f"--- Procesando {len(archivos)} archivos ---")

for archivo in archivos:
    ruta_archivo = os.path.join(ruta_directorio_fuente, archivo)
    
    try:
        doc = opendocument.load(ruta_archivo)
        tablas = doc.getElementsByType(table.Table)
        tabla_encontrada = None

        for t in tablas:
            if t.getAttribute("name") == nombre_tabla_objetivo:
                tabla_encontrada = t
                break

        if tabla_encontrada:
            filas = tabla_encontrada.getElementsByType(table.TableRow)
            if fila_destino < len(filas):
                celdas = filas[fila_destino].getElementsByType(table.TableCell)
                if columna_destino < len(celdas):
                    contenido = teletype.extractText(celdas[columna_destino])
                    datos_acumulados.append([archivo, contenido])
                    print(f"[OK] {archivo} -> Extraído")
                else:
                    print(f"[Error] {archivo} -> Columna {columna_destino} no existe.")
            else:
                print(f"[Error] {archivo} -> Fila {fila_destino} no existe.")
        else:
            print(f"[Error] {archivo} -> Tabla '{nombre_tabla_objetivo}' no encontrada.")

    except Exception as e:
        print(f"[Error] {archivo} -> No se pudo procesar: {e}")

# --- 4. GENERACIÓN DEL ARCHIVO CSV ---
if datos_acumulados:
    try:
        with open(ruta_completa_csv, mode='a', newline='', encoding='utf-8-sig') as archivo_csv:
            escritor = csv.writer(archivo_csv, delimiter=';') 	# Usamos ';' por compatibilidad con Excel en español
            escritor.writerow(["Nombre del Archivo", "Contenido de la Celda"])	# Escribir cabeceras
            escritor.writerows(datos_acumulados)								# Escribir los datos
        print(f"\n--- PROCESO FINALIZADO ---")
        print(f"Archivo guardado en: {ruta_completa_csv}")
    except Exception as e:
        print(f"\nError al intentar crear el archivo CSV: {e}")
else:
    print("\nNo se encontraron datos para guardar en el CSV.")


Este script requiere el conocimiento previo del documento para ser concretado, dado que es encesario que el directorio contenga documentos que compartan características, estructura y contenidos. De no ser así los datos que se obtengan dificilmente satisfarán el objetivo con el que se creó este script. Este conocimiento se basa, en lo concreto, en el estudio de la estructura de tablas del documento y de las celdas de la tabla elgida según el objetivo del estudio, especialmente de la naturaleza de ese contenido textual.

Además de acceder al documento, a la tabla y a la celda seleccionadas por el usuario, y capturar su contendio, se muestra por pantalla el proceso de extracción de datos y después se genera un archivo CSV (nombre_archivo_csv = "extraccion_tabla_celda.csv") en el directorio que el usuario decida (directorio_destino = r"directorio_de_escritura_csv"). Este archivo CSV se puede manipular mediante código, pero también visualizar y manipular directamente desde un servicio de hoja de cálculo (Excel o Calc).

Aunque aquí sólo ejemplificamos un procedimiento básico (extraer únicamente un dato) el script se puede modificar para la extracción de todos los datos que se consideren necesarios para un estudio concreto, por ejemplo, el estudio de la relación entre dos variables.

DATOS. Limpieza de datos

Tablas complejas (.odt) (III)

Acceso a tablas

Sirva lo visto en la entrada anterior como base para un mejor conocimiento de la estructura de tablas de un documento .odt y ocupémonos en esta de cómo acceder a esa colección y a cada una de esas tablas mediante código Python.

Para manejar documentos .odt desde Python disponemos de varias bibliotecas que ahora únicamente menciono para que puedas conocerlas y estudiarlas con más detalle. Me refiero a:

  1. ODFPY, biblioteca de bajo nivel que permite controlar prácticamente cualquier aspecto de un archivo .odt
  2. EzODF, cuyo objetivo es que crear un documento sea mucho más intuitivo.
  3. Pypandoc, pensada para la conversión de archivos .odt (de Markdown a ODT o de ODT a PDF/HTML).
  4. y finalmente Relatorio, interesante para trabajar con plantillas.

Dado que nosotros nos centramos ahora en la extracción de datos, las bibliotecas que usaremos serán preferentemente las dos primeras, por lo que te sugiero que te centres en ellas en tus investigaciones.

El acceso al contenido de un documento .odt es, en realidad, el acceso a un archivo zip que contiene varios documentos XML, por lo que si conviertes un archivo .odt a .zip y accedes a su contenido podrás localizar, entre otros, un documento llamado content.xml; si lo abres verás todo el código de etiquetas xml entre las que están las que identifican a las tablas y su estructura.

Pero si lo que deseas es acceder a las tablas de forma mucho más funcional puedes aplicar este script al documento que tu elijas, siempre que sea un documento.odt y contega tablas.



from odf import opendocument, table

#Funciones de los módulos de la biblioteca odfpy
'''
1. opendocument. Permite la gestión del archivo como un "contenedor" completo
Cargar y guardar: Permite abrir archivos (opendocument.load()) y salvar los cambios (doc.save())
Crear documentos: Generar un documento nuevo desde cero
Acceso a la estructura: Administrador de todos los elementos del archivo (tablas, párrafos, estilos)

2. table. Trabajo con las hojas de cálculo y las tablas de los documentos de texto
Creación de tablas: Permite definir nuevas tablas (table.Table)
Gestión de filas y celdas: Proporciona los objetos necesarios: table.TableRow para las filas y table.TableCell para las celdas
Atributos de celda: Manejar propiedades como la unión de celdas (spans) y el nombre de la tabla
'''

doc = opendocument.load("mi_documento.odt")		# Cargar archivo

tablas = doc.getElementsByType(table.Table)		# Buscar todas las tablas (etiqueta )

for t in tablas:								# Recorrido y visualización del identificador o nombre de las tablas
    nombre_tabla = t.getAttribute("name")
    print(f"Encontrada: {nombre_tabla}")


Observa que estamos trabajando con la biblioteca ODFPY, concretamente los módulos opendocument, que permite acceder al documento (doc = opendocument.load("mi_documento.odt"), y table, que facilita el acceso a las tablas (tablas = doc.getElementsByType(table.Table)); de las cuales visualizamos su atributo "name" (nombre_tabla = t.getAttribute("name")), que es el mismo que nos permite ver el navegador del documento.

Hasta aquí hemos conseguido acceder al equivalente de visualización del listado de tablas del documento que proporciona la utilidad Navegador del procesador de texto (Writer), lo cual no deja de tener su interés, ya que refuerza la idea de que es útil conocer y manejar ese navegador. Pero la utilidad inmediata puede que aun no se constate, así que vamos a avanzar un poco más en esa línea, accediendo al contenido de una celda concreta de una tabla determinada.

Aquí nos interesa hablar de una cuestión que puede parecer obvia, pero que no deja de tener relevancia en el manejo de estas tablas-formulario: en ellas no existe nada que diferencie lo que en la lógica de la estructura de una tabla de datos (base de datos) se puede considerar una diferenciación fundamental y básica: no es lo mismo una etiqueta o nombre de campo que el campo (contenido del campo) en si mismo. En una tabla-formulario una determinada posición o celda (A1, por ejemplo) simplemente contiene texto (y siempre texto) o no lo contiene, sin importar si ese texto es el dato o es la etiqueta. El conocimiento de esa diferencia, con absoluta seguridad, sólo lo puede aportar la persona que trabaja con el documento.Pueden emplearse determinadas estrategias para inferir si estamos ante uno u otro contenido (ejemplo, si el texto de la celda finaliza en : (dos puntos), es de esperar que se trate de una etiqueta), pero no existe seguridad absoluta en que la inferencia sea viable ni cierta. De aquí quiero derivar la importancia de la implicación directa del programador en el diseño del procedimiento, por encima de la automatización y/o del uso de la IA.

Pero volvamos a retomar el desarrollo de código para acceder al contenido de las tablas, ahora con independencia de la naturaleza de este contenido, el cual es "etiquetado" como etiqueta o como dato por el programador. El código sólo puede facilitar la automatización del acceso, lo cual no es es poco.



from odf import opendocument, table, teletype, text 

#Funciones de los módulos nuevos de la biblioteca odfpy
'''
3. text. Facilita el trabajo con los elementos estructurales del documento
Párrafos (text.P): Crear los bloques de texto que se insertan en celdas o en el cuerpo del documento
Estilos de texto: Definir encabezados (text.H) y aplicar formatos a fragmentos de texto

4. teletype. Utilidad para facilitar el trabajo con los nodos XML
extractText(): Extrae todo el texto plano de un elemento (una celda o una tabla completa), ignorando las etiquetas internas. Necesario para leer contenido
addTextToElement(): Inserta una cadena de texto dentro de un elemento ODF, encargándose de convertir los caracteres originales

'''

# --- 1. CONFIGURACIÓN ---
ruta_archivo = r""                   #Aquí la ruta completa de tu documento
nombre_tabla_objetivo = ""    #Aquí la tabla elegida según nombre de el navegador
fila_destino = 0                     # Fila de interés
columna_destino = 0             # Columna de interés
nuevo_texto = ""                  #Aquí tu nuevo texto

# --- 2. ACCESO ----
doc = opendocument.load(ruta_archivo)   #Acceso al documento

# --- 3. LOCALIZACIÓN E INVENTARIO ---
tablas = doc.getElementsByType(table.Table)
tabla_encontrada = None

print("\n--- Inventario de tablas en el documento ---")

for i, t in enumerate(tablas):
    nombre_actual = t.getAttribute("name")
    if nombre_actual == nombre_tabla_objetivo:      # Verificamos si es la tabla que buscamos para añadir una marca visual
        tabla_encontrada = t
        print(f"  {i+1}. [X] {nombre_actual} <-- ¡TABLA IDENTIFICADA!")  # Marca visual (-->) y mayúsculas para resaltara
    else:
        print(f"  {i+1}. [ ] {nombre_actual}")      # Listamos el resto de tablas con un prefijo normal

print("-------------------------------------------\n")

if not tabla_encontrada:            # Si tras el bucle no se encontró, detenemos el proceso con un aviso
    print(f"Error: No se encontró la tabla '{nombre_tabla_objetivo}' en el listado anterior.")
    exit()                           # Aquí ponemos un exit()

# --- 4. ACCESO Y ESCRITURA CORREGIDA ---
if tabla_encontrada:
    filas = tabla_encontrada.getElementsByType(table.TableRow)
    if fila_destino < len(filas):
        celdas = filas[fila_destino].getElementsByType(table.TableCell)
        if columna_destino < len(celdas):
            celda_objetivo = celdas[columna_destino]
            contenido_viejo = teletype.extractText(celda_objetivo) # LEER: Esto sigue funcionando igual
            print(f"Contenido previo: '{contenido_viejo}'")
            													 	# ESCRIBIR: El procedimiento correcto
            celda_objetivo.childNodes = []                       	# 1. Limpiamos la celda de párrafos u objetos anteriores
            nuevo_parrafo = text.P()                             	# 2. Creamos un elemento párrafo
            teletype.addTextToElement(nuevo_parrafo, nuevo_texto)   # 3. Añadimos el texto al párrafo
            celda_objetivo.addElement(nuevo_parrafo)                # 4. Insertamos el párrafo dentro de la celda
            
# --- 5. GUARDADO ---
            doc.save(ruta_archivo)
            print(f"Éxito: Se ha escrito '{nuevo_texto}' en la celda.")
        else:
            print("Error: Columna no encontrada.")
    else:
        print("Error: Fila no encontrada.")
else:
    print(f"No se encontró la tabla '{nombre_tabla_objetivo}'.")


Este script, además de acceder y visualizar a las tablas, identifica y se centra en una de ellas (nombre_tabla_objetivo = "") que queda identificada en el listado (if nombre_actual == nombre_tabla_objetivo:). Sobre ella realizamos dos acciones:

  1. Capturar el texto que contiene la celda
  2. Escribir en ella, aunque de forma indirecta, pasando antes por el objeto párrafo

Sobre esta base es posible escribir contenido en las tablas, pero también (y es lo que nos interesa ahora) obtener el escrito en documentos trabajados manualmente. De ahí podemos derivar procedimientos de acceso a datos específicos, habiendo previamente dlimitado los documentos a analizar, la tabla según se muestra en el navegador y la celda identificada según vimos en la entrada correspondiente. Todo esto nos permite construir una utilidad de obtención de datos concretos de un documento o de una colección de documentos iguales.

domingo, 3 de mayo de 2026

DATOS. Limpieza de datos

Tablas complejas (.odt) (II)

Acceso a tablas desde el navegador

Para empezar creo necesario plantea algunas cuestiones respecto a cómo se concreta la implementación de este tipo de tabla-formulario en la mayoría de los documentos prescriptivos y/o de comunicación entre servicios. Y lo hago a sabiendas que, en gran parte, es un brindis al Sol: si no hemos sido capaces (aun) de generalizar la cumplimentación de tablas mediante procedimientos basados en la lógica de Combinar correspondencia, a pesar de tener disponible esta funcionalidad desde hace tanto tiempo, no parece razonable esperar que en el diseño de los documentos se tenga en cuenta lo que implica un procedimiento posterior e hipotético de acceso automatización del acceso a los datos que contienen las tablas de dichos documentos. Y sin embargo sería de gran utilidad hacerlo y no supone mayor complicación.

Tomemos el ejemplo de las tablas mostradas en la entrada precedente...

Lo que vemos es una captura de pantalla de parte de la página 1 del actual modelo de informe de la consejería de Educación de Asturias, el cual aporta como principal novedad un incremento considerable de la estructura tabular y el incremento también significativo de su plantemamiento como formulario, incluyendo el uso de controles de formulario, aunque no en estas tablas. No voy a entrar aquí en lo que este cambio implica en términos de práctica de los SEO ni para la conceptualización de la evaluación psicopedagógica (no es el momento), pero sí en lo que afecta a la cuestión que sí se trata en esta entrada: el desarrollo de un procedimiento de automatización tanto de la cumplimentación del documento como de la estracción de los datos que potencialmente podrían albergar estas tablas.

Al respecto aprecio que esta nueva formulación acentúa la conceptualización del modelo de informe como un soporte para ser usado directamente, efectivamente a modo de formulario; un formulario extenso y complejo, pero un formulario a fin de cuentas. Y hace ya mucho tiempo que este plantemiento está obsoleto, habiendo quedado demostrado largamente que, lejos de facilitar el trabajo a los profesionales, se lo complica aun más. Conisdero cierto esto desde la perspectiva de la creación del informe, pero también (añado ahora) pensando en un futuro en el que se plantee procesar estos documentos como fuente de información pero, dadas las circunstancias que la propia práctica actual refuerza, nos encontremos con que no existe una fuente de datos normalizada, la misma que debería haber servido en su momento para automatizar la creación del documento y que ahora podría servir como fuente de datos, estructurada y accesible.

Ya en otras ocasiones, como motivo del análisis de otros modelos de informe, comenté la conveniencia de mejorar la formualción estructural de las tablas para facilitar su uso manual y la propia automatización. Es evidente que no es la vía por la que ha optado la Administración; tampoco evidentemente por impulsar la automatización ni de la composición de los informes, ni del acceso a sus datos como fuente de información relevante para el conocimiento de la práctica de los SEO. Y eso que debería estar interesada en ello, más aun en tiempos en los que tanto se habla del interés que tiene el análisis de datos como recurso para la mejora de los servicios.

Aun a sabiendas de que lo que diga sea, como ya dije al principio de esta entrada, un brindis al Sol, vuelvo a plantear algunas propuestas pensando en esas posibles mejoras de manejo documental en la composición, pero también, ahora, en facilitar el acceso futuro a los datos.

Y planteo como cuestión de interés la recomendación de que conocer y manejar la información que aporta el navegador del documento; el cual, por permanecer normalmente oculto, no suele ser ni conocido ni, en consecuencia, consultado, aunque la información que aporta es de gran utilidad: entre otras cosas nos ayuda a identificar los componentes estructurales del documento, incluyendo las tablas, a trasladarnos de uno a otro y a conocer sus identificadores. En Writer ofrece esta apariencia...

... y es suficiente con hacer click derecho sobre la cruz (+) de Tablas, o posicionarse en una de ellas en el documento, para que se despliegue el listado de todas las que contiene el documento, lo que facilita información cuantitativa, ordinal y nominal sobre todas ellas, además del desplazamiento por cada una de ellas.

Un poco más oculto aun, en la parte inferior derecha de la pantalla, casi al borde de la zona de trabajo, se ubica un elemento informativo de gran utilidad para el conocimiento y estudio de la estructura de las tablas, conocimiento que es necesario (como veremos) para el desarrollo de procedimientos de acceso a tablas y a datos. Me refiero a la información que devuelve en esa zona el posicionamiento del cursor en el interior de una tabla. Como podemos ver en la imagen que sigue, obtenemos información sobre la tabla: su identificador (conforme al navegador) y celda en coordenadas cartesianas compatible con el formato de hoja de cálculo. En este ejemplo, subrayado en amarillo, nos indica que el cursor se ubica actualmente en la celda B2 de la identificada como Tabla3.

Además de para conocer mejor el servicio Writer, todo lo anteriormente ducho es útil para el manejo del documento, aunque mostrará aun más su utilidad en los procedimientos de automatización que iremos explicando en estas entradas.

martes, 3 de junio de 2025

Textos. OOo Basic

Matrices como recurso para generar datos (output)


También podemos utilizar matrices asociadas a bucles como recurso para generar datos de salida. En realidad no es un procedimiento muy diferente al que empleamos para [facilitar el input] como podremos ver en esta entrada.



En este caso tenemos que dar por supuesto que la matriz de datos cuenta con todos los necesarios, sin entrar en detalles sobre el modo en que llegamos a esta situación (1). Suponemos también que nos enfrentamos a la tarea de automatizar la generación del texto de salida (output) (2) siendo el script el que sigue:

Dim mDatos(7) As String, vTexto As String
Dim i As Integer

mDatos(0) = "Con fecha"
mDatos(1) = "12 de mayo de 2024"
mDatos(2) = "en el CP Caudalia"
mDatos(3) = "tiene lugar la reunión de tutoría"
mDatos(4) = "con la madre"
mDatos(5) = "del alumno"
mDatos(6) = "Carlos López"

Para reconfigurar los segmentos textuales contenidos en la matriz en un único string (3), usamos el bucle For como sigue

vTexto = ""

For i = 0 To UBound(mDatos())
vTexto = vTexto & " " & mDatos(i)
Next

MsgBox (vTexto)

Quede aclarado que MsgBox() suple aquí el procedimiento de escritura del texto resultante en el documento (output), simplificación que me permito por motivos didácticos. Pero lo importante es explicar el funcionamiento del bucle.
  • Recorremos la matriz mDatos() desde su inicio (i = 0) hasta su final (UBound(mDatos()) mediante el bucle For (For i = 0 To UBound(mDatos()))...
  • Asignando a la variable vTexto el contenido de cada uno de los elementos de la matriz (vTexto = vTexto & " " & mDatos(i))...
  • Instrucción en la que quiero incidir en dos detalles:
    • La recursividad: en el nuevo ciclo, sobre la variable vTexto escribimos el contenido de la propia variable en el ciclo precedente (vTexto = vTexto & ...)
    • El añadido de una cadena vacía como separador entre segmentos (& " " &), que no sería necesaria si cada segmento ya la incluyera como elemento final de su string, solución que no recomiendo.
El resultado presenta una pequeña "deficiencia": el string resultante se inicia con un espacio en blanco debido al modo en que se construye la secuencia. Para la mayoría de los usos del procedimiento esto puede resultar irrelevante, así que la solución anterior puede ser más que suficiente. No obstante si nos interesa eliminar ese espacio al inicio de la cadena podremos utilizar la alternativa que sigue...

For i = 0 To UBound(mDatos())
If i = 0 Then
vTexto = mDatos(i)
Else
vTexto = vTexto & " " & mDatos(i)
End If
Next

... en la que mediante un condicional diferenciamos el procedimiento al inicio del ciclo (If i = 0 Then -> vTexto = mDatos(i)) en el que asignamos directamente a vTexto el contenido del primer elemento de la matriz (i = 0) sin el ahora innecesario espacio en blanco, de las siguientes fases del mismo, en las que mantenemos el procedimiento ya explicado (Else -> vTexto = vTexto & " " & mDatos(i)).

NOTAS

(1) Aunque es fácil plantearlo de este modo, en realidad se trata de una cuestión que está cargada de interrogantes, pero ahora simplemente no toca plantearlos.
(2) Supongamos que es parte de la automatización de la composición de un documento
(3) Que reconstruimos sobre la variable vTexto, por lo que inicialmente asignamos a ésta el contenido cadena vacía (vTexto = "") como forma de clarificar el funcionamiento esperado.

Textos. OOo Basic

Matrices como recurso para la entrada de datos

Con ser muy importante la mejora  que permite el [cambio de matrices por variables] para el manejo del código, no es la única ventaja que aporta, ya que esta sustitución facilita el acceso a las posibilidades que supone el uso de estructuras de iteración. En esta entrada concretaremos esa ventaja en la dase input o de solicitud de datos al usuario.



Ya hemos explicado que el uso de InputBox() es probablemente la forma más simple de resolver el problema de cómo aportar al script los datos que se necesitan para cumplir su objetivo, sea éste el que sea (1)...

vAlNom = InputBox("Nombre del alumno o alumna: ")

... pero cuando tenemos que repetir esto 20 o treinta veces esta sencillez se convierte en molesta. Aquí caben dos alternativas: crear una función o emplear un bucle.

En realidad, lo de la función no deja de ser simplificar el código, pero no evita tener que repetir la llamada a la función tantas veces como variables tengamos... o cuantos elementos tenga la matriz. Sin embargo, la segunda alternativa, el uso de un bucle, sí resuelve el tema, tanto si (además) utilizamos una función como si no. Claro que para ello es necesario utilizar matrices en lugar de variables (2).

Veamos un ejemplo:

  • Tenemos una matriz con 10 elementos -> Dim mDatos(10) As String
  • Declaramos además una variable contador -> Dim i As Integer
  • Mediante la estructura For recorremos la matriz aplicando la función InputBox() a cada elemento:
For i = 0 To UBound(mDatos())

mDatos(i) = InputBox()

Next

De este modo, y de forma sencilla, el usuario introduce los datos en cada uno de los diez elementos de la matriz sin que sea necesario repetir diez veces el mismo proceso. No obstante, tenemos un problema para el que existen dos soluciones: el usuario no sabe qué información se le solicita, así que no sabe qué introducir, ya que le falta información aclaratorio en el InputBox(), la cual sí estaba disponible en el formato anterior (InputBox("Nombre del alumno o alumna: ")).

Este problema no se resuelve en el ciclo del mismo modo que fuera de él, ya que no se solicita 10 veces (por ejemplo) el mismo dato. Debemos buscar otra solución, porque no podemos esperar que se resuelva solo recurriendo a que el usuario es conocedor del documento, ni incluso que sea la misma persona que hizo el script. 

La solución más sencilla es crear, aportar (y tener a mano en el momento adecuado) un documento informativo que asocie cada elemento de la matriz con su contenido.

Ítem 0 -> Nombre del alumno 
Ítem 2 -> Apellidos del alumno 

... En este caso sería necesaria una pequeña modificación en el InputBox() (InputBox("Ítem " & i)o similar).

La segunda opción es más costosa pero más apropiada, además de posiblemente también más funcional (3): crear una segunda matriz tipo string cuyos elementos sean sencillas cadenas de texto que informan sobre el contenido de cada elemento de la matriz de datos (4)...

Dim mConten() As String
mConten = Array("Alumno. Nombre","Alumno. Apellidos","Alumno. Fecha de nacimiento"...)

... siendo necesario modificar también el InputBox() (InputBox(mConten(i))

NOTAS

(1) Por ejemplo, la automatización de la escritura de un texto. La forma más sencilla de disponer de un dato tan simple como necesario como es el nombre del alumno es preguntarle al usuario que trabaja sobre el documento que hemos automatizado.
(2) En realidad podemos utilizar variables (y no matrices), pero necesitamos que el nombre de las matrices se asemeje al formato de los elementos de la matriz, así que no deja de ser un sucedáneo del uso de matrices.
(3) Evitamos el riesgo de que se pierda el documento que contiene el listado anterior.
(4) Aun en este caso el código es más limpio y sencillo que utilizando reiteradamente la forma primitiva de la instrucción de input

Textos. OOo Basic

Variables vs. matrices


A fuerza de necesitar conjuntos cada vez más grandes de variables en proyectos de automatización de textos, el uso alternativo de matrices ha supuesto mejoras significativas en el manejo y en la creación del script, aunque este cambio también presenta inconvenientes. Una ventaja asociada al uso de matrices es la posibilidad de trabajar con bucles.



Primero explicaré qué supone modificar la asignación de contenido a variables vs. matrices. Para ello un ejemplo sencillo es la creación de un sistema de asignación de datos mediante InputBox() (1) cuando el número de variables es ya un tanto elevado, por ejemplo cuando alcanzamos la veintena de datos que solicitamos al usuario (2).

En realidad es indiferente que esas veinte o más variables requieran todas ellas de su correspondiente interfaz, o que parte de ellas se cumplimenten "automáticamente" mediante un procedimiento de condicionalidad referenciada (3)...

If vNombreSex = "H" Then
vTratam1 = "niño"
Else 
vTratam1 = "niña"

... el hecho es que ya la simple declaración de veinte variables supone realizar un trabajo innecesario, además de complicar el manejo de este código. Por suerte tenemos una fácil solución a mano: utilizar matrices.

El resultado es un código mucho más limpio y manejable (4), aunque debemos saber que tiene un coste: perdemos en inteligibilidad del código. Usando variables, el nombre de cada una de ellas puede ser suficientemente esclarecedor respecto al dato que contiene (5), siendo ésta una recomendación que se reitera en los manuales de programación; pero cuando usamos una matriz esa posibilidad se pierde en cierta medida. 

Entiendo que en este caso puede ser muy conveniente (6) crear una documentación anexa al script que contenga una tabla en la que se indique la relación entre cada elemento de la matriz con el contenido que referencia

NOTAS

(1) Por poner un ejemplo muy socorrido y usado por su sencillez de implementación. Es dudoso que cuando el número de variables-input a manejar sea elevado, la solución interfaz sea InputBox(). En estos casos, posiblemente lo mejor y más sencillo sea trabajar con un formulario sobre una hoja de cálculo con funciones de gestor, posiblemente una solución con categoría de [docap complejo].
(2) Esa es una cantidad relativamente moderada de variables, ya que en documentos complejos (vg. dictamen de escolarización) las variables necesarias la superan con mucho. En un [ejemplo reciente], la automatización de este documento ha supuesto trabajar con un total de 63 campos/variables, y eso que sólo se trabaja con el núcleo principal del documento y no con sus dos anexos.
(3) Este es el caso de las variables que contienen datos relativos a la expresión de la concordancia gramatical de género o número. En los ejemplos de documentos con los que trabajamos, estas variables expresan normalmente la concordancia de género y se asocian al género gramatical del sujeto, muy frecuentemente a la dicotomía alumno vs. alumna.
(4) Cierto que más cercano a infinito conforme mayor sea el número de variables. LO que debemos sopesar es si interesa trabajar con una o con varias matrices, atendiendo al tipo de datos. La solución de matriz tipo variant no es ni siempre ni necesariamente la mejor opción, aunque nos permite concentrar en una única matriz los distintos tipos de datos posibles.
(5) Al dato e incluso al tipo: es posible leer Dim txtNomAl As String como variable texto (string) que contiene el nombre del alumno.
(6) También aquí, más conveniente cuando mayor sea el tamaño de la matriz. Esta tabla permitirá a otro profesional que desee trabajar con el script una mejor comprensión de su funcionamiento, evitándole una búsqueda de referencias que a veces se vuelve muy complicada.

lunes, 26 de mayo de 2025

Documentos. Dictamen.

DocAp complejo. Dictamen de escolarización


Lo que voy a plantear en esta entrada ya ha tenido otras soluciones en formato [DocAp simple] e incluso como [DocAp complejo] basado en Base, Calc y Writer implicando tres documentos: Informe, Dictamen y Acreditación. Ahora busco desarrollar un DocAp complejo basado en dos servicios (Calc como gestor y base de datos) y Writer como soporte del documento-modelo.


Respecto al objeto de automatización, aunque no es el único documento prescriptivo basado en tablas, el dictamen de escolarización es posiblemente el que mejor representa esta tipología documental. Es además suficientemente extenso y complejo como para que su automatización suponga un ahorro real de tiempo de trabajo, a la vez que resulta un reto y un modelo para aplicar en la automatización de otros documentos similares.

El ahorro de tiempo es consecuencia de que su cumplimentación requiere un tiempo significativo de trabajo, más aun si tenemos en cuenta que es un documento de uso relativamente frecuente a lo largo del curso. Además, como documento prescriptivo que es, aunque se producen o pueden producir variaciones de curso a curso, éstas no suelen ser ni muchas ni importantes, por lo que es posible rentabilizar el esfuerzo que implica crear un DocAp.

En cuanto al documento en sí, cabe decir que en realidad se trata de tres documentos diferenciados que pueden ser tratados de forma independiente: 
  • un cuerpo principal, responsabilidad exclusiva del OE del SEO, y dos anexos. 
  • El primero de estos anexos es responsabilidad del ER y resulta prescriptivo cuando se cumplen determinadas condiciones que implican la participación obligada del ER; en caso contrario este anexo no forma parte del DE. 
  • El segundo anexo es obligatorio en cualquier caso y requiere la participación de la familia o de los representantes legales del menor y recibe un tratamiento diferenciado en cuanto a su gestión.
Esta estructura me permite abordar ahora únicamente la automatización del cuerpo principal del dictamen, dejando los dos anexos para otro momento.

El desarrollo de este DocAp requiere que nos posicionemos respecto al modo de empleo del documento, cuestión esta que presenta mayores dificultades de las que aparenta, pero que, entre ellas, nos permite también un planteamiento simplificador (no alejado de la realidad) en la que el dictamen se cumplimenta en su totalidad por parte del OE tras finalizar la elaboración del informe. Esto permite manejar el documento como un todo y como una única actuación, siendo esta la perspectiva que adopto a la hora de plantear la sucesión de procesos en el manejo del DocAp:
  1. El OE cumplimenta los datos a incluir en el documento usando un formulario como recurso de entrada de datos soportado sobre una hoja de cálculo (Calc)
  2. El script accede a estos datos y los traslada a la plantilla del dictamen-modelo (Writer) y posiciona la información mediante el uso de los marcadores previamente establecidos en ese soporte.
  3. El script genera un nuevo registro en la base de datos (construida sobre Calc)
Esta secuencia debería ser diferente en caso de idear un procedimiento distinto, siendo esto posible gracias a lo que implica el tratamiento modular del procedimiento, lo que ahora se concreta como DocAp complejo.

Documentos. Puedes descargar el [gestor-Calc] y el [documento-modelo] desde sus respectivos enlaces. debes modificar la ruta para que el script acceda correctamente al dictamen...
  • "C:/Users/alons/Desktop/Dictamen24Modelo.ott"
... y no te olvides de generar una plantilla a partir del documento-modelo dándole este nombre...
  • Dictamen24Modelo.ott
... o modificando también el nombre del documento en la ruta que crees.

Becas NEAE


Beca NEAE


DocAp simple sobre marcadores



En esta entrada presento un modelo de cumplimentación automatizada de informe para solicitud de Beca NEAE basado en Writer y OOo Basic. Es posible que esté un tanto obsoleto y que requiera ser modificado, pero te puede servir de base incluso en su actual formulación.

Este tipo de documento plantea diversas vías de automatización, incluyendo la combinación del procedimiento basado en la identificación de variables y el uso a los marcadores. Esta segunda opción se puede considerar la más apropiada si en el documento se suceden intercalados bloques de contenido basados en tablas y otros basados en párrafos, como es el caso del informe de solicitud de beca NEAE, documento cuya automatización es posible abordar de diferentes maneras, siendo una de ellas la que ejemplifica el DocAp al que puedes acceder desde este enlace [InfoBecaNEAE].

He calificado este DocAp como simple por usar únicamente el servicio Writer, pero no por ello deja de presentar cierta complejidad, causada en parte por al uso de un cuadro de diálogo como interfaz, pero sobre todo por el uso de macros modificadas a modo de script, procedimiento un tanto anacrónico, pero básico en el momento en que creé la versión original del DocAp (2023).

No tengo interés en modificar el código; es más, me parece interesante mantenerlo tal y como está para que se aprecie la evolución que ha tenido este blog desde su inicio a la actualidad (2025). Tampoco descarto realizar una versión actualizada, precisamente para facilitar esa misma comparación.

De momento, después de descargarlo y tras realizar una copia de seguridad (siempre recomendable) puedes acceder al Docap desde Herramientas | Macros | Ejecutar macro y buscar la macro/script Documento que está en el directorio Standard/ModEscritura de las macros asociadas al documento-base.

domingo, 25 de mayo de 2025

Documentos. Informe

Informe. Carátula sobre DocAp simple.


Con independencia de la complejidad del documento a automatizar, podemos optar por procedimientos diferentes, incluyendo el continuum categorial en función del nivel de complejidad de la propuesta técnica. En esta entrada vamos a trabajar con las carátulas del informe psicopedagógico, lo que supone tratar un caso de automatización de un documento complejo, aunque limitando su automatización a su componente tabular del documento (por lo que entra dentro de esta sección), lo que supone que sólo será automatizado parcialmente, y mediante un procedimiento relativamente simple (un DocAp simple, esto es, que no implica recurrir a más de un servicio LO; en este caso Writer)


Esta estrategia de automatización comparte la simplicidad, pero también la complejidad del procedimiento simple visto [en esta entrada], que la que podemos considerarla expansión. Por ello presenta la ventaja de simplicidad que permite que sea generalizable a documentos de similar o menor nivel de dificultad, especialmente si el uso del documento no va a ser frecuente o va a estar limitado temporalmente.

Se trabaja directamente sobre el documento-modelo que tras la creación del DocAp pasará a ser usado como plantilla y no requiere utilizar otro servicio más que Writer.

Aunque no se da por sentado, resulta conveniente que el usuario final (se espera que la misma persona que crea el DocAp) conozca y acceda al IDE para establecer en él aquellos valores de las variables que en realidad, para esa persona, van a funcionar como constantes. De hecho, aunque no se da en el DocAp que aquí sirve de modelo, es posible eliminar de esas variables  el uso de la función InputBox() y hacer una asignación directa de datos, evitando o saltando esta parte en el funcionamiento visible del script.

Otros datos que no corresponde sean tratados como constantes, sí pueden ser expresados mediante ejemplos. Esto es lo que aquí se hace consiguiendo ejemplificando qué se espera del usuario y, en algún caso, incluso ofreciendo una opción que sólo necesita aceptar como respuesta.

Este DocAp no pretende disimular que sólo es capaz de automatizar la escritura de datos en las tablas, dejando el resto del documento para se tratado "manualmente" por el usuario, pero ya sólo este limitado resultado supone un ahorro de trabajo: el que conlleva el desplazamiento "a mano" por los diferentes campos de las tablas y por las diferentes tablas del documento. No obstante, no es la única solución disponible y, sin salirse del ámbito procedimental de los DocAp, es posible generar respuestas de mayor rendimiento. Aun así, repito, el procedimiento que aquí se presenta puede ser suficiente en determinados casos. Si así lo considerar, sólo necesitas copiar el código del IDE, trasladarlo al módulo del nuevo documento al que lo quieras aplicar (tras crear los marcadores correspondientes, respetando el identificador que se usa en este documento, eso sí) y realizar las adaptaciones de contenido que sea pertinente.

Por eso te expongo y explico a continuación el código OOo Basic de este DocAp, el cual, como verás, consta de un script largo y una subrutina corta:

Sub Main

'Variables y matriz

Dim Conta As Integer
Dim sVar As String
Dim mDatos (33) As String
Dim AlNombre As String, AlApellidos As String
Dim iMAD_RepOrd As Integer, iMAD_PermanExt As Integer, iMAD_MedOrd As Integer, iMAD_MedExt As Integer,iMAD_Otras As Integer
Dim sInfoMotivacion As String

'Datos de identificación del informe

mDatos(0) = InputBox("SEO","DATOS DEL INFORME","EOE de Oviedo")
mDatos(1) = InputBox("Fecha de presentación del informe","DATOS DEL INFORME","12/03/2024")
mDatos(2) = InputBox("Orientador/a responsable del informe","DATOS DEL INFORME","Javier Alonso")

'Datos personales del alumno

AlNombre = InputBox("Nombre del alumno/a","DATOS DEL ALUMNADO","Jaime")
AlApellidos = InputBox("Apellidos de " & AlNombre,"DATOS DE" & AlNombre,"López Pérez")
mDatos(3) = InputBox("NIE de " & AlNombre,"DATOS DE " & AlNombre & " " & AlApellidos,"11122233")
mDatos(4) = AlNombre & " " & AlApellidos
mDatos(5) = InputBox("Fecha de nacimiento de " & AlNOmbre,"DATOS DE "  & AlNombre & " " & AlApellidos,"12/03/2018")
mDatos(6) = InputBox("Edad actual de " & AlNOmbre,"DATOS DE "&  AlNombre & " " & AlApellidos,"7:03 años")

'Datos de escolarización actual

mDatos(7) = InputBox("Centro actual de escolarización de " & AlNombre,"DATOS DE ESCOLARIZACIÓN DE "& AlNombre & " " & AlApellidos,"CP Auleriano Buendía")
mDatos(8) = InputBox("Curso actual de " & AlNombre,"DATOS DE ESCOLARIZACIÓN DE "& AlNombre & " " & AlApellidos,"2º")
mDatos(9) = InputBox("Etapa educativa","DATOS DE ESCOLARIZACIÓN DE "& AlNombre & " " & AlApellidos,"E. Primaria")

'Datos familiares

mDatos(10) = InputBox ("Nombre y apellidos de la madre de" & AlNombre,"Datos familiares de "& AlNombre & " " & AlApellidos,"Maria Soledad Pérez Ramírez")
mDatos(11) = InputBox ("DNI/NIF de la madre","Datos familiares de "& AlNombre & " " & AlApellidos,"10456765J")
mDatos(12) = InputBox ("Nombre y apellidos del padre de" & AlNombre,"Datos familiares de "& AlNombre & " " & AlApellidos,"Domingo López Aramendi")
mDatos(13) = InputBox ("DNI/NIF del padre","Datos familiares de "& AlNombre & " " & AlApellidos,"12345676M")
mDatos(14) = InputBox("Domicilio actual de "& AlNombre,"Datos familiares de "& AlNombre & " " & AlApellidos,"Adv de Portugal 23 - 3º D")
mDatos(15) = InputBox("Código postal del domicilio","Datos familiares de "& AlNombre & " " & AlApellidos,"33011")
mDatos(16) = InputBox("Localidad de residencia","Datos familiares de "& AlNombre & " " & AlApellidos,"Oviedo")
mDatos(17) = InputBox("Teléfono de contacto para notificación","Datos familiares de "& AlNombre & " " & AlApellidos,"654 765 567")

'Escolarización previa

mDatos(18) = InputBox("Centro anteriores (en caso de ser el actual mantener lo propuesto","Escolarización y medidas educativas previas de "& AlNombre & " " & AlApellidos, mDatos(7) & " desde los inicios de la escolarización.")

'MAD previas

iMAD_RepOrd = MsgBox("¿Ha realizado " &AlNombre & " repetición ordinaria de curso a lo largo de su escolarización?",4,"Medidas previas de respuesta educativa")
If iMAD_RepOrd = 6 Then
mDatos(19) = "Sí"
mDatos(20) = InputBox("¿En qué curso o curso realizó repetición ordinaria?","MAD previas")
Else
mDatos(19) = "No"
End If

iMAD_PermanExt = MsgBox("¿Ha realizado " &AlNombre & " repetición extraordinaria a lo largo de su escolarización?",4,"Medidas previas de respuesta educativa")
If iMAD_PermanExt = 6 Then
mDatos(21) = "Sí"
mDatos(22) = InputBox("¿En qué curso o curso realizó permanencia extraordinaria?","MAD previas")
Else
mDatos(21) = "No"
End If

iMAD_MedOrd = MsgBox("¿Se han aplicado " &AlNombre & " medidas ordinarias en los cursos anteriores (ajustes organizativos y/o adaptaciones no significativas)?",4,"Medidas previas de respuesta educativa")
If iMAD_MedOrd = 6 Then
mDatos(23) = "Sí"
mDatos(24) = InputBox ("¿Qué medidas ordinarias se aplicarón?","MAD previas","Ajuste organizativos,Ajustes curriculares de acceso y Adaptaciones metodológicas")
Else
mDatos(23) = "No"
End If

iMAD_MedExt = MsgBox("¿Y medidas extraordinarias (amplicación, flexibilización,exención o ACS)?",4,"Medidas previas de respuesta educativa")
If iMAD_MedExt = 6 Then
mDatos(25) = "Sí"
mDatos(26) = InputBox("¿Qué medidas extraordinarias se han aplicado? (en caso de ACS especificar área(s)","MAD previas","Amplicación, flexibilizazación, exención,ACS en el área de...")
Else
mDatos(25) = "No"
End If

iMAD_Otras = MsgBox("Ha tenido " &AlNombre & " medidas educativas específicas (refuerzo educativo ordinario, apoyos especializados...)?",4,"Medidas previas de respuesta educativa")
If iMAD_Otras = 6 Then
mDatos(27) = "Sí"
mDatos(28) = InputBox("Indica las medidas adoptadas. En caso de apoyos especializados indicar cuáles","MAD previas")
Else
mDatos(27) = "No"
End If

'Circunstancias que motivan el informe

sInfoMotivacion= InputBox("Selecciona el motivo por el que se emite este informe" & Chr(13) &_
"A - Por cambio de centro" & Chr(13) &_
"B - Por nueva escolarización"  & Chr(13) &_
"C - Por cambio de etapa"  & Chr(13) &_
"D - Para revisión o modificación","Motivación del Informe","A-B-C-D")
If sInfoMotivacion="A" Then
mDatos(29) = "X"
ElseIf sInfoMotivacion="B" Then
mDatos(30) = "X"
ElseIf sInfoMotivacion="C" Then
mDatos(31) = "X"
ElseIf sInfoMotivacion="D" Then
mDatos(32) = "X"
End If

mDatos(33) = InputBox("Puedes basarte en el modelo de respuesta que se propone o indicar las que consideres pertinentes.","OBSERVACIONES","Este informe responde a la demanda formulada con fecha 10/02/2024 por la Dirección del centro a petición del Tutor del alumno a fin de ajustar la respuesta educativa a sus actuales necesidades específicas de apoyo.")

'Escritura de contenidos

For Conta = 0 To 33:
sVar = mDatos(Conta)
Posicionamiento(sVar,Conta)
Next

End Sub
 
La mayor parte del script está ocupada por el tratamiento de los elementos de la matriz de datos mDatos(), que en la mayoría de los casos se resuelve con el uso repetido de la función InputBox(), aunque en otros se requieren procedimientos algo más complejos que implican el uso de condicionales. Esto se observa en el bloque MAD previas y Circunstancias...

Aunque ni por extensión ni por complejidad lo parezca, resulta de vital importancia comprender el interés que tiene el bucle con el que finaliza el script, (For Conta = 0 To 33:) que implica llamar a la subrutina que facilita la escritura de los datos en las posiciones que indican los marcadores.

Sub Posicionamiento (sDatos As String,i As Integer)

Dim oMarca As Object

oMarca = ThisComponent.getBookmarks().getByName("m"+CStr(i))
oMarca.getAnchor.setString(sDatos)

End Sub

Esta subrutina, que se puede construir de otra forma (por ejemplo incluyendo en ella el bucle con el que finaliza el script), recibe dos parámetros: el contenido del elemento de la matriz de datos y el valor de una variable contador. Con ellos ejecuta la acción de acceder al objeto marcador (Dim oMarca As Object) referenciado por el valor del contador (oMarca = ThisComponent.getBookmarks().getByName("m"+CStr(i))) y ubicar en esa posición el contenido de la variable (oMarca.getAnchor.setString(sDatos)) que, a su vez el bucle For que lo llama ha pasado desde el elemento de la matriz de datos (sVar = mDatos(Conta)).

Documento. Desde este enlace puedes descargar el DocAp. Te sugiero creer inmediatamente una versión plantilla con la que trabajar; al menos una copia de seguridad para evitar problemas con los marcadores, que ya sabes son muy sensibles y es fácil perderlos.