viernes, 8 de mayo de 2026

DATOS. Tratamiento de datos

Organización de datos

Datos semi-estructurados

Cuando trabajamos con tablas-formulario (en documentos .odt en nuestro caso, aunque podrían estar ubicadas en otro tipos de documentos) fuimos capaces de extraer todos los datos que contenían, pero, como quedó dicho en ese momento, obtenemos tanto las etiquetas (identificadores de los campos) como los datos propiamente dicho. Esto conlleva que las tablas no sean directamente utilizables dentro de un planteamiento de automatización de ese uso, ya que estaríamos considerando una etiqueta como dato. Por ello es necesario reorganizarlas, dando a cada contenido su función específica, lo que equivale a convertir a estos datos de semi-estructurados a estructurados.

Para concretar lo que significa esta reorganización de los datos, vamos a trabajar con una tabla resultante del procedimiento de extracción desarrollado en entradas precedentes (por ejemplo). Por motivos que entenderás óbvios, me limitaré a mostrar únicamente unos pocos registros anonimizados para que quede claro cómo se presenta la tabla inicialmente y cómo debería quedar al finalizar el procedimiento. En este punto, ese procedimiento ha sido realizado manualmente, pero la idea es que se ejecute de forma automatizada, mediante un script Python.

Pero recordemos antes el origen de los datos:

... para visualizar a continuación la tabla que deberemos reorganizar:

Detengámonos un momento en analizar ambas imágenes (que es lo que son, capturas de pantalla modificadas) para concretar lo que dijimos al inicio de esta entrada: aunque capturamos integralmente el contenido de la tabla de formacorrecta, el resultado (precisamente por ello) no es directamente utilizable o, lo que es lo mismo, estos datos se presentan como datos estructurados (filas-columnas) pero no están debidamente organizados ni limpios. Pero por ahora nos centraremos en la primera cuestión, que tiempo habrá para tratar la segunda.

Las tres primeras columnas son directamente válidas, pero las que restan necesitan ser reorganizadas. En otro tipo de tablas, es posible que ni siquuera pasáramos de las dos primeras, y eso porque han sido generadas mediante el script, al margen de la recuperación de datos propiamente dicha. Es por eso que, en esta tabla, todo lo que no son datos identicativos del registro es necesario mapearlo para reconocer su naturaleza como etiquetas o como datos. Pero para hacer esto debemos fijarnos en dos cuestiones: la variedad de estructuras que presenta la tabla y las etiquetas que haya generado el script.

No existe una única estructura aunque visualamente no observemos, en algunos casos, esas diferencias. La tabla-formulario que mostramos (informe, no dictamen, por ajustarse a los datos obtenidos que mostramos) sólo permite identificar 6 celdas y una estructura de 2x3, pero en la tabla resultante comprobamos primero que existe una diversidad de estructuras (2x4, 2x3, 3x3) con un predomino de una de ellas (2x4), que es lo que explica las etiquetas (o campos): A1 - B1 - D1 - A2 - B2 - C2. Al ser la estructura predominante es a la que se adapta la tabla resultante; en ella no se recoge la celda B1, pero tampoco la celda D2, como sería de esperar según la diversidad de estructuras. Esto facilita, en términos generales, la reorganizar la tabla, ya que deberemos ajustarnos a los identificadores y no a la diversidad de estructuras detectadas, pero tampoco a lo que visualmente observamos, sino a lo que debemos inferir en función de las etiquetas establecidas por el script: C1 es asimilable a D1, lo que resuelve la reorganización de la mayoría de los registros (tipos 2x3 y 2x4), aunque es posible que algunos de los que se ajustan a la estructura 3x3 deberán ser objeto de una atención específica. Esta estructura responde a tablas en las que se diferencia la intervención de al menos dos profesionales, quedando el segundo fuera del esquema de la tabla. Realmente no se contempló en el procedimiento de reorganización que aquí se expone, lo que implica que éste deberá desarrollarse en al menos dos fases.

Con todos esos conocimientos, el profesional que realiza la reestructuración de la tabla, si lo hace manualmente (cosa que aquí no es especialmente costoso), deberá identificar qué campos (columnas) contienen etiquetas [A1(C) - B1(D) - B2(G)] y cuáles datos [D1(E) - A2(F) -C2(H)]. LO que resta es reformular las etiquetas que corresponden y reubicar esos bloques de forma lógica, según se espera en una colección (tabla) de datos estructurados. Este sería el resultado (lo que se aprecia en el documento INF_003 corresponde a la realidad: ese documento carece de datos):

Si queremos automatizar este mismo procedimiento, el script que sigue nos permitirá hacerlo para los registros cuya estructura se ajuste a los modelos 2x4 y 2x3, pero no para los esquemas 3x3.



import csv
import os
from datetime import datetime

# --- Función secundaria 1. Registro de errores ---

def registrar_error_log(ruta_log, nombre_fichero, mensaje_error):

    fecha_hora = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    try:
        with open(ruta_log, mode='a', encoding='utf-8') as f_log:
            f_log.write(f"[{fecha_hora}] FICHERO: {nombre_fichero} | ERROR: {mensaje_error}\n")
    except Exception as e:
        print(f"Error crítico: No se pudo escribir en el log de errores: {e}")

