Mostrando entradas con la etiqueta Automatización de procesos. Mostrar todas las entradas
Mostrando entradas con la etiqueta Automatización de procesos. Mostrar todas las entradas

viernes, 20 de marzo de 2026

Expedientes

Números básicos

Vamos a analizar en esta entrada los aspectos cuantitativos de nuestra muestra de expedientes, empezando por su cuantificación básica: número de expedientes y cantidad de archivos que contienen. El segundo análisis de esta entrada se centrará en el estudio de la tipología de los documentos. En ambos casos se exppondrán y analizarán los datos resultates del estudio y los recursos informáticos empleados.

Con el conocimiento de los datos cuantitativos básicos nos podremos hacer una idea de la masa documental que suponen, lo cual es un dato importante para conocer la capacidad que tienen de servir al objetivo (o a los objetivos) que nos planteamos con ellos, además de el trabajo que representa su mentanimiento.

El conocimiento de la tipología documental y su peso en esa masa de documentos nos acercará a un primer conocimiento de las prácticas de trabajo que implica la creación de los expedientes y su manteniento, y nos permitirá realizar previsiones sobre las tecnologías que necesitaremos para acceder a su contenido.

Empezaremos por indicar la necedidad de archivar en una unidad que vamos a considerar unidad central del SEO, un directorio al que llamaremos Expedientes_SEO, en que se archivarán todos los directorios o expedientes individuales identificados únicamente por el nombre o identificador del alumno. Una alternativa puede ser sudvidir este directorio principal en subdirectorios por centro, pero a la larga resulta más sencillo de gestionar un directorio general no diferenciado, aunque sea necesario generar algún sistema de gestión que incluya la posible elección de expediente en función del centro. De todas formas esta es una cuestión de gestión de la que nos ocuparemos en su momento. De momento sirva únicamente como base para comprende los procedimientos de trabajo que explicaré a continuación.

Para lo que ahora nos ocupa partiré de la existencia de dicho directorio Expedientes_SEO en el que se encuentran organidazos en subdirectorios los archivos de que consta cada uno de los expedientes. Lo que ahora queremos saber es de cuanto expedientes consta nuestra muestra, cosa que podemos conocer de diferentes formas. Yo voy a emplear un script Python creado para contar las carpetas o directorio que existe en una ruta dada.



import os

#Función para contar directorios ---------------------------------------

def contar_subdirectorios(ruta_principal):
    """
    Cuenta cuántas carpetas o dirctorios hay dentro de una ruta específica.
    """
    try:
        # Verificamos si la ruta existe
        if not os.path.exists(ruta_principal):
            return "La ruta especificada no existe."

        # Listamos el contenido y filtramos los directorios. os.path.isdir une la ruta principal con el nombre del elemento
        subdirs = [nombre for nombre in os.listdir(ruta_principal) if os.path.isdir(os.path.join(ruta_principal, nombre))]
        return len(subdirs)

    except PermissionError:
        return "Error: No tienes permisos para acceder a esta carpeta."

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

ruta = "ruta" # Cambia "ruta" por tu ruta absoluta (ej.: "C:/Usuarios/Documentos").
resultado = contar_subdirectorios(ruta)

print(f"Número de subdirectorios en '{ruta}': {resultado}")


En este script debemos fijarnos especialmente en...

subdirs = [nombre for nombre in os.listdir(ruta_principal) if os.path.isdir(os.path.join(ruta_principal, nombre))]

... instrucción de síntesis que nos permite recorrer el directorio que contiene los expedientes y contar los directorios (expdientes) gracias a la función os.path.isdir().

En cuanro al resultado que obtenemos de aplicar esta función, el total es 482 expedientes, número suficiente para que nuestro análisis pueda considerarse representativo del número de expedientes y de la documentación que se puede manejar en un SEO.

El siguiente paso es conocer el número total de documentos de que constan esos expedientes, lo que nos proporciona una primera idea de su contenido, de cómo han sido tratados estos expedientes y de la utilidad que pueden tener para la finalidad que buscamos: facilitar el seguiento del alumnado.

Para realizar este cálculo vamos autilizar tambien un script de Python a modo de "herramienta".



from pathlib import Path

def contar_archivos_recursivos_pathlib(ruta_base):
    # Convertimos la cadena en un objeto Path
    path_principal = Path(ruta_base)
    
    if not path_principal.exists():
        return "La ruta no existe."

    # rglob("*") busca TODO de forma recursiva (carpetas y archivos). Usamos una "comprensión de lista" para filtrar y contar
    conteo = sum(
        1 for elemento in path_principal.rglob("*") 
        if elemento.is_file() and elemento.parent != path_principal
    )
    
    return conteo

# --- Configuración ---
	ruta = "Tu ruta"	# Ecribe aquí la ruta de tu directorio
	total = contar_archivos_recursivos_pathlib(ruta)

print(f"Total de archivos dentro de los subdirectorios: {total}")


También es este caso podemos identificar una expresión que resulta clave para el funcionamiento del script. En este caso es la expresión...

conteo = sum(1 for elemento in path_principal.rglob("*") if elemento.is_file() and elemento.parent != path_principal)

...en la que resulta clave la función elemento.is_file().

El resultado es un total de 3.777 archivos, lo que también resulta ser un volumen significativo de documentos; suficiente en principio para que su análisis arroje alguna luz sobre los expedientes SEO.

Conocidos los datos previos, podemos saber cual es la media de archivos por expediente, dato necesario para una primera aproximación al objetivo básico de este análisis: ¿sirven los expedientes SEO como recurso para proporcionar información relevante para el siguimiento del alumnado?. Ahora podríamos concretarlo como sigue: ¿cuentan los expedientes SEO del número suficiente de archivos como para ofrecer información relevante para el seguimiento del alumnado?. La respuesta cuantitativa es la siguiente: 3,68 archivos por expediente.

Realmente este número no nos permite responde a nuestra pregunta, ya que esos casi 4 archivos por expediente sitúan a nuestros expedientes en un nivel muy próximo al mínimo necesario para que, según la experiencia, se pueda considerar que un expediente contiene información suficiente para servir a nuestro objetivo. Realmente ese podría ser el mínimo de archivos resultante del inicio de una intervención, pero no para aquellas que llevan tiempo desarrollándose. Dico de otro modo: son pocos archivos por expediente, lo que supone, presumiblemente, que existen importantes disparidades en cuanto al contenido entre este conjunto de expedientes.

Y esa es precisamente la cuestión que debemos resolver ahora: conocer la composición real de cada uno de los expedientes en cuanto al número de archivos que contienen. Esto nos permitirá conocer el grado de homogeneidad vs. disparidad en la composición, dato éste de interés para pareciar también la nivel de sistematicidad con que se tratan los expedientes, cuestión esta que es clave para entender el planteamiento de trabajo del SEO al respecto y todo lo que de ello se puede derivar

Pero antes de analizar datos debemos disponer de ellos, cosa que aun no hemos resuelto.

Podemos ahordar esta tarea, igual que las dos precedentes, de forma manual, pero también mediante un script-herramienta Python. Definitivamente la opción manual queda descartada (disponiendo de un recurso eficiente de automatización), ya que su coste es muy elevado en tiempo, como tambien lo hubiera sido el recuento de archivos, aunque bien podríamos haberlo resuelto directamente mediante un recuento sistemático basado en la opción Propiedades del sistema operativo, o empleando la instrucción dir sobre cada carpeta-expediente.

Esta imagen muestra la primera opción, pero ambas comporten el mismo problema: aunque es un procedimiento que funciona, es realmente muy costoso en tiempo ejecutar este procedimiento cuanto el número de directorios es elevado, como es el caso. Como alternativa podemos hacer uso de los comandos del sistema operativo, creando incluso un archivo .bat, pero llegados a este extremo, la opción Python resulta más funcional.


import csv
from pathlib import Path

#ruta = Path('Tu_Ruta')				# Definir aquí tu ruta

# Definimos la carpeta 'documentos' en el mismo nivel de directorio que este script
ruta_destino = Path(__file__).parent / 'documentos'
archivo_csv = ruta_destino / 'listado_archivos.csv'

ruta_destino.mkdir(exist_ok=True)	# Nos aseguramos que la carpeta 'documentos' existe

datos_para_csv = []

