Mostrando entradas con la etiqueta Tratamiento de datos. Mostrar todas las entradas
Mostrando entradas con la etiqueta Tratamiento de datos. Mostrar todas las entradas

lunes, 4 de mayo de 2026

DATOS. Tratamiento 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. Tratamiento 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. Tratamiento 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. Tratamiento 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. Tratamiento 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.

DATOS. Tratamiento de datos

Tablas complejas (.odt) (I)

Tablas-formulario y su contexto

Lo visto y trabajado en las entradas anteriores responde (y lo hace correctamente) a las situaciones más simples y favorables; aquellas en las que, con independencia de la cantidad de datos que contenga/n la/s tabla/s del .docx, su estructura es asimilable a la de los datos estructurados y el reto para la automatización se limita a identificar la tabla como tal, recorrerla y obtener su contenido. En estas labores y condiciones los recursos empleados han permitido dearrollar con éxito la automatización de la obtención de datos y su conversión a formatos de datos estructurados (por ejemplo, como .xlsx). Pero si en vez de trabajar con ese tipo de tablas tenemos que hacerlo con tablas como la que se muestra abajo, la cosa cambia radicalmente.

Es este un extracto del modelo de informe psicopedagógico, sólo de una pequeña parte de dicho documento, pero mucho más que suficiente para comprobar lo alejados que estamos en este caso de la sencillez de las fuentes de datos con las que hemos trabajado antes. En realidad es dudoso que ahora podemos hablar de tablas en sentido estricto, ya que lo que aquí mostramos tiene más de formulario (sobre tabla) que de tabla en sentido estricto.

Lo cierto es que el abordaje de este tipo de tabla-formulario suele hacerse en sentido inverso al que aquí se plantea: el reto suele consistir en automatizar la cumplimentación de estas "tablas" con un conjunto ya estrucurturado de datos a fin de automatizar parte de la composición del documento; eso que podemos llamar automatizar la composición de la carátula del informe (ver en este enlace cuestiones relativas a temática). Pero ahora nos estamos planteando automatizar la obtención de los datos contenidos en documentos creados mediante servicios de procesamiento de texto (.docx, .odt, por ejemplo) de este nivel de complejidad formal y estructural (eso que hemos llamado antes tabla-formulario) como recurso para la obtención de datos, a su vez, como parte de un proyectos de análisis de datos.