# --- Función secundaria 2. Reestructuración de BD (csv)

def registrar_en_csv_rev(ruta_salida, datos_mapeados):

    campos = ['ARCHIVO_ORIGEN', 'ESTRUCTURA', 'fecha', 'SEO', 'OE']
    archivo_existe = os.path.exists(ruta_salida)
    
    try:
        with open(ruta_salida, mode='a', encoding='utf-8', newline='') as f_rev:
            escritor = csv.DictWriter(f_rev, fieldnames=campos, delimiter=';')
            if not archivo_existe:
                escritor.writeheader()
            escritor.writerow(datos_mapeados)
        return True, None
    except Exception as e:
        return False, str(e)

# --- Función principal ---

def ejecutar_procesamiento_batch(ruta_csv, filtro_nombre, filtro_estruc):
    
    if not os.path.exists(ruta_csv):
        print(f"Error: No se encuentra el archivo maestro en {ruta_csv}")
        return

    dir_name = os.path.dirname(ruta_csv)                                                # Rutas de salida
    base_name = os.path.splitext(os.path.basename(ruta_csv))[0]
    ruta_rev = os.path.join(dir_name, f"{base_name}_REV.csv")
    ruta_log = os.path.join(dir_name, f"{base_name}_ERRORES.txt")

    try:
        with open(ruta_csv, mode='r', encoding='utf-8-sig') as f:
            muestra = f.read(2048)
            dialecto = csv.Sniffer().sniff(muestra)
            f.seek(0)
            
            lector = csv.DictReader(f, dialect=dialecto)
            
            print(f"--- INICIANDO PROCESAMIENTO AUTOMÁTICO ---")
            
            for fila in lector:
                orig = (fila.get('ARCHIVO_ORIGEN') or "DESCONOCIDO").strip()
                tipo = (fila.get('ESTRUCTURA') or "").strip()

                if filtro_nombre in orig and tipo == filtro_estruc:
                    registro_rev = {                                        # Mapeo de datos
                        'ARCHIVO_ORIGEN': orig,
                        'ESTRUCTURA': tipo,                                 # Datos ajustados para tabla 3x3
                        'fecha': (fila.get('A2') or "").strip(),            #  'fecha': (fila.get('C2') or "").strip()
                        'SEO': (fila.get('D1') or "").strip(),              #  'SEO': (fila.get('D1') or "").strip()
                        'OE': (fila.get('C2') or "").strip()                #  'OE': (fila.get('B2') or "").strip()
                    }
                    
                    exito, error_msg = registrar_en_csv_rev(ruta_rev, registro_rev)     # Proceso de guardado (función secundaria 2)
                    
                    if exito:
                        print(f"[OK] {orig} procesado correctamente.")
                    else:
                        print(f"[AVISO] Fallo en {orig}. Registrando en log...")
                        registrar_error_log(ruta_log, orig, error_msg)				# Llamada (condicionada) a la función secundaria 1

            print(f"--- PROCESAMIENTO FINALIZADO ---")

    except Exception as e:
        print(f"Error general en la lectura del CSV maestro: {e}")

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

if __name__ == "__main__":
    
    RUTA_SISTEMA = r"ruta_de_mi_archivo.csv"           # Aquí la ruta absoluta (incluido nombre) de la tabla CSV a reestructurar
    TIPO_DOC = "INF"                                   # Aquí el identificador del tipo de archivo (DE vs. INF)
    ESTRUCTURA = "2x4"                                 # Aquí el identificador del tipo de estructura (2x3, 2x4)
    
    ejecutar_procesamiento_batch(RUTA_SISTEMA, TIPO_DOC, ESTRUCTURA)


El resultado para los registros 2x3 y 2x4 es el positivo, ya que el script de extracción previo asimiló las celdas, según expliqué en el párrafo anterior a la presentación del script, así que la tabla de reformulación automatizada de la estructura es la misma que la resultante del procedimiento manual. Así que nos ahorramos repetirla.

Por explicar el script, decir que, como se puede ver, está compuesto por tres funciones, dos secundarias y la principal, y el ejecutor de la función princial. Empezando por éste, se diferencian en él las variables que el usuario deberá adaptar a su caso (siempre con la limitación de la tabla original, tanto de los documentos de informe como de dictamen de los modelos asturianos): ruta del csv a reformular, identificador del tipo de documento e identificador del modelo de tabla. Este script llama a la función principal pasando los datos anteriores como parámetros y esta función, además de ejecutar su cometido, llama a las funciones secundarias o auxiliares (ver los comentarios). Destacar en esta función principal el segmento de código encargado del mapeo de los datos, entre otras cosas, porque es el que deberemos adaptar a nuestras necesidades, por ejemplo para procesar los registro 3x3 (en comentarios se concreta esa alternativa)

La función secundaria 1 se activa de forma condicionada, por lo que no necesariamente se ejecuta. Tiene como objetivo crear un regisgtro (txt) de errores en el proceso, pero no he observado que se ejecute. La que sí se ejecuta (afortunadamente) es la función secundaria 2ª, ya que su comentido es crear el archivo csv de reestructuración de la tabla.