if ruta.exists() and ruta.is_dir():
    for carpeta in ruta.iterdir(): 	# Nos aseguramos que la carpeta 'documentos' existe
        if carpeta.is_dir():
            total_archivos = sum(1 for f in carpeta.rglob('*') if f.is_file())	# Contamos todos los archivos dentro de ESTA carpeta y sus subcarpetas usando rglob('*')
            print(f"{carpeta.name} || {total_archivos}")
            datos_para_csv.append([carpeta.name, total_archivos])	# Guardamos el nombre y el total como una fila
# Creación del archivo CSV
    with open(archivo_csv, mode='a', newline='', encoding='utf-8') as f:
        escritor = csv.writer(f)
        if archivo_csv.stat().st_size == 0:					# Verificamos que el csv está vacío y (en su caso) escribimos la cabecera
            escritor.writerow(['Carpeta', 'Total_Archivos'])
        escritor.writerows(datos_para_csv)					# Escribimos los datos recolectados
    print(f"\n✅ Listado guardado en: {archivo_csv}")
else:
    print("La ruta 'Mi_Directorio' no es válida.")


Lo que ahora necesitamos es algo tan simple como un recuento expediente a expediente, que es lo que hace este script, que además archiva el resultado del recuento en un documento csv que contiene el listado de expedientes (identificador) y del número de archivos que contiene cada expediente. Se trata de crear la base de datos necesaria para el posterior análisis de datos, que requerirá acceder al citado archivo csv mediante un segundo script Python.

Para no repetir script, me voy a adelantar ahora a los resultados de ese futuro análisis mostrando el resultado de un primer análisis que resulta revelador de la realidad de nuestra muestra de datos. Muestro el gráfico resultante para ilustrar este primer análisis.

Considero que este gráfico, creado a partir del recuento de datos mediante la librería matplotlib muestra con claridad que nuestra muestra de expedientes se aleja bastante de la uniformidad que engañosamente parece indicar el promedio de archivos por expediente. Pero en realidad muestra mucho más. Veamos qué.

Cuando calculé el citado promedio dije que era un promedio bajo, demasiado próximo al mínimo suficiente (o no tanto) para considerar funcional a ese "expediente": si la documentación de que consta un expediente debe servir para proporcionar datos para el seguimiento del "caso", es necesario que conste de un número mínimo de archivos (además de otros requisitos que veremos en su momento), y es evidente que ni uno ni dos archivos alcanza ese mínimo, y bastante dudoso que tres archivos lo haga (diríamos que si y sólo sí se cumplen otras condiciones, pero de momento no vamos a entrar en estas cuestiones). Con cuatro archivos es posible que estemos ya dentro de posibles, en este caso, y al contrario que con tres, siempre que no se cumplan los criterios opuestos.

Y resulta que las dos mayores frecuencias observadas, y en orden, son precisamente esos valores: en nuestra distribución predominan ampliamente los expedientes que constan de 1 (99) y de 2 (64), en total 163 expedientes (prácticamente el 34% del total de los expedientes) no contienen el mínimo necesario de archivos para permitir (potencialmente) el cumlimiento del objetivo planteado. Pero tenemos, además, otros 43 expedientes cuya capacidad para lo mismo resulta dudosa. Esto eleva el total "inseguro" a cerca del 43% del total de los expedientes disponibles.

Aun con todo seguimos disponiendo de un total de 276 expedientes que, a priori, nos permiten avanzar en nuestro análisis, lo que nos deja un margen suficiente para dar respuesta a ese objetivo, pero los datos anteriores dicen algo más sobre el trabajo con los expedientes SEO: al menos en este caso concreto se puede observar que no existe en el SEO una praxis sistemática de trabajo con la documentación generada en la intervención, no siendo infrecuente (todo lo contrario) generar "expedientes" que me resisto a considerar tales, sin dar continuidad ni desarrollar un planteamiento de trabajo coherente con el mantenimiento (y mucho menos el uso) de dichos "agrupamientos de documentos".

De esto se puede derivar perfectamente que sólo esos 276 expedientes pueden ser considerados de utilidad para nuestro estudio; y en ese sentido vamos a reformular el conteo y posterior análisis de datos. Pero no está justificado que despreciemos sin más esa nada desdeñable cantidad de "carpetas" (ya que no expedientes) y los archivos que contienen (356) por más que, en este caso, representen sólo el 9.5% del total de archivos.

En consecuencia me propongo realizar dos análisis, aunque sólo uno de ellos (el segundo) seguirá la línea del objetivo inicialmente planteado:

  • Primero. Analizar la composición documental de las carpetas de alumnos (antiguos expedientes con 1, 2 y 3 archivos)
  • Segundo. Analizar los expedientes con potencial para serlo en términos de consecución del objetivo planteado: facilitar datos para el seguimeinto del alumnado

En una próxima entrada retomaré estos análisis.

miércoles, 18 de marzo de 2026

Expedientes

Por qué expedientes SEO

Lo primero que podemos decir respecto a los expedientes SEO es que, de una u otra forma, por un motivo o por otro, son una realidad persistente. Lo que entiendo está indicando que los SEO los considerar (ayer y hoy) como un recurso necesario.

Primero fueron carpetas y folios, más notas en cuadernos y agendas. Después de fueron conformando en formato digital hasta ser ahora predominante pero no exclusivamente digitales. La necesidad inicial derivaba del modelo de intervención en centros como servicio de carácter marcadamente externo y como servicio autónomo y diferenciado. Después las Unidades de Orientación, incluso en centros sin SEO propio, articularon un modelo de intervención más ligado al centro, en el que el SEO del EOE se iba identificando con la dinámica y la organización del Centro sin perder por ello su estatus específico como perteneciente al EOE. En este contexto, la Administración educativa fue implementando servicios informátizados de gestión documental y del expediente escolar (en Asturias, SAUCE, además de otros servicios de gestión complementarios en algunos casos, puramente de gestión otros). Con todo lo que este cambio de contexto implica de cambio de acceso a datos, nada ha impedido que se mantengan lo que aquí estamos llamado expedientes SEO y su funcionalidad para los propios EOE.

Es cierto que no siempre esta necesidad de la que deriva la pervivencia ha estado acompañada de un esperable esfuerzo de mantenimiento y mejora de la gestión. Y no me refiero sólo a la pervivencia de procedimientos basados en el papel, algunos de ellos mero vestigio de prácticas heredadas, otros consecuencia de la falta de alternativas digitales suficientemente funcionales y sencillas de implementar. Me refiero también a la falta de sistematicidad en los recursos y procedimientos de gestión, empezando por la muy deplorable ausencia criterios compartidos y explicitados de organización del contenido y siguiendo por la no menos incomprensible ausencia de interés (¿aparente?) por el desarrollo de procedimientos de aprovechamiento de toda esta riqueza documental, incluyendo la muy interesante abundancia de documentación antigua aun no digitalizada y menos explorada.

Diría yo que esta asuencia de actualización y aprovechameinto hace aun más destacable la persistencia de estas prácticas (por llamarlas de algún modo) que llevan a que, curso tras curso, los EOE incrementen el número de expedientes y el volumen de la documentación que contienen los creados en cursos posteriores. ¿Para qué tanto esfuerzo?, ¿qué hace que resulte de interés para los SEO perdurar en estas prácticas, aun a pesar de considerarlas una carga burocrática más?.

Varias podrían ser las respuestas, pero se me ocurre una muy simple y no siempre coherente con lo que muestra la realidad del uso de los expedientes: porque los SEO considerar los expedientes como la principal y más fiable fuente de información sobre un alumno y sobre las actuaciones desarrolladas con él por parte del Servicio. Y se considera así precisamente por nutrirse de la información que aportan, curso a curso, los propios SEO. Para que un "expediente" cumpla esta función, realmente no necesita estar inmaculadamente organizado, es suficiente con que esté actualizado.

Es por ello que el principal cuello de botella para la utilidad efectiva sea realmente un problema y esté sin resolver de modo totalmente satisfactorio. Me refiero a la incorporación efectiva al expediente de esas actuaciones "menores" que sólo quedan recogidas en el cuaderno de trabajo, notas cuyo estatus no siempre está claro, pero que normalmente se quedan en poder del OE o del PSC, aunque son el centro del muy serio intercambio de información que se da entre profesionales cuando se porduce un cambio de referente en la intervención. En algunos casos (PSC, fundamentalmente) se ha llegado a formas funcionales de informatización sistemática de esta recogida de datos (informes SISE, por ejemplo), pero ni están sistematizados ni dejan de suponer una sobrecarga de trabajo difícilmente asumible y, por ello, de escasa incidencia en los expedientes.