Veamos brevemente algunas diferencias entre estas tablas-formulario y una tabla sencilla:

  • La disposición puede obedecer a la disposición fila-columna, pero la más frecuente obedece a la secuencia etiqueta->contenido
  • Esta secuencia (etiqueta->contenido) se presenta en sucesiones lineales variadas, una vez o varias veces por linea de texto; aunque en todas se observa la distribución vertical de la estructura (disposición de bloques etiqueta->contenido, por oposición a la horizontal de la tabla simple: etiqueta - etiqueta - etiqueta ... |contenido - contenido - contenido...
  • En una misma tabla (visualmente identificada como una unidad y localizable como tal desde el navegador del procesador de texto) se pueden mezclar diferentes presentaciones, además de fusiones de celdas y otras modificaciones respecto a la configuración dominante en la tabla.
  • Además de las estrucuras anteriores y sus contenidos básicos de interés, las tablas pueden tener otros contenidos, como encabezados, subencabezados (para diferenciar secciones), textos explicativos...
  • En ocasiones se pueden emplear controles de formulario, acentuando aun más la configuración de la tabla como formulario.
  • En ocasiones también, en celdas de amplia extensión (toda la línea e incluso resultante de la fusión de celdas) se puede ubicar dentro de la estructura tabular un volumen importante de información en forma de contenido textual (de una o varias frases a párrafos enteros); este contenido requiere, cuanto menos, un tratamiento posterior como bloque de texto
  • También se pueden anidar tablas (subformulario) dentro de la tabla principal.

Lo determinante de esta complejidad es la variedad de formulaciones y su combinación dentro del mismo documento y e incluso de la misma tabla. Ante tanta complejidad, script pensados para trabajar con tablas sencillas fracasan absolutamente, desmintiendo cualquier pretensión de universalidad. Esto sucede con independencia del tipo de documento que sea, .dpf, .docx o .odt. Ante esta complejidad, ¿cuál puede ser la alternativa?.

En principio podríamos pensar en la IA como alternativa; pero ni podemos asegurar el exito ni debemos darlo por supuesto; lo que rompe radicalmente la confiabilidad del recurso y obliga a la revisión sistemática y exhaustiva del resultado, con el coste de tiempo que esto supone. Pero la principal limitación de la IA es la ausencia de confidencialidad que implica, dado que no es viable la anonimación del contenido de las tablas, puesto que es precisamente la recuperación de sus datos (confidenciales) el motivo de la automatización del proceso. Existen alternativas que prometen respetar el anonimato de los datos y garantizar su correcto procesamiento, pero estas soluciones implican cargos económicos que no suelen estar al alcance de los SEO, y que no se justifican por el objetivo último que da sentido al tratamiento de esta documentación.

En resumen, estamos ante un posible callejón sin salida, aunque también cabe verlo como un proceso en el que ciertas tecnologías solamente pueden aportar soluciones parciales y relativamente satisfactorias; siendo necesaria la intervención "manual", la cual, a su vez, también puede ser auxiliada parcialmente mediante la tecnología. A falta de ¿mejores? opciones, menos es nada, pero no deja de ser "curioso" que el proceso inverso (cumplimentar documentos tabulares mediante asociación a bases de datos) haya sido tan temprana y eficientemente resuelto, y su reverso ofrezca tantas resistencia a la automatización. Pero sea curioso o no lo sea tanto, lo cierto es que es lo que hay.

En ausencia de procedimientos sencillos y universales de automatización, vamos a plantear en esta entrada y en las siguientes cómo abordar el tratamiento de estas tablas complejas, empezando por contextualizarlas en escenarios posibles, ya que no deja de resultar hasta cierto punto una anomalía que nodebería ser necesario abordar: lo lógico sería disponder de la base de datos que permitió "escribir" el contenido de esas tablas, pero la realidad es la contraria, muy frecuentemente y por muchas razones, empezando por la escasa práctica de automatizar la escritura de las tablas de este tipo de de domentos, siendo que sus "formularios" se rellenan tan manualmente como se componen los textos explicativos que pueden contener. Otra buena razón es que la procedencia o la antiguedad del documento haga inviable acceder a una supuesta fuente de datos que, casi con toda seguridad, no existe como conjunto estructurado. Para no tener que justificar lo que es difícilmente justificable, es en este segundo escenario donde nos vamos a situar, concretamente en el manejo de documentación propia de cierta antiguedad con fines de análisis de prácticas y de datos digitalizados en soporte documental.

El primer paso será obtener los documentos (tema de cierta complejidad que no corresponde abordar aquí, por lo que lo damos simplemente por resuelto) y el segundo unificar la tipología del soporte, optando en este caso por uniformar todos los documentos como .odt, dado que la arquiteftura interna de este soporte facilita el acceso a su contendio mediante código Python (pero tampoco es en esta entrada donde se tratan estas cuestiones técnicas, por lo que también las damos simplemente por resueltas). Partimos pues de que diponemos de un conjunto suficientemente amplio de documentos que contienen información de interés en una estructura compleja de tablas-formulario. Esta documentación ha sido transformada al formato .odt, lo que facilita su tratamiento mediante un conjunto de bibliotecas Python específicamente desarrolladas para procesar estos documentos. Lo que nos plantearemos a continuación, en las entradas que siguen, es cómo resolver distinto tipo de problemas o retos; desde el acceso al documento y la identificación de las tablas hasta la extracción masiva de datos o la obtención de todos los datos de un documento individual.Todo ello poco a poco y en las entradas que vienen.

miércoles, 22 de abril de 2026

DATOS. Tratsamiento de datos

Tablas Word (.docx) (I)

Acceso a tablas simples

El trabajo con textos .docx ya ha sido tratado en otras entradas de este blog, pero desde la prespectiva de la composición de textos. Ahora nos interesa el proceso inverso: la obtención de datos. Empezaremos por la identificación de tablas simples (estructura tabular básica) y la obtención de los datos que contienen.

El documento que nos sirve de modelo no tiene más que ese interés. Obviamente los datos son inventados y carecen de relevancia para cualquier otro objetivo. Se trata de un texto en soporte .docx, que contiene unos breves párrafos de texto (datos no estructurados) y tres tablas de estructura simple (asimilables al formato BD: fila (registro) - columna (campo/variable), que se comportan inicialmente como datos semi-estructurados, aunque sólo en función del soporte (de presentarse en un formato estructurado en campos podrían considerarse datos estructurados).

El primer objetivo que nos proponemos ahora es automatizar identificación de las tablas (obviando el texto) y la extracción de los datos que contienen. Para ello utilizamos el siguiente script Python:



# 0. Importamos las bibliotecas necesarias

import pandas as pd
from docx import Document
import warnings                     # Importamos el módulo de avisos

# 1. Instrucción para ignorar todos los avisos técnicos de las librerías

warnings.filterwarnings("ignore")

# 2. Función para identificación de los tipos de datos para posterior tratamiento

def identificar_tipos_datos(df):    # Se asume la posibilidad de distintos tipos de datos (fechas y numéricos)
    for col in df.columns:
        intentar_fecha = pd.to_datetime(df[col], errors='coerce')   # Fechas
        if intentar_fecha.notna().all():
            df[col] = intentar_fecha
            continue
        intentar_num = pd.to_numeric(df[col], errors='coerce')      # Numéricos
        if intentar_num.notna().sum() > len(df) * 0.8: 
            df[col] = intentar_num
    return df

# 3. Función para la extracción de datos de las tablas-docx

def extractor_universal(ruta_docx):
    try:
        doc = Document(ruta_docx)
        for i, tabla in enumerate(doc.tables):
            datos = []
            for fila in tabla.rows:
                datos.append([celda.text.strip() for celda in fila.cells])

            if not datos: continue

            cabecera = datos[0]
            cuerpo = datos[1:]
            df = pd.DataFrame(cuerpo, columns=cabecera)
            df_estructurado = identificar_tipos_datos(df)	 # Llamada a la función 2

            print(f"\n--- TABLA {i+1} ---")                  # Visualización de tablas en el CMD
            print(df_estructurado.to_string(index=False)) 
            print("-" * 40)

    except Exception as e:
        print(f"Error en el proceso: {e}")

# 4. Ejecución de la función principal

extractor_universal("tablas.docx")


Las pretensiones de este script son muchas, a pesar de lo limitado de su objetivo: se trata de:

  • Identificar las tablas presentes en el documento. Al tratarse de un documento docx se hace uso de la biblioteca python-docx
  • Se pretende crear un script de caracter universal, de ahí que se esperen distintos tipos de datos, los cuales se desean identificar para un posterior tratamiento que no se da en este caso. Esto explica la presencia de la función def identificar_tipos_datos(df) que se relaciona con el uso de la bibliteca import pandas as pd
  • Esto hace posible que se generen avisos no deseados y que carecen de utilidad inmediata, por eso se importa el módulo de control de avisos import warnings y se inhibe su aparición en el CMD con warnings.filterwarnings("ignore"), procedimiento que en otras ocasiones puede ser contraproducente, pero no en esta, ya que en realidad conocemos el contenido de las tablas
  • El núcleo central del script se plantea dentro del bucle for i, tabla in enumerate(doc.tables):, que es donde se obtiene el recuento de las tablas del documento, se recorren éstas, se extrae su contenido y se procesa, incluyendo su conversión de texto a diferentes tipos de datos. Además, finalmente, se muestran por pantalla.

    Dado que con esto finaliza el script, se pierden en realidad posibilidades de tratamiento que sí están disponibles en función de la conversión de datos y de la participación de Pandas; pero ahora lo que nos interesaba era la obtención de los datos, sólo los datos de las tablas, y este objetivo está conseguido. En posteriores entradas profundizaremos en el desarrollo de este proceso.

viernes, 17 de abril de 2026

DATOS. Tratamiento de datos

Datos semi-estructurados PDF (II)

De tabla-pdf a txt y a csv

Dado el objetivo de extraer los datos que contienen tablas de archivos .pdf (datos semi-estrucurados), lo normal es aplicar procedimientos que, en teoría, se ajustan al objetivo y no recurrir a la conversión de tabla-pdf a imagen para usar después un OCR. Por eso califiqué antes esta opción como "ultimo recurso", y por eso en esta entrada probaremos a obtener esos datos por otros medios, concretamente mediante la biblioteca PyMuPDF, de la cual ya hablamos en este blog [pendiente enlace a entrada].

Cierto que disponemos de varias opciones para la salida de los datos, pero por claridad expositiva voy a mostrar en primer lugar la que devuelve la mera visualización por pantalla (consola) de los datos que obtenemos con el script. De esta forma podremos observar mejor la lógica del código y las características de lo que nos devuelve.



'''
Este script utiliza la biblioteca PyMuPDF para acceder a un documento pdf, localizar las tablas
que contiene, extraerlo y mostrarlo por pantalla (CMD>
Este script ha sido desarrollado usando IA-Gemini como asistente de programación
'''

#0. Importar biblioteca

import fitz  # PyMuPDF

#Función para acceso y lectura de tablas del pdf------------------------------------

def extraer_tablas_pdf(ruta_pdf):
    
# 1. Abrir el documento pdf

    doc = fitz.open(ruta_pdf)
    
    print(f"Analizando documento: {ruta_pdf}\n")

# 2. Recorrer las páginas del documento...
    for num_pagina, pagina in enumerate(doc):

# ... buscando las tablas en cada página
        tabs = pagina.find_tables()

# Si se encuentra tabla en la página...        
        if tabs:
            print(f"--- Página {num_pagina + 1} ({len(tabs.tables)} tablas encontradas) ---")
#... se recorrer la tabla y...
            for i, tabla in enumerate(tabs.tables):
                
# 3. Se extrae el contenido de la tabla como una lista de listas
                datos = tabla.extract()            
                print(f"\nTabla {i + 1}:")
                for fila in datos:
# Limpiamos saltos de línea internos para facilitar la lectura por pantalla
                    fila_limpia = [str(celda).replace('\n', ' ') if celda else "" for celda in fila]
                    print(fila_limpia)
        else:
# Muestra las páginas que no contienen tablas
                print(f"Página {num_pagina + 1}: No se detectaron tablas.")
        pass
    doc.close()
# Fin de la función-------------------------------------------------------------------------------

# Script de llamada a la función----------------------------------------------------------------

# 4. Configuración de rutas
ruta = "TuArchivo.pdf" # Identificamos el archivo pdf

# 5. Llamada a la función pasando el parámetro (ruta del pdf)

extraer_tablas_pdf(ruta)


Te dejo que leas detenidamente este código para que lo interpretes con ayuda de los comentarios que contiene; también que lo pruebes sobre algun .pdf que contenga tablas, requisito este indispensable. Un uso más centrado en lo funcional que en lo expositivo eliminaría órdenes como print(f"Página {num_pagina + 1}: No se detectaron tablas."), o simplemente las comentaría, que es lo que te yo sugiero; pero aquí cumplen su función, por lo que son necesarias

Y ahora vamos a comprobar qué obtenemos mostrando, a modo de ejemplo, el contenido de la identificada como tabla 2

Al haber transformado el contenido en una lista de listas (como vemos en 3), podemos visualizar cada elemento textiual (indiferenciado entre etiqueta y campo) cada una de las sublistas (líneas de texto) que contiene la gran lista que es el conjunto de los datos obtenidos. Es un avance respecto al mero texto plano, pero no es aun lo deseado, ya que aquí es tratada como igual la etiqueta y el dato.

Vamos a probar con otra salida, la conversión del resultado de la extracción en un archivo .txt, lo que equipara el resultado al obtenido mediente el procedimiento OCR aplicado antes. Además de estudiar el resultado por comparación con el anterior y el referenciado, también podrás comprobar los cambios que conlleva esta propuesta respecto a la mera visualización por pantalla.


'''
Este script utiliza la biblioteca PyMuPDF para acceder a un documento pdf, localizar las tablas
que contiene y extraer sus datos.
Posteriormente se archiva el resultado como .txt
Este script ha sido desarrollado usando IA-Gemini como asistente de programación
'''
#0. Cargar la biblioteca necesaria

import fitz  # PyMuPDF

#Función de acceso a datos y creación de txt------------------------------------------------

def tablas_pdf_a_txt(ruta_pdf, ruta_txt):
# 1. Abrir el documento PDF
    doc = fitz.open(ruta_pdf)
# 2. Creación del archivo txt de salida
    with open(ruta_txt, "w", encoding="utf-8") as archivo_salida:
        archivo_salida.write(f"EXTRACCIÓN DE TABLAS - {ruta_pdf}\n")
        archivo_salida.write("=" * 50 + "\n\n")

        for num_pagina, pagina in enumerate(doc):
# 3. Buscar tablas en la página del pdf
            tabs = pagina.find_tables()
            
            if tabs.tables:
                archivo_salida.write(f"--- PÁGINA {num_pagina + 1} ---\n")
                
                for i, tabla in enumerate(tabs.tables):
                    archivo_salida.write(f"\n[Tabla {i + 1}]\n")
                    
# 4.Extraer los datos de la tabla
                    datos = tabla.extract()
                    
                    for fila in datos:
# Limpiar celdas: quitar saltos de línea y convertir None en vacío
                        fila_limpia = [str(celda).replace('\n', ' ').strip() if celda else "" for celda in fila]                      
# Unir la fila con tabuladores para mantener formato de columnas
                        linea = "\t".join(fila_limpia)
                        archivo_salida.write(linea + "\n")
                    
                    archivo_salida.write("-" * 30 + "\n")
                
                archivo_salida.write("\n")
        
        print(f"Proceso finalizado. Las tablas se han guardado en: {ruta_txt}")

    doc.close()

# Fin de la función-------------------------------------------------------------------------------------------
#Procedimiento de llamada a la función------------------------------------------------------------------------
# 5. Configuración de rutas
pdf_entrada = "mi_archivo.pdf"          # Cambia por tu archivo pdf (sólo el nombre y la extensión)
txt_salida = "tablas_extraidas.txt"     # Cambia por tu nombre de archivo.txt de salida

# 6. Llamada a la función
tablas_pdf_a_txt(pdf_entrada, txt_salida)   


También aquí implementamos alguna información funcionalmente innecesaría (pero que nos sirven para mejorar la comprensión del resultado), lo que distorsiona el logro del objetivo e incrementa el trabajo posterior de adaptación del .txt resultante, pero lo interesante es comprobar que la salida actual ...

... se parece más a la que obtuvimos mediante ocr, de modo que en este caso podemos optar indistintamente por cualquiera de los dos procedimientos, aunque lo esperable es optar por PyMuPDF por resultar menos costoso computacionalmente...

... y menos a la que el mismo script nos ofrece en el CMD y que pudimos ver antes. Las tres, además, se asemejan más a la mera visualización de la tabla (que te muestro debajo) que a lo que implica de estructura de datos, incluyendo la pérdida de referencia al contenido del último de sus cinco (que no cuatro) campos:

Para finalizar esta entrada vamos a modificar la salida anterior y converirla en .csv. Comprobarás que el script es muy parecido al anterior, pero que el resultado es muy diferente. Empecemos por mostrar el script.



'''
Este script utiliza la biblioteca PyMuPDF para acceder a un documento pdf, localizar las tablas
que contiene y extraer sus datos.
Posteriormente se archiva el resultado como .csv, accesible desde un servicio Hoja de cálculo (Excel o Calc)
Este script ha sido desarrollado usando IA-Gemini como asistente de programación
'''
#0. Importar las bibliotecas necesarias

import fitz  # PyMuPDF
import csv

#Función para extraer las tablas y su contenido-----------------------------------

def tablas_a_csv(pdf, archivo_csv_salida):

# 1. Abrir el documento PDF e iniciar variables
    doc = fitz.open(pdf)
    total_paginas = len(doc)
    tablas_encontradas = 0

# 2. Generar el archivo csv de salida
    with open(archivo_csv_salida, mode="w", newline="", encoding="utf-8-sig") as fichero:
        escritor_csv = csv.writer(fichero, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL)    
        
# 3. Recorrer las páginas
        for num_pagina in range(total_paginas):
            pagina = doc[num_pagina]
            tabs = pagina.find_tables() # Buscamos tablas en la página actual
# Si se encuentran tablas en la página actual...
            if tabs.tables:
#... se recorre la tabla y...
                for i, tabla in enumerate(tabs.tables):
                    
# 4. Extraemos el contenido de la tabla
                    datos = tabla.extract()
                    if datos:
                        tablas_encontradas += 1
# Escribimos el encabezado de identificación para la tabla
                        escritor_csv.writerow([f"--- TABLA {tablas_encontradas} (Página {num_pagina + 1}) ---"])  
                        for fila in datos:
# Limpiamos cada celda: quitar saltos de línea y espacios en blanco
                            fila_limpia = [str(celda).replace('\n', ' ').strip() if celda is not None else "" for celda in fila]
                            escritor_csv.writerow(fila_limpia)    
# Añadimos una línea en blanco para separar visualmente las tablas en el CSV
                        escritor_csv.writerow([])
# Generamos mensajes de finalización
    if tablas_encontradas > 0:
        print(f"Éxito: Se han extraído {tablas_encontradas} tablas en '{archivo_csv_salida}'.")
    else:
        print("No se detectaron tablas en ninguna página del documento.")

    doc.close()

#Fin de la función ---------------------------------------------------------------------------------
    
#  Procedimiento de llamada a función-----------------------------------------------------------

# 5. Identificación de rutas
pdf_entrada = "Mi_Archivo.pdf"         # Identificamos el archivo pdf de entrada [Cambia esto por tu archivo pdf (nombre + extensión entre comillas)]
csv_salida = "tablas_extraidas.csv"    # Identificamos el txt de salidad [Pon nombre de tu archivo txt de salida]

# 6. Llamada a la función
tablas_a_csv(pdf_entrada, csv_salida)


En cuanto al script, existen algunas diferencias con el anterior script, empezando por import csv y siguendo por la estructura que genera el archivo .csv (ver código asociado al comentario 2), pero el resto es muy parecido. Lo que no lo es tanto es el resultado.

Al tratarse de un .csv podemos visualizarlo desde Bloc de notas, pudiendo apreciar la similitud que mantiene con la visualización de los datos en pantalla (lista de listas)...

... con el consecuente procedimiento de ajuste y limpieza posterior que implica y que nos devuelve al conocido procedimiento ofimático; pero también podemos acceder a él desde un servicio de Hoja de cálculo (vg. Calc, donde observamos un resultado similar a este...

... que si bien no permite un tratamiento directo como tabla de datos (en el sentido de que no podemos considerarlos aun como datos estructurados), sí nos permite un tratamiento automatizado, cuanto menos para la definitiva conversión o incorporación en una base de datos. En esto proceso de automatización no podemos utilizar directamente una solución como esta, pero sí una similar, aunque para ellos es posible que sea necesario convertir previamente el .csv en .ods (para aplicar un script OOo Basic) o .xlsx (para aplicar un script Python basado en la biblioteca OpenPyXL.

jueves, 16 de abril de 2026

DATOS. Tratamiento de datos

Datos semi-estructurados

Estrategia básica Macro-Docap

Dejamos el procedimiento ofimático de converisón de las tablas pdf, pero sobre todo doc/docx/odt expresado como tabla-registro en nuestra hoja de cálculo, pendiente de su última transformación: de vertical a horizontal, a fin de presentarse como una tabla de doble entrada en la que cada columna es un campo o variable y cada fila una colección de datos o registro. Este último paso es más que una formalidad, ya que la recuperación y el tratamiento posterior de los datos y la propia creación de la base de datos depende de ello.

Para realizar este cambio Calc dispone de la función Trasponer, el cual IA-Gemini explica del siguiente modo:

  • Primero se selecciona el rango de datos que se quiere transponer
  • Clic derecho + opción Copiar
  • Posicionarse en la celda de destino que ocupará el primer campo de la base de datos
  • Hacer clic derecho y elegir Pegado especial
  • Seleccionar la opción emergente Transponer y hacer clic izquierdo en esa opción

Una vez resuelto el problema de la direccionalidad de este primer registro (y creada de paso la estructura de campos de la base de datos, la adición de nuevos registros nos obliga a procesar las celdas de la configuración vertical de un modo ligeramente diferente:

  • Generamos el nuevo registro (vertical) completo (etiqueta+dato) en la hoja 1 (igual que hicimos antes
  • Pero sólo seleccionamos de esta estrucura la columna de datos, siguiendo el procedimiento anterior
  • En la hoja 2 (base de datos) nos posicionamos en la celda que ocupará el dato del primer campo de este nuevo registro y ejecutamos la misma secuencia de acciones que cuando creamos la base de datos
  • Deberemos respetar escrupulosamente este procedimiento para evitar sustituir un registro antiguo por otro nuevo

Esta es la solución ofimática o manual, pero una vez que nuestros datos básicos ya están en una hoja de cálculo no resulta especialmente complicado automatizar la creación y la actualización de la base de datos (disposición tabular horizontal) mediante una macro y/o un script OOo Basic o la combinación de ambos. Obsérvese que lo que estoy planteando ahora supone un cambio importante respecto al procedimiento manual-ofimático anterior: ahora hablamos de automatizar el procedimiento, no de aplicar sistemática y repetidamente una secuencia de acciones.

Esta alternativa no se diferencia mucho de la que hemos aplicado a la creación de DocAp complejos de evaluación, basados en el desarrollo de un gestor sobre Calc que, entre otras funciones tiene la de recoger sistemáticamente los datos de la aplicación a modo de tabla de datos vertical y, postriormente, trasladarlos a una base de datos acumulativa diseñada en horizontal. Todo ello se basa en un conjunto de script que aquí deberán ser diferentes para cada manipulación de tablas-Word que deseemos realizar, pero como todos ellos se basan en el mismo procedimiento, no será complicado generar un código-base, el cual se ajustará lo necesario para adaptarlo a la nueva base de datos.

La automatización puede dividirse en dos fases y empezar con la selección y copia del contenido del documento .odt, pero lo fundamental se desarrolla asociado a la hoja de cálculo y se basa en un mecanismo básico de captura de datos y escritura de contenido, ambos en base a matriz y bucle. Dado que aquí no estamos desarrollando un procedimiento concretoy completo, me limitaré a construir el código básico de la automatización, el cual responde en esencia a lo que acabo de describir.

Este código está dividido en dos script que aquí creé mediante Grabar macro, en parte para ilustrar la forma más básica de automatizar el procedimiento, por decirlo de alguna manera, el procedimiento más "ofimático". El primer script genera la cabecera de la base de datos (listado de etiquetas de los campos)...



sub CrearEncabezados
rem ----------------------------------------------------------------------
rem definir variables
dim document   as object
dim dispatcher as object
rem ----------------------------------------------------------------------
rem Acceder al dispatcher
document   = ThisComponent.CurrentController.Frame
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
rem Acceder a la hoja 1
dim args1(0) as new com.sun.star.beans.PropertyValue
	args1(0).Name = "Nr"
	args1(0).Value = 1
dispatcher.executeDispatch(document, ".uno:JumpToTable", "", 0, args1())

rem Seleccionar las celdas a copiar------------------------------------------
dim args2(0) as new com.sun.star.beans.PropertyValue
	args2(0).Name = "ToPoint"
	args2(0).Value = "$A$1:$A$20"
dispatcher.executeDispatch(document, ".uno:GoToCell", "", 0, args2())

rem Copiar el contendio de esas celdas----------------------------------------
dispatcher.executeDispatch(document, ".uno:Copy", "", 0, Array())

rem Posicionarse en la hoja 2----------------------------------------------------
dim args3(0) as new com.sun.star.beans.PropertyValue
	args3(0).Name = "Nr"
	args3(0).Value = 2
dispatcher.executeDispatch(document, ".uno:JumpToTable", "", 0, args3())

rem Posicionarse en la celda A1-----------------------------------------------------
dim args4(0) as new com.sun.star.beans.PropertyValue
	args4(0).Name = "ToPoint"
	args4(0).Value = "$A$1"
dispatcher.executeDispatch(document, ".uno:GoToCell", "", 0, args4())

rem Copiar (transponiendo) el contenido copiado ---------------------------------
dispatcher.executeDispatch(document, ".uno:PasteTransposed", "", 0, Array())

end sub


... y el segundo copia el contenido del registro.



sub CrearRegistro
rem ----------------------------------------------------------------------
rem definir variables
dim document   as object
dim dispatcher as object
rem ----------------------------------------------------------------------
rem Acceder al dispatcher
document   = ThisComponent.CurrentController.Frame
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")

rem Acceder a la hoja 1
dim args1(0) as new com.sun.star.beans.PropertyValue
	args1(0).Name = "Nr"
	args1(0).Value = 1
dispatcher.executeDispatch(document, ".uno:JumpToTable", "", 0, args1())

rem Posicionados en la hoja 1, seleccionar las celdas B1:B20-----------------
dim args2(0) as new com.sun.star.beans.PropertyValue
	args2(0).Name = "ToPoint"
	args2(0).Value = "$B$1:$B$20"
dispatcher.executeDispatch(document, ".uno:GoToCell", "", 0, args2())

rem Copiar el contendio de esas celdas----------------------------------------
dispatcher.executeDispatch(document, ".uno:Copy", "", 0, Array())

rem Posicionarse en la hoja 2----------------------------------------------------
dim args3(0) as new com.sun.star.beans.PropertyValue
	args3(0).Name = "Nr"
	args3(0).Value = 2
dispatcher.executeDispatch(document, ".uno:JumpToTable", "", 0, args3())

rem Posicionarse en la celda A2-----------------------------------------------------
dim args4(0) as new com.sun.star.beans.PropertyValue
	args4(0).Name = "ToPoint"	
	args4(0).Value = "$A$2"
dispatcher.executeDispatch(document, ".uno:GoToCell", "", 0, args4())

rem Copiar (transponiendo) el contenido copiado---------------------------------
dispatcher.executeDispatch(document, ".uno:PasteTransposed", "", 0, Array())

end sub


Como puedes ver, ambos script son prácticamente el mismo, y de hecho podríamos trabajar sólo con uno, realizando las mínimas modificaciones de posicionamiento, pero me ha parecido preferible diferenciar la creación de la base de datos (en realidad, la escritura de las etiquetas de los campos) de la copia del contenido del registro. Cierto que así el primer script sólo se modifica cuando se crea el proyecto concreto (el intervalo de celdas en la Hoja1: args2(0).Value = "$A$1:$A$20"), mientras que el segundo, además del asjuste del intervalo de celdas (args2(0).Value = "$B$1:$B$20") al inicio del proyecto, es necesario modificar también la identificación de la posición de inicio de copia transpuesta en Hoja2 (args4(0).Value = "$A$2") cada vez que creamos un nuevo registro en la base de datos.

La solución anterior no está completa, ya que se puede automatizar más y mejor, pero es suficiente para ilustrar lo que intento decir, así que para este objetivo es suficiente.

Como ves, los datos semi-estructurados, una vez que es posible manejarlos desde una hoja de cálculo, también es posible automarizar su conversión en datos estructurados, al menos en términos de forma. Otra cosa es qué se puede necesitar para realizar esta automatización (en esta entrada hemos visto la forma más simple de hacerlo) y qué limpiezas posteriores requieran ya formalmente como datos estructurados. Pero estas son otras cuestiones.

miércoles, 15 de abril de 2026

DATOS. Tratamiento de datos

Datos semi-estructurados

Tratamiento ofimático

Antes de contar con recursos para manejar los datos que hemos llamado semi-estructurados y entre los que figuran aquellos que proceden de tablas insertadas en documentos de texto, todo el proceso se realizaba "manualmente", aunque sería más apropiado decir que mediante procedimientos ofimáticos. Dado que lo que se pretende en esta sub-sección de DATOS Y TEXTOS es automatizar el procedimiento (en la medida de lo posible) es muy conveniente recordar los orígenes, ya que nos van a servir como punto de referencia.

Empecemos por decir que, ya sea en documentos generados con un procesador de texto o en otro en formato .pdf, las primeras tablas con las que hemos trabajado han estado originalmente en este formato, cumpliendo funciones de formulario o como recurso para estructurar la presentación de la información. Esto es importante decirlo, ya que pudiera parecer que en el principio estuvieron las hojas de cálculo, cuando en realidad han sido un recurso incorporado tarde, mál y pobremente, y no pocas veces de forma un tanto obligada o forzada por demandas externas de rendición de cuentas (estadillos pedidos de NEE y documentos similares solicitados por la autoridad, sea esta quien fuera, los cuales no siempre se presentaban en una hoja de cálculo).

Por diversas causas, a veces esos datos contenidos en tablas-Word han terminado formando parte de una tabla excel, proceso que hace tiempo fue ocasional y que actualmente es mucho más frecuente, pero que en cuanto a procedimiento aun sigue sin ser resuelto de forma sistemática, y eso que, salvo que se empleen procedimientos específicos, ofimáticos o basados en script, lo cierto es que, aunque no presenta mayor dificultad, sí consume demasiado tiempo.

Veamos en qué puede consistir el procedimiento manual-ofimático, partiendo de disponer ya de una copia en texto del contenido de las tablas-Word, resultado de aplicar las funcionalidades Seleccionar->Copiar. Aunque caben varias opciones, para evitar que se copien elementos gráficos no deseados, propongo pegar el contenido en un documento .txt (utilidad Bloc de notas, por ejemplo) como segundo paso.

Lo normal es que lo que se copie reproduzca grosso modo la distribución original del texto, lo que no es aceptable en términos de transformación de los datos en datos estructurados. Por eso motivo propongo actuar manualmente sobre el documento .txt para transformar el contenido en una sucesión de líneas de texto etiqueta-dato, lo que supone eliminar muchos espacios en blanco, diferenciar bloques etiqueta-dato que pueden aparentar ser una unidad, separarlos por líneas y hasta transformar algunos contenidos (por ejemplo las "X" que simulan la elección de una (aparente) opción.

Debo decir que si la tabla cuenta con elementos de formulario es posible que esta estrategia no funcione bien, pero de momento vamos a apartar este tipo de situaciones para no complicar innecesariamente la explicación. Por ahora, los controles de formulario nos obligan a reconstruir el contenido manualmente, así que cuantos menos controles de formulario tenga nuestra tabla mejor para nosotros.

También vamos a eludir ahora la necesidad de valorar algunas opciones de estructuración de los datos cuando se plantean varios como opcionales, tomando provisionalmente partido por la opción más simple: mantener sólo la opción elegida y eliminar el resto.

Una vez asumidas estas opciones y organizados los datos según los criterios antes indicados, dispondermos de un documento .txt simple. De uno o de varios, dependiendo de si optamos por tratar toda la información como un único bloque de datos, o diferenciar cada una de las tablas que originalmente presentaba el documento. Y digo esto porque, de optar por la segunda posibilidad considero preferible diferenciar cada bloque-tabla en un archivo .txt diferente y seguir el procedimiento de transformación también de forma diferenciada. Así mantenemos la unidad de procedimiento y eliminamos espacios en blanco inncesarios o diferenciadores de tablas que a la larga terminan creando confusión en el acceso a los datos.

Partiendo en este caso de la unidad de conjunto de los datos, lo que equivale a considerarlos como una tabla única, una vez limpio el documento .txt (el cual guardaremos al menos provisionalmente hasta disponer de la base de datos/tabla Excel), ejecutamos Seleccionar todo-Copiar, creamos un documento word (Writer) y pegamos el contenido copiado antes en el portapapeles.

Fíjate que usamos el procesador de texto y no directamente la hoja de cálculo para evitar posibles errores en la gestión de la información (que se copie todo el texto en una celda o la pareja etiqueta-dato en una misma celda. Si trasladamos los datos al procesador de texto, podremos realizar una manipulación previa al traslado a la hoja de cálculo que evitará esos problemas de gestión. No obstante, es posible que la copia directa del contenido del .txt en Excel-Calc se resuelva satisfactoriamente y se genere automáticamente un listado vertical en el que se diferencian en sendas celdas la etiqueta y el dato.

Si no ese el caso, o si no quieres correr riesgos, lo que deberás hacer es separar TODAS las parejas etiqueta-dato con un caracter, siempre con el mismo (etiqueta:dato, etiqueta,dato...). Con esto suele ser sificiente para que la utilidad de hoja de cálculo interprete que se trata de una estructura campo-contenido (por lo que incluso te puedes ahorrar el uso del procesador de texto), pero de nuevo si quieres asegurar el existo, lo que debes hacer es seleccionar todo el texto y aplicar la utilidad Writer/Word Tablas|Convetir|Texto en tablas, y seleccionar como separador el caracter empleado en la diferenciación etiqueta-dato (por similitud con el formato .csv te sugiero emplear la coma (,) como separador.

Repito, aunque es posible que otras opciones funciones igualmente, este es el procedimiento que te garantiza que todo salga bien. Con él crearás una tabla-registro donde los campos están dispuestos verticalmente, lo que facilita la lectura del registro pero no la creación de una base de datos en la que acumular los diferentes registros que vayamos creando. Este tipo de base de datos se organiza en horizontal, siendo cada columna un campo y cada fila in conjunto de datos o registro. Transformar una en otra es tema para un posterior desarrollo del procedimiento.

martes, 14 de abril de 2026

DATOS. Tratamiento de datos

¿Qué datos?

Datos estructurados, semi-estructurados y no estructurados

Como resultado de la aplicación de las diferentes tecnologías que hemos visto en las entradas del subapartado anterior, disponemos ahora, on estamos en proceso de disponer, de un conjunto de datos que requieren, posiblemente, algún tipo de tratamiento, incluyendo su ajuste a forma y la eliminación de algún tipo de falla. A esto nos vamos a dedicar en este subapartado, pero antes deberemos aclarar una cuestión que incide muy mucho en este conjunto de tarea, hasta el punto de determinar cómo se pueden desarrollar. Me refiero al tipo de datos sobre el que tendremos que trabajar.

En función del tipo de datos y de cómo se presentan podemos distinguir entre datos estructurados y datos no estructurados. Los primeros son aquellos que, textuales, numéricos o lógicos (esto ahora es secundario), se presentan (o se pueden presentar) organizados (de ahí lo de "estructurados") en forma de tablas (para resumir) de doble entrada, en las que las columnas se deben entender como campos (y tipos de datos) y las filas como registros, lo que en nuestro campo (SEO) suele significar alumno. Los no estructurados más que datos los vamos a entender no como datos sino como segmentos textuales que contienen información (contenidos), y su tratamiento será diferente al que se da a los anteriores.

Pero aun podemos diferenciar un tercer bloque que vamos a denominar semi-estructurados por compartir con los no estructrados algunos rasgos relativos a los aspectos formales, pero mayormente aspiramos a convertirlos en estructurados mediante diversos procedimientos. Este tipo de datos se presentan en tablas insertas en documentos textuales, creados mediante procesadores de texto (.doc y otros) o en formato .pdf. Aun dentro de ellos podremos diferenciar aquellos en los que el continente formal tiene una función de estructuración de la presentación más que del contenido, lo que da lugar a posibles conflictos en su tratamiento posterior como estructurados o no estructurados. Tal es el caso de algunos soportes normativos del modelo de informe psicopedagógico; sin ir más lejos el actual de la Consejería de Educación de Asturias que, rompiendo con su formulaciones anteriores del documento, ha modifica el formato para presentarlo todo él como un conjunto de tablas, en lugar del anterior que combinaba tablas con secciones textuales.

Esos modelos mixtos combinan (o combinaban) datos semi-estructurados con vocación de desarrollo a estructurados y bloque de datos no estructurados (bloques textuales de información). Los primeros permitían procedimientos de automatización basados en estrategias ofimáticas (Combinar correspondencia) o en script (DocAp OOo Basic y/o script Python). Los segundos requieren otros planteamientos, como mínimo basados basados en textos maestros junto con el uso de marcadores, que requieren necesariamente de script, aunque son susceptibles a ser tratados también mediante procedimientos IA, especialmente dentro del campo del PLN (Procesamiento del Lenguaje Natural)

No tengo muy claro que esta deriva hacia la total estructuración del modelo de informe en formato tabla, junto con el uso ocasional de controles de formulario vaya a suponer una mejora en términos de ahorro de trabajo para los profesionales de los SEO, pero sí que va a significar mayor complejidad respecto al tratamiento de automatización parcial en su elaboración que se había logrado con el formato mixto. Pero esta, definitivamente, es ahora una cuestión irrelevante.

martes, 17 de febrero de 2026

DATOS. Tratamiento de datos

Limpieza de datos

¿En qué consiste?

La limpieza de datos es un conjunto de procedimientos que permiten identificar y corregir los errores e incoherencias que pueden presentar los datos para mejorar su calidad, a fin de garantizar que sean precisos, completos, coherentes y utilizables (1).

Se puede decir que la limpieza de datos es una fase crítica en el desarrollo de cualquier solución basada en código, especialmente necesaria en el campo de la inteligencia artificial (IA), dado que en ella importa tanto la cantidad como la calidad. Si la cantidad no es suficiente, los modelos no alcanzan el nivel crítico de rendimiento para ser funcionales, pero si los datos son de mala calidad, el resultado es necesariamente también de mala calidad.

Fruto del interés que tiene la limpieza de datos es la sistematización de la que ha sido objeto, derivando de esta la identificación de seis fases en su desarrollo:

1. Inspección: Antes de nada, observa si hay columnas con nombres extraños, fechas que parecen texto, números imposibles...
2. Gestión de los valores faltantes: Los famosos códigos NaN ante los que caben tres opciones: eliminarlos (si son pocos), imputarlos (usando la media o la mediana) o marcarlos como "Desconocido".
3. Tratamiento de valores atípicos: Identificar outliers: ¿ese dato (900 archivos en un expediente) es un error de escritura o un valor extraordinario?. Según la respuesta y el objetivo que se persiga con el análisis previsto se decide mantenerlo o eliminarlo.
4. Estandarización: Unificar formatos en expresiones como "madrid", "MADRID" y "Madrid ".
5. Deduplicación: Los registros repetidos (duplicados) son ruido que se debe elimínar para evitar sesgos.
6. Validación final: Si tras la limpieza, en un registro aparece una edad de -5 años es que algo no se hizo bien.

Para desarrollar todo este conjunto de actuaciones disponemos de diferentes estrategias, herramienta y enfoques.

POdemos recurrir a procedimientos manuales, basados en la inspección visual, las referencias cruzadas o las tablas dinámicas basadas en Excel o Calc.

En el extremo opuesto a lo "manual" se sitúan las alternativas basadas en la IA, con los que se prioriza la automatización del proceso por entero y que contempla diferentes opciones:

  • Para el análisis de los datos originales, las herramientas de limpieza de datos con IA pueden identificar automáticamente patrones, anomalías e incoherencias y sugerir correcciones.
  • Para la estandarización de los datos, las técnicas de procesamiento del lenguaje natural (PLN) pueden estandarizar texto no estructurado (el formato de direcciones, por ejemplo); los modelos de machine learning (ML) pueden identificar formatos y recomendar los que se adecúan a determinados datos, como fechas o unidades monetarias; y los generadores de expresiones regulares basados en IA permite automatizar la detección y normalización de formatos incoherentes con la naturaleza de los datos.
  • Para la consolidación de duplicados, los modelos de IA basados en reglas o en estrategias aprendizaje-máquina pueden decidir la mejor opción ante la posibilidad de eliminar duplicados, atendiendo a criterios de precisión, actualización y fiabilidad.
  • Para la aplicación de reglas, los modelos de IA pueden automatizar la creación y aplicación de reglas de limpieza de datos resultanres del aprendizaje (historial de correcciones pasadas) y aplicar estas reglas a nuevos conjuntos de datos. También pueden generar reglas personalizadas para aplicar en sectores o dominios específicos.

A pesar de su potencia, estos sistemas no son infalibles, no siempre están disponibles y no son necesariamente la mejor opción; de hecho presentan problemas de coste, de tratamiento confidencial de datos y de limitaciones para correr en nuestros sistemas en local.

Además simplemente podemos optar por mantener el control sobre el proceso de limpieza sin delegarlo totalmente en la IA y/o preferir alternativas basadas en lenguajes como Python o R. Si optamos por Python tenemos a nuestra disposición herramientas (bibliotecas) como las siguientes:
1. Pandas: Herramienta básica que permite cargar datos, filtrarlos y manejar valores nulos.
2. NumPy: Potencia matemática ideal para transformaciones numéricas complejas y para el manejo eficiente de grandes matrices.
3. Scikit-learn: Herramienta pensada para machine learning (ML) cuyo módulo preprocessing permite normalizar escalas y codificar variables categóricas con gran precisión y fiabilidad.
4. Missingno: Visualización de datos vacíos que permite vilualizar dónde están los "huecos" en tu conjunto de datos (dataset)

Nota (1)

Fuentes de información utilizadas: artículo www.ibm.com e información resultante de la consulta a IA Gemini (versión gratuíta básica)