Pero salvo este nada menor problema pendiente de resolución, lo cierto es que en lo que al resto de la documentación se refiere es difícil que un mantenido y actualizado (aunque no necesariamente organizado) expediente SEO pueda ser superado en funcionalidad por los expedientes escolares de carácter administrativo o por las bases de datos oficiales tipo SAUCE. Funcionalidad para el seguimiento de la intervención, que es de lo que se trata en primer (y a veces único) término, aunque bien podría servir también para el análisis de la intervención y para el desarrollo de proyectos de investigación basados en la práctica.

Cierto que la carencia de estos dos últimos no se debe a la falta de documentación, y sí a la ausencia de propyectos (¿y de interés por ddesarrollarlos?), pero también lo es que la falta de organización y sistematización de la documentación de los expedientes SEO no ayuda precisamente. Tampoco lo hace para el uso básico (el seguimiento), pero aquí estas carencias son más fáciles de suplir con paciencia, tiempo y el saber hacer que proporcional la experiencia.

El objetivo de esta sección es precisamente proporcionar herramientas para facilitar que estas prácticas sean más viables, o cuanto menos menos costosas. Para ello empezaremos por aprender de lo que hay y de cómo está configurado para realizar algunas propuestas de mejora.

jueves, 5 de marzo de 2026

DATOS. Acceso a datos

CSV. Datos estructurados (III)

Acceso y descarga de csv

Vamos a centrarnos en este script en el trabajo con un archivo .csv, pensando no en el acceso (tema ya tratado en esta entrada), sino en tener disponible su contendio desde el script. Eso sí, seguiremos usando el módulo propio CSV, si bien ahora en la opción de diccionario.

Para que se entienda mejor el proceso a seguir, empezaremos por el acceso al archivo .csv. La diferencia principal respecto al script parejo de la entrada anterior es que sustituímos lector = csv.reader(archivo, delimiter=',') por lector_dict = csv.DictReader(archivo). El resto derivan de ésta.



import csv

archivo_nombre = 'datos/datos.csv'										# 1. Definimos la ruta del archivo

try:
    with open(archivo_nombre, mode='r', encoding='utf-8') as archivo:  # 2. Abrimos el archivo en modo lectura ('r')
        lector_dict = csv.DictReader(archivo)                          # 3. Creamos el lector de diccionarios
        print(f"Leyendo datos de: {archivo_nombre}\n" + "-"*30)
        for fila in lector_dict:                                       # 4. Recorremos cada fila (cada fila es un diccionario)
            nombre = fila['Nombre']                                                         
            edad = fila['Edad']        
            print(f"Usuario: {nombre} | Edad: {edad}")                          

except FileNotFoundError:                                              #Control de errores
    print(f"Error: El archivo '{archivo_nombre}' no existe.")
except KeyError as e:
    print(f"Error: No se encontró la columna {e} en el archivo.")


A pesar de las diferencias, ambos archivo comparten la misma limitación: la base de datos sólo está disponible dentro del ámbito de with open(), pero fuera no podemos acceder a los datos, así que si nos interesa deberemos trasladarlos a una colección de datos del propio script, concretamente a una lista de diccionarios.



import csv

archivo_nombre = 'datos/datos.csv'                                                  # 1. Definimos la ruta del archivo

datos_internos = []                                                                 # Lista para almacenar la base de datos en memoria


try:
    with open(archivo_nombre, mode='r', encoding='utf-8') as archivo:  				# 2. Abrimos el archivo en modo lectura ('r')
        lector_dict = csv.DictReader(archivo)                                       # 3. Creamos el lector de diccionarios
        print(f"Leyendo datos de: {archivo_nombre}\n" + "-"*30)
        for fila in lector_dict:                                                    # 4. Recorremos cada fila (que es un diccionario)
            nombre = fila['Nombre']                                                         
            edad = int(fila['Edad'])
            datos_internos.append(fila)        
            print(f"Usuario: {nombre} | Edad: {edad}")                          
        print(f"--- Carga finalizada ---")
        print(f"Se han almacenado {len(datos_internos)} registros en el diccionario interno.")

    if datos_internos:																# Ejemplo: Acceder al primer registro almacenado
        primero = datos_internos[0]
        print(f"Primer registro en memoria: {primero['Nombre']} tiene {primero['Edad']} años.")
        
except FileNotFoundError:                                                           #Control de errores
    print(f"Error: El archivo '{archivo_nombre}' no existe.")
except KeyError as e:
    print(f"Error: No se encontró la columna {e} en el archivo.")

#Ejemplo de acceso a la estructura del diccionario ---------------------------------------------------------------------------------------

registro_ejemplo = datos_internos[0]                   # Tomamos el primer diccionario de nuestra lista para el ejemplo
print(f"Claves: {list(registro_ejemplo.keys())}")      # A. Identificar solo las CLAVES
print(f"Valores: {list(registro_ejemplo.values())}")   # B. Identificar solo los VALORES


Lo que nos permite importar al script el contenido del archivo .csv empieza aquí (datos_internos = []) y se concreta en estas dos líneas:
  • for fila in lector_dict: ----> Recorre el archivo .csv
  • ----datos_internos.append(fila) ----> Añade contenido a la lista (de diccionarios) datos_internos

Ellas hacen posible el correcto acceso a los datos mediante instrucciones como print(f"Claves: {list(registro_ejemplo.keys())}"), que muestra las claves del diccionario.

Cierto que la consecuencia es un incremento de la carga de memoria, pero la ventaja en rapidez de funcionamiento compensa cuando necesitamos realizar diferentes acciones sobre los datos desde un mismo script. Tal es el caso del que sigue y con el que finalizamos esta entrada: un script mediante el que accedemos a una base de datos, la cargamos en memoria y realizamso sobre ella un conjunto de acciones que aquí se concretan en seleccionar y filtrar registros.



import csv

# --- FASE 1: CARGA DE DATOS  ---------------------------------------------------

ruta_archivo = 'datos/datos.csv'
datos_internos = []

try:
    with open(ruta_archivo, mode='r', encoding='utf-8') as archivo:
        lector = csv.DictReader(archivo)
        for fila in lector:
            fila['Edad'] = int(fila['Edad']) # Convertimos el valor de [Edad] a entero para poder filtrar
            datos_internos.append(fila)
    print(f"Sistema listo. {len(datos_internos)} registros cargados.\n")

except FileNotFoundError:
    print("Error: No se encontró el archivo en D:/. Por favor, créalo primero.")
    exit() # Finaliza el script si no hay datos

# --- FASE 2: OPCIONES ------------------------------------------------------------

while True:
    print("\n===============================")
    print("   GESTOR DE CONSULTAS")
    print("===============================")
    print("1. Buscar por NOMBRE (Coincidencia parcial)")
    print("2. Filtrar por EDAD (Exacta o Rango)")
    print("3. Salir")
    
    opcion = input("\nSeleccione una opción: ").strip()

    if opcion == '3':
        print("Saliendo del sistema...")
        break

# --- OPCIÓN 1: BÚSQUEDA POR NOMBRE ---
    if opcion == '1':
        termino = input("Escriba el nombre o parte de él: ").strip().lower()
        encontrados = [r for r in datos_internos if termino in r['Nombre'].lower()]
        
        print(f"\nResultados para '{termino}':")
        if encontrados:
            for e in encontrados:
                print(f"ID: {e['Nombre']} | Edad: {e['Edad']}")
        else:
            print("No se encontraron coincidencias.")

# --- OPCIÓN 2: FILTRO POR EDAD ---
    elif opcion == '2':
        print("\n--- Filtro de Edad ---")
        print("A. Edad exacta")
        print("B. Rango (Mínimo y Máximo)")
        sub = input("Elija modalidad (A/B): ").strip().upper()

        resultados = []
        try:
            if sub == 'A':
                objetivo = int(input("Edad a buscar: "))
                resultados = [r for r in datos_internos if r['Edad'] == objetivo]  # Identificamos si el valor de la clave 'Edad' coincide
            
            elif sub == 'B':
                v_min = int(input("Edad mínima: "))
                v_max = int(input("Edad máxima: "))
                resultados = [r for r in datos_internos if v_min <= r['Edad'] <= v_max]  # Filtro por intervalo de valores          
         
            if resultados:
                print(f"\nSe hallaron {len(resultados)} personas:")
                for r in resultados:
                    print(f"• {r['Nombre']} - {r['Edad']} años")
            else:
                print("No hay registros en ese rango.")
                
        except ValueError:
            print("Error: Por favor, ingrese solo números enteros para la edad.")

    else:
        print("Opción no válida. Por favor, marque 1, 2 o 3.")

print("\nPrograma finalizado correctamente.")


NOTA: El archivo .csv de prueba es necesario para el correcto funcionamiento de estos tres script, aunque siempre los puedes adaptar para que funcionen con un .csv diferente. No obstante, si prefieren no hacerlo, aquí lo puedes descargar.

domingo, 1 de marzo de 2026

DATOS. Acceso a datos

CSV. Datos estructurados (II)

Módulo CSV. Nuevo registro de datos

En la entrada precedente aprendimos a acceder a un archivo .csv usando el módulo CSV; en la actual aprenderemos a añadir registros usando el mismo módulo. Además iremos directos al grano, digo, al script...



import csv
import os
from datetime import datetime

ruta_archivo = 'datos\libros3.csv'
NOMBRE_CAMPO_FECHA = 'fecha_lectura'

def sesion_carga_masiva():
    if not os.path.exists(ruta_archivo):
        print(f"❌ El archivo '{ruta_archivo}' no existe.")
        return

    try:
        # 1. Carga inicial para conocer la estructura y el último ID
        with open(ruta_archivo, mode='r', encoding='utf-8', newline='') as archivo:
            lector = csv.reader(archivo)
            cabecera = next(lector)
            datos_existentes = list(lector)
            
        # El ID inicial se basa en lo que ya hay en el archivo
        siguiente_id = len(datos_existentes) + 1
        campos_a_pedir = cabecera[1:-2]

        print(f"--- 🚀 Sesión iniciada. Para terminar escribe 'salir'.\n")

        while True:
            print(f"📝 Preparando Registro #{siguiente_id}:")
            datos_usuario = {}
            cancelar = False

            # 2. Bucle para pedir cada campo de la cabecera
            for columna in campos_a_pedir:
                valor = input(f"   {columna}: ").strip()
                
                if valor.lower() == 'salir':
                    cancelar = True
                    break
                datos_usuario[columna] = valor

            if cancelar: 
                break

            # 3. Procesamiento de Fecha y Validación
            fecha_str = datos_usuario.get(NOMBRE_CAMPO_FECHA)
            try:
                fecha_obj = datetime.strptime(fecha_str, "%d/%m/%Y")
                mes_auto = fecha_obj.month
                anio_auto = fecha_obj.year

                # 4. Construcción de la fila
                nueva_fila = [siguiente_id]
                for columna in campos_a_pedir:
                    nueva_fila.append(datos_usuario[columna])
                nueva_fila.extend([anio_auto,mes_auto])

                # 5. Guardado físico en el CSV
                with open(ruta_archivo, mode='a', encoding='utf-8', newline='') as archivo_escritura:
                    escritor = csv.writer(archivo_escritura)
                    escritor.writerow(nueva_fila)

                print(f"✅ Registro #{siguiente_id} guardado con éxito.\n")
                print("Escribe 'salir' para finalizar registro.\n")
                
                # Incrementamos el ID para el próximo libro de esta misma sesión
                siguiente_id += 1

            except ValueError:
                print(f"\n❌ ERROR: '{fecha_str}' no es una fecha válida (DD/MM/AAAA).")
                print("⚠️ Este registro no se guardó. Por favor, reinténtalo.\n")
                # No incrementamos el ID porque el registro falló

        print(f"\n--- Sesión finalizada. Se han añadido nuevos libros. ---")

    except Exception as e:
        print(f"❌ Error crítico en la sesión: {e}")

if __name__ == "__main__":
    sesion_carga_masiva()


Tomo como referencia el mismo conjunto de datos (libros3.csv) que nos sirvió para aprender a acceder y visualizar registros, pero esta vez planteo un procedimiento simple de escritura de registros mediante la función csv.writer(). Además de esta función, writerow() también es básica para el correcto funcionamiento del script, ya que es la que se encarga de añadir un uevo registro a nuestro documento.

En este caso utilizo una función (sesion_carga_masiva()), a la que llamo mediante el procedimiento if __name__ == "__main__":, aun pendiente de explicación. Todo llegará.

Aunque el usuario debe introducir la mayoría de los campos, algunos se implementan automáticamente. Tal es el caso del id del libro (siguiente_id = len(datos_existentes) + 1) y de los datos correspondientes al año y al mes de lectura, dereivados ambos del campo fecha_lectura (nueva_fila.extend([anio_auto,mes_auto])).

Dado que el script debe permitir la entrada múltiple de registros, se implementa un bucle (while True:) que lo hace posible, requiriendo al usuario la expresión 'salir' para cerrar el bucle y finalizar el script.

viernes, 27 de febrero de 2026

DATOS. Acceso a datos

CSV. Datos estructurados (I)

Módulo CSV. Acceso al archivo

Antes de nada una aclaración que parece necesaria: cuando hablamos de acceso a datos debemos distinguir entre lo que es acceder al contenido de un archivo y lo que implica acceder a un determinado dato o conjunto de datos que éste contiene. Lo primero implica hacer uso de determinadas tecnologías y lo segundo, además, la aplicación de determinados procesimientos.

En lo que se refiere a esta entrada cabe diferenciar esos procedimientos en el marco de una distinción que resulta de gran interés: la diferencia que implica acceder a datos estructurados y a datos no estructurados. Cuando hablamos, como ahora, de acceder a datos csv estamos planteando acceder a un tipo de documento (extensión .csv) que es de naturaleza estructurada, como lo son también los datos de una base de datos o los datos contenidos en una hoja de cálculo.

El formato .csv es uno de los formatos básicos para el trabajo con Python. Otro es el formato .txt, pero entre ambos existe una diferencia muy importante: los datos .csv están estructurados, mientras que los datos .txt no lo están. El acceso simple a ambos es sumamente sencillo desde Python, pero mientras que sigue siendo sencillo acceder a contenidos concretos de un archivo .csv, no lo es si trabajamos con un archivo .txt. Es este el motivo por el que (despues de hacerlo sobre directorios y archivos) nos planteamos ahora tratar el acceso a los archivos .csv y sus contenidos.

Un archivo .csv (comma-separated values) es una archivo de texto plano que contiene datos tabulares estructurados en filas y columnas separados por comas u otros delimitadores (punto y coma, tabulaciones).

Cada línea del archivo corresponde a una fila en la tabla, y cada valor de esa fila se separa por un delimitador (coma). Dada su simplicidad pueden ser leídos por casi cualquier hoja de cálculo o software de gestión de datos.

Python cuenta con herramientas para trabajar con archivos .csv, empezando por el módulo nativo CSV (que no requiere instalación mediante pip al ser nativo, aunuse ser importado) y siguiendo por Pandas y NumPy. Empezaremos por CSV

El módulo CSV cuenta con clases para leer y escribir datos tabulares en formato CSV y permite escribir y leer este tipo de archivo.

Existen dos enfoques de trabajo de este módulo en el tratamiento que da a los datos del archivo .csv: como listas o como diccionarios.

  • csv.reader(), enfoque lista en la lectura del archivo csv
  • csv.writer(), enfoque lista en la escribir en un archivo csv
  • csv.DictReader(), enfoque diccionario de la lectura de un archivo csv
  • csv.DictReader(), enfoque diccionario para la escritura en archivos csv

Veamos cómo se concretan ambos enfoques en el manejo de un mismo archivo csv.


import csv

#Acceso al archivo csv
ruta = 'datos\libros2.csv'
with open(ruta, mode='r', encoding='utf-8', newline='') as archivo:

# Creamos el lector enfocado a lista (csv.reader())
    lector = csv.reader(archivo, delimiter=',')
    
# Visualizamos la cabecera de la tabla csv  (mediante next())
    cabecera = next(lector)
    print(f"Listado de nombres de columnas (cabecera): {cabecera}")
    
# Iteramos sobre las filas mostrando las tres primeras columnas: id, autor y título
    for fila in lector:
          print(f"Id: {fila[0]}, Autor: {fila[1]}, Título {fila[2]}")


Esta es la forma más sencilla de acceder a un archivo csv. Es útil cuando conocemos la estructura de la tabla y el archivo tiene un tamaño manejable.

Es importante utilizar al fórmula with open() para facilitar el manejo del archivo csv, ya que nos permite abrirlo y cerrarlo de forma automática.

El acceso al contenido del archivo viene dada por la función csv.reader(), que, como vimos antes, nos permite capturar dicho contenido en una lista; primero a la cabecera (next()) y después a los datos mediante un bucle (for fila in lector:).

Sin embargo, esta forma de acceso tiene una limitación importante para el posterior acceso a los datos: es de recorrido único, por lo que deberemos repetir el procedimiento de acceso tantas veces como maniobras necesitemos realizar con el contenido del archivo csv.

En el script anterior la maniobra consistió en listar los registros y mostrarlos por pantalla (print(f"Id: {fila[0]}, Autor: {fila[1]}, Título {fila[2]}")) y en el siguiente accedemos a un registro determinado y dentro de él a una serie de campos empleado también un bucle y un contador para identificar el número de registro de deseamos visualizar (conta = 0), en este caso el nº 5 (if conta == 5)


#Mostramos un registro determinado y dentro de él dos campos.
#Para ello empleamos la iteración y nuestro conocimiento de la estructura de la tabla 

    for fila in lector:
        conta += 1
        if conta == 5:
            print(f"Autor: {fila[1]}, Título {fila[2]}")
            break 

Basándonos en los anterior, podemos crear un buscador sencillo de registros que funcione a petición del usuario. Simplemente se trata aplicar las funciones vistas antes, csv.reader() y otras, y poco más. Te dejo el script resultante de este propósito para que lo puedas usar en tus proyectos.

import csv

ruta_archivo = 'datos\libros3.csv'

def buscador_interactivo():
    try:
        # 1. Abrimos el archivo
        with open(ruta_archivo, mode='r', encoding='utf-8', newline='') as archivo:
            # Cargamos el lector
            lector = csv.reader(archivo)
            cabecera = next(lector)
            
            # Convertimos el lector a una lista para poder buscar varias veces. De no hacerlo sólo podríamos buscar un registro.
            # La consecuencia es un incremento de la carga de trabajo para la memoria del sistema (el contenido de la lista, ahora en memoria)
            datos = list(lector)
            total_registros = len(datos)

            print(f"--- 🔎 Buscador de registros cargado. ({total_registros} registros disponibles) ---")

            while True:
                # 2. Solicitar número al usuario
                entrada = input("\nIntroduce el número de registro a buscar (o 'salir' para terminar): ")

                if entrada.lower() == 'salir':
                    print("Gracias por usar nuestro buscador de registros.")
                    break

                # 3. Validar que sea un número
                try:
                    objetivo = int(entrada)
                except ValueError:
                    print("❌ Por favor, introduce un número válido.")
                    continue
                encontrado = False
                
                # 4. Buscar usando la función enumerate() sobre la lista de datos
                # 4.1. 'datos' es nuestro conjunto de filas.
                # 4.2. 'start=1' indica el inicio del conteo desde 1 (y no desde 0).
                # 4.3. Cada vuelta, 'i' recibe el número de fila y 'fila' los datos. 'i' es el contador automático de enumerate()
                for i, fila in enumerate(datos, start=1):
                    if i == objetivo:
                        print(f"\n✅ Registro #{i} encontrado:")
                        # Mostramos cada dato con su nombre de columna
                        for col, valor in zip(cabecera, fila):
                            print(f"   {col}: {valor}")
                        encontrado = True
                        break
                
                if not encontrado:
                    print(f"⚠️ El registro {objetivo} no existe. El rango es de 1 a {total_registros}.")

    except FileNotFoundError:
        print(f"❌ Error: No se encontró el archivo '{ruta_archivo}'.")

# Ejecutar el buscador
buscador_interactivo()


sábado, 21 de febrero de 2026

MAV - Audio

Tratamiento del audio. ¿Para qué?

En un mundo en el que los recursos audio-visuales son tan abundantes, donde "una imagen vale más que mil palabras", el audio, a palo seco, parece extremadamente pobre. En principio, pero sólo en principio.

En realidad el audio es la mitad del material AV, pero aparentemente es el pariente pobre: si los efectos visuales no triunfan, el MAV tampoco.

Sin embargo el tratamiento del audio es fundamental como recurso dentro de los modelos DUA, ya que garantizan el correcto acceso al contenido para determinados colectivos, como es el caso de las personas invidentes, pero no sólo.

Otro campo de interés, también dentro del tema de la accesibilidad, es el de los audiolibros y la generación de podcast en base a textos propios o seleccionados personalmente. Hablo de ambos como recursos educativos, sin entrar en su realidad como producto comercial, de los cuales todos podemos ser consumidores.

En el campo específico de los SEO, el uso del audio (en realidad de los MAV) es secundario, por no decir prácticamente inexistente. No acaba de "tener hueco" como recurso de recogida de información dentro del quehacer profesional y su tratamiento posterior es sumamente complejo y costoso.

La causa fundamental de todas estas limitaciones es que nuestra referencia al audio (la de los SEO, quiero decir) es siempre la de su relación con el texto escrito: la transcripción de voz_a_texto. No hace tanto que ésta, en sus dos sentidos, puede considerarse "satisfactoriamente resuelta".

La conversión de texto_a_voz no es especialmente interesante para los SEO, aunque sí lo es para la intervención educativa. Por eso no la voy a abordar aquí. La contraria, de voz_a_texto, sí, y mucho; mucho más de lo que se puede suponer en función de la prácticamente ausencia de uso por parte de los SEO.

Con los consabidos medios (y hoy en día está disponibles), muchas de nuestras actuaciones podrían ser grabadas en audio (incluso en video), permitiendo que el profesional se involucre muchos más activamente en la intervención de lo que le es posible cuando debe atender, además, a la toma de notas. Además nada impide que siga tomando notas en caso de grabar la intervención. En otras ocasiones, esas actuaciones podría agilizarse en cuanto a duración si dejamos a la grabación de audio la responsabilidad de registrar su desarrollo.

Pero si esto no ha sido ni aun hoy en día es así, no lo es por negligencia o apego a la costumbre. Es porque el tratamiento posterior de esos archivos sonoros (o AV) ha sido, hasta hace prácticamente ayer mismo, inabordable por el tiempo de trabajo que consume su transcripción.

Actualmente, sin embargo, contamos con recursos que resuelven satisfactoriamente la conversión de voz_a_texto, lo que pone la información en formato texto a disposición del SEO. Pero con muchas de estas tecnologías nos encontramos con un problema: la exposición de información confidencial en la red no es una opción. Se necesitan medios que permitan realizar transcripciones fiables y seguras en local (off-line).

Afortunadamente en estos disponemos de esos recursos, los cuales nos permiten resolver lo que Balabolka ya hace tiempo que ha resuelto, muy satisfactoriamente, en la conversión inversa (texto_a_voz). De algunos de ellos hablaremos en esta subsección de MEDIOS AV

martes, 17 de febrero de 2026

DATOS

Objetivos del tratamiento de datos

Los datos son la base de todo programa informático, de los clásicos y de los basados en soluciones IA; para estos últimos aun más que para los primeros, puesto que los datos constituyen la base misma sobre la que se sustenta toda su arquitectura.

Tres son las áreas de trabajo que planteamos en esta sección: el acceso a los datos, su limpieza o tratamiento preparatorio y el análisis de datos.

El acceso a datos conlleva mucha más complejidad de lo que en principio se podría pensar, por lo que deberemos dar respuesta a sus diferentes condicionantes, entre los que se incluye la variedad de fuentes y sus implicaciones, el tipos de datos y el modo en que se presentan (no sólo en cuanto al soporte documental, que también), además de la diversidad de objetivos y de planteamientos de trabajo que nos propongamos desarrollar. De todo este conjunto de factores deriva la extensión y el peso de esta temática dentro de la sección.

La limpieza de datos es necesaria para disponer de datos de calidad, lo cual va a permitir el posterior desarrollo de nuestro análisis. En el caso de la automatización de procesos y/o del desarrollo de soluciones basadas en la IA esta calidad de los datos es fundamental, de ahí la importancia del correcto tratamiento preparatorio de nuestros datos.

Finalmente podemos considerar el análisis de datos junto con la automatización de textos, como razón de ser de este blog. En esta tercera subsección trataremos sobre las herramientas y las estrategias que facilitan este análisis. En otra sección plantearé líneas de concreción de estas prácticas adaptadas al trabajo de los SEO.

lunes, 16 de febrero de 2026

TEXTOS

Automatizar la composición de textos

Una de las razones de ser de este blog (posiblemente la principal, al menos en sus inicios) ha sido hacer posiblala semi-automatización de textos. Esto es de esperar dada la ingente cantidad que tiempo que supone para los SEO esta labor.

No voy a ocultar que lo logrado hasta la fecha, siendo mucho, sigue siendo insuficiente. Aun estamos lejos de haber alcanzado el objetivo, al menos de forma satisfactoriamente funcional y generalizable. Aun así es mucho el camino recorrido desde los inicios del blog, e injusto sería no celebrar los avances, que los ha habido y son especialmente interesantes como diversidad de enfoques técnicamente bien resueltos. Esto es especialmente intreresante teniedo en cuenta que la mayor parte del peso de la carga ha sido soportado sobre OOo Basic.

A día de ahoy puedo decir que este camino sigue abierto, y no descarto darle continuidad; pero también es necesario abrir otros horizontes también para profundizar en lo que las últimas propuestas OOo Basic permiten vislumbrar: es necesario incorporar otras herramientas para dar paso a otras perspectiva.

Lo son porque ya están ahí, de algún modo disponibles, pero no del todo; o no del modo en que pueden resultar plenamente satisfactorias. Me estoy refiriendo a las herramientas basadas en la IA.

El uso de la IA en este campo de trabajo resulta ser una solución agridulce por varios motivos; algunos de ellos de orden teórico, pero otros eminentemente prácticos. Estos últimos serán objeto de tratamiento en esta sección. Los primeros, incluyendo la dimensión ética que implican, lo serán en una específica, aunque esto es, a día de hoy, sólo un proyecto.

Lo que no es proyecto, aunque aun carece de desarrollo, es el enfoque de trabajo basado en Python. A día de hoy existen motivos para considerar necesario desarrollar propuestas de automatización basadas en este lenguaje. Lo es porque sólo con él será posible profundizar en algunas las vías de trabajo planteadas en OOo Basic; pero sobre todo porque Python pone a nuestra disposición herramientas que permiten profundizar en las mejores propuestas que nos muestra la IA realmente existente.

lunes, 9 de febrero de 2026

Expedientes

Acceso a archivos

Este se puede considera el proceso inverso a lo que en su momento constituyó la generación automatizada de un documento de acreditación; también se puede entender como fase inicial del análisis de datos. Sea lo uno o lo otro, lo que nos importa es la consecuencia: se trata de generar un procedimiento que nos permita acceder al contenido de determinados documentos.

Pero como de algún modo hay que plantearlo para darle contexto, aprovecho la ocasión y el trabajo ya desarrollado para invertir su lógica y cerrar el círculo: si entonces se trataba de generar masivamente documentos partiendo de una tabla de datos, ahora se pretender procesar automáticamente y en cascada ciertos documentos para acceder a determinados datos y crear una fuente de datos.

Aunque el objetivo no es lo relevante, no está de más decir que se enmarca dentro del seguimiento o análisis de la intervención, que es una forma de análisis de datos adaptado a la acción de los SEO.

Dado que los datos son inventados (escandalosamente inventados), publicar el contenido de los documentos carece de importancia (cualquier parecido es mera coincidencia), incluyendo su uso (a fines comparativos) en Gemini o NotebookLM.

No puedo asegurar que sea así en todos los casos ya que mi experiencia es muy limitada, pero utilizar estas herramientas IA (y también ChatGPT) en el proceso descrito en la entrada antes citada ("Combinar correspondencia"), aunque es posible, sólo relativamente, con limitaciones cuantitativas y nunca exento de posibilidades de error. Esto es cierto al menos en determinadas condiciones, como son en las que yo me encuentro: Gemini y ChatGPT en sus cuentas gratuitas.

Pero si simulo el acceso al contenido de una colección de documentos para obtener determinados datos en NotebookLM los resultados son claramente positivos: es sencillo hacer la consulta y se obtienen resultados fiables y precisos. Cierto que si necesitamos procesar muchos documentos (y muchos no son tantos) deberás hacerlo en varios cuadernos porque no se puede sobrepasar un límite muy moderado (50 archivos en versión usuario, 300 en versión Google Workspace (empresa o educación).

Esta limitación de por sí es un hándicap nada despreciable, pero se puede asumir; lo que no se puede asumir (sí aquí por las características de la "base de datos" que uso) es la vulneración de la confidencialidad de datos que supone subir este tipo de archivos a la red, especialmente a un sistema IA. Esta limitación es radical y nos obliga a desarrollar otro tipo de alternativas si queremos combinar el análisis de nuestros datos con su automatización, lo que con frecuencia equivale a decir simplemente si realmente queremos realizar análisis de datos basados en la documentación disponible en el SEO.

La solución (una de ellas, porque ya sabemos que tampoco en informática hay una única solución) pasa, por ejemplo, por desarrollar un script Python que nos de respuesta al objetivo que nos podamos plantear. El problema: ni es sencillo ni es posible garantizar que lo que sirve para una cuestión sirva para otra, aunque parezca similar.

Es por ello que la propuesta que presento en esta entrada sirve para lo que se propone (y para otros casos) pero no está garantizado que se pueda generalizar. Deberemos plantear otras situaciones y otros objetivos para ir generando un almacén de herramientas de acceso y análisis de documentos. Ahora sólo estamos en los meros inicios.

Rebobino y concreto: tomo los archivos generados automáticamente mediante el script presentado en esta entrada y los convierto en documentación objeto de análisis. En realidad, más que de un análisis se trata de obtener de ellos datos funcionales para, por ejemplo, generar una "agenda de teléfonos" para establecer comunicación con los progenitores. Se requiere, por tanto, acceso al nombre del progenitor y al teléfono de contacto. Ambos son campos disponibles en nuestra base de datos de referencia, pero vamos a situarnos en ausencia de ese documento.

Primero buscaremos el nombre de los familiares. Para ello podemos aplicar este script...



import os
import re
from docx import Document

folder_path = 'Creados'
def extraer_familiar_regex(file_path):
	doc = Document(file_path)
    texto_total = []
    for tabla in doc.tables:
    	for fila in tabla.rows:
        	for celda in fila.cells:
            	t = celda.text.strip()
                if t and (not texto_total or t != texto_total[-1]):
                	texto_total.append(t)
	etiqueta_objetivo = "Nombre y apellidos:"
    contador = 0
	for i, texto in enumerate(texto_total):
    	if re.search(rf"^{etiqueta_objetivo}", texto, re.IGNORECASE):
        	contador += 1
            if contador == 2:
            	if i + 1 < len(texto_total):
                	return texto_total[i + 1]
               	
	return None

# --- Ejecución y muestra por consola ---

print(f"👤 Buscando nombres de familiares en '{folder_path}'...\n")

for filename in os.listdir(folder_path):
	if filename.endswith('.docx') and not filename.startswith('~$'):
    	ruta = os.path.join(folder_path, filename)
        try:
        	nombre_familiar = extraer_familiar_regex(ruta)
            if nombre_familiar:
            	print(f"✅ {filename}: {nombre_familiar}")
            else:
            	print(f"⚠️ {filename}: No se encontró el nombre del familiar.")
		except Exception as e:
        	print(f"❌ Error en {filename}: {e}")
print("\nBúsqueda finalizada. 🎯")


... que nos devuelve el listado de familiares por consola (te animo a que lo pruebes ubicándolo en la raíz del directorio que contiene la carpeta (subdirectorio) Creados, que contiene los documentos que sirve de base para este proyecto y que te dejo para descarga en Documentos.

De modo similar y con resultados parecidos, podemos obtener el dato Teléfonos:



import os
import re
from docx import Document

folder_path = 'Creados'

def extraer_telefonos_flexibles(file_path):
    doc = Document(file_path)
    telefonos_limpios = []
    
    # EXPLICACIÓN DEL PATRÓN (Marca):
    # \b[679]        -> Empieza por 6, 7 o 9 (límite de palabra)
    # (?:[\s.-]?\d)  -> Un número precedido opcionalmente por un espacio, punto o guion
    # {8}            -> Esto se repite 8 veces para completar los 9 dígitos
    # \b             -> Límite de palabra al final
    patron_flexible = r'\b[679](?:[\s.-]?\d){8}\b'
    
    for tabla in doc.tables:
        for fila in tabla.rows:
            for celda in fila.cells:
                texto = celda.text.strip()
                coincidencias = re.findall(patron_flexible, texto)
                
                for tel in coincidencias:
                    # Limpiamos el teléfono para que en el Excel/Consola quede uniforme (sin espacios)
                    # Ejemplo: "600 12 34 56" -> "600123456"
                    tel_limpio = re.sub(r'[\s.-]', '', tel)
                    
                    if tel_limpio not in telefonos_limpios:
                        telefonos_limpios.append(tel_limpio)
                        
    return telefonos_limpios

# --- Ejecución ---
print(f"🧐 Buscando teléfonos con formatos variables en '{folder_path}'...\n")

for filename in os.listdir(folder_path):
    if filename.endswith('.docx') and not filename.startswith('~$'):
        try:
            tels = extraer_telefonos_flexibles(os.path.join(folder_path, filename))
            if tels:
                print(f"✅ {filename}: {tels}")
            else:
                print(f"⚠️ {filename}: No se encontraron teléfonos.")
        except Exception as e:
            print(f"❌ Error en {filename}: {e}")


Aunque el código de ambos es muy interesante (lo que lo trabajaremos en entradas posteriores), no deja de ser una solución poco funcional, ya que lo deseable es que ambas búsquedas se den en el mismo script y que queden recogidas en un soporte fácil de consultar, como puede ser un archivo xlsx, por ejemplo.

Para responder a estas demandas vamos a presentar un script que unifique ambos procedimientos y devuelva ese archivo Excel.



import os
import re
import pandas as pd
from docx import Document

# Configuración
folder_path = 'Creados'
output_file = 'lista_datos2.xlsx'

def procesar_documentos():
    datos_finales = []
    
    # Patrones Regex
    patron_tel = r'\b[679](?:[\s.-]?\d){8}\b'
    etiqueta_nombre = "Nombre y apellidos:"

    print(f"🚀 Iniciando procesamiento de archivos en '{folder_path}'...")

    for filename in os.listdir(folder_path):
        if filename.endswith('.docx') and not filename.startswith('~$'):
            ruta = os.path.join(folder_path, filename)
            try:
                doc = Document(ruta)
                texto_total = []
                
                # 1. Aplanamiento lineal
                for tabla in doc.tables:
                    for fila in tabla.rows:
                        for celda in fila.cells:
                            t = celda.text.strip()
                            if t and (not texto_total or t != texto_total[-1]):
                                texto_total.append(t)
                
                # 2. Extracción de Familiar (2ª ocurrencia)
                nombre_familiar = "No encontrado"
                contador_nombres = 0
                for i, texto in enumerate(texto_total):
                    if re.search(rf"^{etiqueta_nombre}", texto, re.IGNORECASE):
                        contador_nombres += 1
                        if contador_nombres == 2:
                            if i + 1 < len(texto_total):
                                nombre_familiar = texto_total[i+1]
                            break

                # 3. Extracción de Teléfono (el primero que encuentre con Regex)
                # Unimos todo el texto para que Regex busque en todo el documento
                todo_el_texto = " ".join(texto_total)
                coincidencias_tel = re.findall(patron_tel, todo_el_texto)
                
                # Limpiamos el teléfono si existe
                tel_final = "No encontrado"
                if coincidencias_tel:
                    # Tomamos el primero y le quitamos espacios/puntos
                    tel_final = re.sub(r'[\s.-]', '', coincidencias_tel[0])

                # 4. Guardamos en la lista para Pandas
                datos_finales.append({
                    "Archivo": filename,
                    "Familiar": nombre_familiar,
                    "Teléfono": tel_final
                })
                
                print(f"✅ Procesado: {nombre_familiar} -> {tel_final}")

            except Exception as e:
                print(f"❌ Error en {filename}: {e}")

    # 5. Creación del DataFrame y Excel con Pandas
    if datos_finales:
        df = pd.DataFrame(datos_finales)
        df.to_excel(output_file, index=False)
        print(f"\n✨ Proceso completado. Se ha generado '{output_file}' con {len(df)} registros.")
    else:
        print("\n⚠️ No se encontraron datos para guardar.")

if __name__ == "__main__":
    procesar_documentos()
Mostrando lista_fam_tlf.py.


Este script, además de presentar por pantalla el listado de familiares y teléfonos, genera un archivo Excel (.xlsx) que contiene una tabla con esos mismos datos.

Aunque sólo sea por curiosidad, si quieres saber cómo lee Python los archivos docx, este script te muestra el resultado de aplicar las técnicas de simplificación que permiten a Python acceder a contenidos concretos. No se trata de la única opción existente, sólo una de las más sencillas pero potentes, como puedes ver por los resultados.



from docx import Document

file_path = 'Creados/Acredita_1.docx' 

def ver_lista_lineal(ruta):
    try:
        doc = Document(ruta)
        texto_total = []
        
        # 1. Proceso de "aplanamiento" (igual que en tu script original)
        for tabla in doc.tables:
            for fila in tabla.rows:
                for celda in fila.cells:
                    t = celda.text.strip()
                    # Filtro para evitar duplicados por celdas combinadas
                    if t and (not texto_total or t != texto_total[-1]):
                        texto_total.append(t)
        
        # 2. Mostrar el resultado como una lista pura
        print(f"--- LISTA LINEAL DE: {ruta} ---")
        for i, elemento in enumerate(texto_total):
            # Imprimimos el índice para que veas la "posición" de cada dato
            print(f"Posición {i}: {elemento}")
            
    except Exception as e:
        print(f"❌ Error: {e}")

ver_lista_lineal(file_path)


Puedes compara la sucesión de "etiquetas" y "campos" y la diferencia con la apariencia visual del documento .docx. Y es que uno de los problemas con los que nos encontramos a la hora de trabajar en este tipo de proyectos es la complejidad de los documentos objeto de análisis. Esta complejidad se incrementa cuanto tratamos con documentos prescriptivos, formados por una complicada estructura de tablas las cuales han sido modificadas a lo largo del tiempo generando problemas de acceso importantes que explicar las dificultades con las que nos podemos encontrar al tratar de automatizar procedimientos. Algo de esto ya sabemos.

Documentos.

Para finalizar te dejo acceso a los script Python y al listado de documentos sobre los que trabajan. En esta ocasión no aporto un cuaderno Colab porque no me detengo a analizar el código de los script. Son muchos y suficientemente complejos como para hacerlo ahora. Tiempo habrá y en ello colaborará Gemini, auxiliar también en la creación de estas soluciones.

RECUERDA Debes descargar estos documentos en una misma carpeta.

martes, 23 de diciembre de 2025

Evaluación

Automatización de la evaluación


Análisis de la puntuación total mediante OOo Basic



Normalmente lo primero que se analiza de los resultados de la aplicación de un test es el sumatorio de las puntuaciones obtenidas, bien para convertirlas a algún tipo de escala en función de unos estadísticos, bien para compararla con un criterio de nivel de ejecución. 

En ambos casos podemos encontrarnos con datos que tienen un único y posible significado o diferente en función de determinado referente, como puede ser la edad del sujeto o su nivel o curso.

Cuando el significado es único el procedimiento que sigue no se diferencia mucho del expuesto en [esta entrada], aunque empleemos código OOo Basic en lugar de funciones Calc. Pero cuando el significado es diferente en función de, por ejemplo, la edad, previamente podemos que realizar ciertos procesos para automatizar correctamente el análisis y no tener que plantear procedimientos específicos para cada edad o contexto de referencia.

Aunque hay muchos, el test PLON-R. Fonología es un ejemplo de lo que acabo de exponer: una misma puntuación total o directa (PD) tiene significados diferentes en función de la edad. Por ejemplo, obtener 21 ptos es lo esperado para 3 años, ya que indica que el niño pronuncia correctamente todos los fonemas y grupos fonémicos propios de su edad, pero es indicador de déficit para el resto de las edades. Lo mismo sucede con la PD 44, aunque en este caso es indicador de error en la aplicación del test para niños de 3 años (o de alto nivel de desarrollo fonológico), se ajusta a lo esperado para 4 años y es indicador de déficit para edades superiores.

Es por ello que antes de realizar cualquier valoración de la PD, antes deberemos ponerla en relación con la edad (en este caso). La forma de realizar esta operación de forma automatizada, sin que sea necesario explicitar conocimiento de los datos propios del grupo de referencia ni desarrollar procedimientos específicos para cada edad, es trabajar con matrices y con bucles.

Este procedimiento ya [ha sido empleado] en el tratamiento del input con otros objetivos, pero es suficientemente interesante como para que de nuevo le dediquemos nuestra atención.

Partimos del conocimiento de dos datos del sujeto: su edad y la PD que obtuvo, pero también de los posibles valores que pueden tener ambos datos. Los primeros los almacenamos en sendas variables (edad y pd) y los segundos en dos matrices; una para las edades...

Dim edades() As Integer
edades = Array(3,4,5,6)

... y otra para las PD esperadas para cada edad.

Dim ptos_max() AS Integer
ptos_max = Array(21,44,56,61)

Como puedes ver ambas colecciones de datos guardan relación (se emparejan) en función de la posición que ocupan en sus respectivas matrices: edades(0) guarda el valor (años) del primer grupo etario (3) y ptos_max(0) el valor de la PD esperada del primer grupo etario (21).

3 años -> 21 ptos en total

Precisamente en la equivalencia de posiciones de edades y valores PD está la base de automatización; gracias a ella nos podemos olvidar de los valores numéricos y limitarnos a recorrer las matrices con un bucle y a realizar comparaciones condicionales.
  • Primero. Recorremos la matriz mediante For 
For i = 0 To UBound(edades()))
  • Segundo. Localizamos el valor posicional de la edad real del sujeto y la utilizamos como primera condición
For i = 0 To UBound(edades())
If edades(i) = edad Then
    -> [Segunda condición]
End If
Next 
  • Tercero. Cuando el recorrido del bucle hace True la primera condición, lanzamos el segundo análisis condicional sobre el que realizamos la valoración de la pd del sujeto.
If pd = ptos_max(i) Then
Valoracion = "Acierto total"
Else
Valoracion = "Resultado insuficiente"
End If

Esta es la forma más simple en la que se realiza la valoración de la PD, pero posiblemente la menos común. De todas formas, es fundamental, ya que, por muy complejas que sean las demás, todas parten de la relación que establecemos mediante la estructura que hemos explicado.

Evaluación

Automatización de la evaluación

Input incremental (II)




Dado que en la [entrada anterior] establecimos previamente los diferentes soportes de interfaz para la fase input, en esta daremos un paso más y crearemos esa interfaz a partir de los datos de una matriz y en función de la edad. En este caso es así dadas las características de la prueba de referencia (PLON-R. Fonología).

No se trata de un procedimiento totalmente automatizado, ya que la hoja sobre la que crearemos la interfaz y parte de los elementos que contiene han tenido que ser creada previamente, pero lo que ahora nos interesa es la parte nuclear de su contenido: el listado de palabras que deberá producir el alumno y que deberemos evaluar nosotros.

Este modo de resolver la cuestión nos ahorra crear tantas hojas/interfaces como grupos input existan en la prueba. Así en PLON-R. Fonología resolvemos con una lo que según el procedimiento anterior precisaba cuatro. Se eliminan también unos cuantos script asociados al manejo de esas hojas.

Sub CrearLista

Dim oHoja As Object, oCelda As Object
Dim listado() As String

Dim annos As String
Dim edad As Integer

annos = al_edad()
edad = CInt(annos)

If edad > 6 Then
edad = 6
End If

listado() = copiaPal(edad)

'Acceso a la página

oHoja = ThisComponent.getSheets().getByName("ListaPal")

oCelda = oHoja.getCellRangeByName("B2")
oCelda.setString("Palabras")
oCelda = oHoja.getCellRangeByName("C2")
oCelda.setString("Puntos")

For i = 0 To UBound(listado())
oCelda = oHoja.getCellRangeByName("B" & i+3)
oCelda.setString(listado(i))
Next

End Sub

Pasemos ahora a analizar cómo se desarrolla esta propuesta. El primer paso consiste en acceder a la edad del sujeto, ya que ésta es la que determinará el desarrollo del resto. Para ello, desde nuestro script principal, haremos uso de la función, creada en versiones anteriores, que nos permitía acceder a ese datos (annos = al_edad()). 

Function al_edad () As string

Dim oHoja As Object, oCelda As Object
Dim i As Integer
Dim edad As String

'Acceso a Resultados para capturar la edad
oHoja = ThisComponent.getSheets().getByName("Resultados")
oCelda = oHoja.getCellRangeByName("B4")
edad = oCelda.getString

al_edad = edad

End Function

Una vez que disponemos de ese dato (string) lo convertimos a integer (edad = CInt(annos)) para operar con él (If edad > 6 Then -> edad = 6 -> End If), evitando posibles errores en el manejo de los datos de edad cuando ésta supera los 6 años.

El paso siguiente consiste en acceder a la función que nos da acceso al contenido con el que trabajar (listado() = copiaPal(edad)): el listado de palabras que corresponde por edad y que deberemos recuperar de la hoja Matrices.

Function CopiaPal(al_edad As Integer)

'para acceso a objetos
Dim oHoja As Object, oCelda As Object
Dim listaPal () As String

'Para acceso a datos
Dim i As Integer
Dim max As Integer
Dim edades() As Integer
Dim ptos_max() AS Integer

edades = Array(3,4,5,6)
ptos_max = Array(21,44,56,61)

'Calculo y establecimiento del límite superior del listado

For i = 0 To UBound(edades())
If edades(i) = al_edad Then
max = ptos_max(i)
End If
Next 

ReDim listaPal(max-1)

'Acceso a la hoja que contiene la base de datos

oHoja = ThisComponent.getSheets().getByName("Matrices")

For i = 0 To UBound(listaPal())
oCelda = oHoja.getCellRangeByName("I" & i+1)
listaPal(i) = oCelda.getString()
Next

CopiaPal = listaPal()

End Function

Esta función accede al contenido de la hoja Matrices y accede al valor numérico de la extensión del listado que debe crear haciendo uso del parámetro al_edad que le hemos pasado desde el script principal, mediante una analogía entre los valores de posición de la matriz edades y los de la matroz ptos_max.

For i = 0 To UBound(edades())
If edades(i) = al_edad Then
max = ptos_max(i)
End If
Next 

Con ese dato de longitud o límite superior redimensionamos la matriz que contendrá el listado de palabras (ReDim listaPal(max-1)). Mediante un segundo bucle recorremos la columna I y accedemos a las palabras que incluimos como elementos de la matriz (listaPal(i) = oCelda.getString()) que la función devolverá al script principal (CopiaPal = listaPal()).

De regreso a éste, sólo nos queda posicionarnos en la hoja sobre la que escribir los títulos de las columnas...

oCelda = oHoja.getCellRangeByName("B2")
oCelda.setString("Palabras")

... y el listado y proceder a ello mediante el procedimiento ya conocido de asignar contenido a las celdas.

For i = 0 To UBound(listado())
oCelda = oHoja.getCellRangeByName("B" & i+3)
oCelda.setString(listado(i))
Next

Si deseáramos completar el procedimiento y dejar listo los materiales para un uso posterior deberíamos implementar código que capture el resultado de la aplicación del test (puntuación de cada ítem) para trasladarlos a la hoja Resultados, eliminando antes el contenido de la lista de palabras y de la puntuación.

A parte de este posible desarrollo del script, también nos podemos plantear otros que implican establecer un condicionamiento múltiple. Un ejemplo sobre PLON-R. Fonología podría ser seleccionar únicamente los fonemas y no los grupos fonémicos, o sólo los fonemas oclusivos, o sólo los fonemas sonoros... Los ejemplos son múltiples, tantos como variables diferenciadoras hayamos establecido. Además esas subcategorizaciones podemos establecerlas secuencialmente o mediante condicionales anidados. En cualquier caso, lo interesante es que podemos hacer uso del procedimiento básico que hemos explicado en esta entrada para automatizar la selección del contenido sobre el que trabajar en función de los criterios que nos interese aplicar en cada caso.