jueves, 26 de marzo de 2026

Expedientes

No-Expedientes (II)

Hemos aprendido mucho del análisis de lo que hemos venido a llamar no-expedientes de archivo único, pero el siguiente grupo de directorios (no-expedientes de dos archivos) requiere nuevos planteamientos de análisis.

Conocemos por la fase previa de este análisis que son 64 los directorios que contienen dos archivos y que el número de archivos se eleva a 128, pero desconocemos de qué tipo son estos archivos y en cuantos de los no-expedientes está presentes. Estos fueron cálculo que obtuvimos para los no-expedientes de un archivo y no veo motivo para no aplicarlos también a este nuevo subconjunto: además de obtener resultados de interés (respecto a la tipología documental), también podremos aplicar el mismo criterio que en el grupo anterios e identificar cual es la incidencia de esas tipologías no-procesador que en su momento consideramos indicadores fallidos de generación de expedientes. Para ello necesitamos realizar una asociación entre tipos de archivos y directorios.

El modo en que esto se ha concretado implica procedimientos de mayor complejidad que en el caso de los directorios con un único archivo, ya que es ncesario proceder a diferenciar los directorios en dos subgrupos, dado que sólo deberemos trabajar con uno de ellos para el análisis subsiguiente. Pero no avancemos acontecimiento antes de tiempo.

Primero debemos obtener el listado de los archivos del subconjunto (los 128 archivos) e información sobre su tipología, datos que te aporto en la tabla que va a continuación:

Tipología Número Porcentaje
.doc 79 61.7
.docx 9 7
.odt 7 5.4
Procesador 95 74.2
PDF 28 21.8
H. cálculo 5 3.9
No procesador 33 25.8

Aunque no es tiempo aun para este tipo de análisis, no me resisto a decir que respecto a los directorios de un único archivo se observa ahora un incremento porcentual del peso de los archivos que no derivan del uso del procesador de texto (pasamos de suponer el 9% del total de archivos a alcanzar cerca del 26%) lo que lógicamente tendrá una incidencia importante en el número de directorios que podremos considerar expedientes fallidos y no meros no-expdientes. Pero para obtener datos al respecto necesitamos que nuestro script nos proporcione información directa sobre la indidencia y reparto de esos archivos (.pdf y H. de cálculo) en el conjunto de directorios de dos archivos.

La segunda fase consiste precisamente en realizar esa asociación entre archivos y expedientes, pero necesitamos algo más que los datos, ya que posteriormente deberemos trabajar de forma diferenciada y específica con el subgrupo de aquellos no-expedientes que no contienen no-Procesadores. Es por ello que generamos un script que, además de identificar de qué archivo se trata (según extensión), identifique el directorio en que se encuentra y lo traslade a un directorio específico.

El resultado de este proceso son dos directorios que contienen respectivamente 26 expedientes (que podemos identificar como expedientes fallidos por contener documentos no-Procesador) y 38 no-expedientes (que contienen archivos Procesador exclusivamente. Esto supone que el 43.75% de los directorios (inicialmente no-expedientes) de dos archivos no entran dentro del grupo predominante (90,9%) de los no-expdientes de un archivo. Evidentemente un cambio muy significativo que posiblemente requiera de un análisis complementario (¿qué son en realidad los archivos .pdf?, ¿se pueden considerar directamente como archivos-con-intención-de-conformar-expediente?); aunque no nos tiene que hacer olvidar que en este segundo subgrupo siguen predominando (cerca del 60%) los no-expedientes. ¿O no?

Esa es la tercera cuestión de interés por ser la que más nos hace avanzar en el procedimiento de análisis y su automatización. Para que nos situemos: inicialmente habíamos considerado que cierta agrupación de cierto tipo de archivos podría estar indicando indicios de pre-expedientes que no podríamos considerar expedientes fallidos (por carecer de determinada composición, la antes vista, en la que no constan los informes psicopedagógicos) pero tampoco como no-expediente por observarse interés por recopilar precisamente esos documentos (los informes psicopedagógicos), que no son suficientes pero sí necesarios para que un grupo de documentos pase a constituirse en expediente.

Para facilitar el desarrollo de este análisis antes diferenciamos físicamente dos subconjuntos de directorios y ahora trabajaremos sólo sobre uno de ellos: el que contiene únicamente directorios ("expedientes) con archivos .doc, .docx y .odt. Esta disparidad tipológica, aunque reducida, sigue siendo un inconveniente para automarizar el análisis, así que vamos a realizar una copia de ese directorio y, sobre ellas, aplicaremos un procedimiento para convertir todos los archivos en .docx.



#Bibliotecas necesarias ---------------------------------------------------

import os
import subprocess
import pathlib

#Función -------------------------------------------------------------------

def convertir_y_limpiar(ruta_absoluta):
    # Configuración de ruta
    RUTA_LIBREOFFICE = r'C:\Program Files\LibreOffice\program\soffice.exe'
    
    if not os.path.exists(RUTA_LIBREOFFICE):
        print(f"Error: No se encontró LibreOffice en {RUTA_LIBREOFFICE}")
        return

    extensiones_a_convertir = {'.doc', '.odt'}
    
    if not os.path.exists(ruta_absoluta):
        print(f"Error: La ruta {ruta_absoluta} no existe.")
        return

    print(f"Procesando archivos en: {ruta_absoluta}...\n")
    
    for raiz, dirs, archivos in os.walk(ruta_absoluta):
        for nombre_archivo in archivos:
            ruta_original = pathlib.Path(os.path.join(raiz, nombre_archivo))
            ext = ruta_original.suffix.lower()

            # 1. Ignorar si ya es .docx
            if ext == '.docx':
                continue

            # 2. Procesar solo .doc y .odt
            if ext in extensiones_a_convertir:
                # Definimos cómo se llamaría el nuevo archivo
                ruta_nuevo_docx = ruta_original.with_suffix('.docx')
                
                print(f"Transformando: {nombre_archivo} -> {ruta_nuevo_docx.name}")
                
                try:
                    # Ejecutar conversión
                    resultado = subprocess.run([
                        RUTA_LIBREOFFICE, 
                        '--headless', 
                        '--convert-to', 'docx', 
                        '--outdir', str(ruta_original.parent),
                        str(ruta_original)
                    ], check=True, capture_output=True)

                    # 3. Verificación de seguridad: ¿Existe el nuevo archivo?
                    if ruta_nuevo_docx.exists():
                        # Eliminamos el original solo si el nuevo ya existe
                        os.remove(ruta_original)
                        print(f"   [OK] Convertido y eliminado original.")
                    else:
                        print(f"   [!] Error: No se encontró el archivo convertido {ruta_nuevo_docx.name}")

                except Exception as e:
                    print(f"   [X] Error procesando {nombre_archivo}: {e}")

    print("\n¡Limpieza y conversión completadas!")

#Script de aplicación de función ---------------------------------------------------------------------

if __name__ == "__main__":
    mi_ruta = "Mi_Ruta"
    convertir_y_limpiar(mi_ruta)


Mediante este script, y usando la utilidad de conversión de LibreOffice RUTA_LIBREOFFICE = r'C:\Program Files\LibreOffice\program\soffice.exe', accedemos a cada uno de los archivos de la ruta mi_ruta = "Mi_Ruta" que pasamos como parámetro a la función convertir_y_limpiar(mi_ruta), identificamos su extensión y convertimos a docx ruta_nuevo_docx = ruta_original.with_suffix('.docx'). Gracias a ello, a partir de este momento podemos aplicar la lógica de análisis, que se basa parcialmente en el uso de la biblioteca python-docx. Explico primero la lógica y después muestro y comento brevemente el script.

Después de analizar posibilidades opto por una combinación de procedimientos que puede resultar restrictiva, pero que evitará falsos positivos, lo que constituye el objetrivo prioritario en este momento: se trata de seleccionar aquellos no-expedientes que pudieran ser expedientes-incipientes, habiendo definido (en su momento) como tales aquellos en los que su creador recogió los documentos indispensables pero insuficientes para condicionar que un directorio o carpeta pueda ser considerado como expediente SEO cuando cuenta con el número mínimo de archivos. Todo esto, para directorios de dos archivos se concreta del siguiente modo: los dos archivos son informes psicopedagógicos.

La consecuencia que deriva del planteamiento anterior es que tenemos que identificar aquellos directorios que contienen dos archivos-procesador (ahora ambos .docx) cuyo contenido sea un informe psicopedagógico. Sólo en ese caso, sólo con esa combinación de contendio, un directorio pasa de la categoría de no-expedientes a la de expediente-incipiente; tercera categoría en que se pueden dividir los directorios-expedientes de dos archivos.

Para identificar estos directorios he pensado que sería útil analizar primero el nombre del archivo, ya que es común que los informes psicopedagógicos lleven la secuencia "inf" , "info" o "informe" en su nombre; pero esto no es definitorio, ya que otros documentos también llevan esa secuencia sin ser informes psicopedagógicos en sentido estricto. Por ello es necesario ir más allá del análisis del título y acceder al contenido del documento (para lo es que necesaria la biblioteca python-docx) y confirmar la presencia de cierto contenido que sea propio de un informe psicopedagógico.

Este segundo paso, que sólo se ejecuta si se cumple el primer criterio (lo que puede resultar demasiado restictivo) supone dar un salto muy importante en el acceso al contenido de un documento y debemos ser cautos y comedidos al darlo, ya que se abren muchas posibilidades y ahora sólo nos interesan unos mínimos, aunque claros y suficientes. Esa cautela implica que opte por identificar en el encabezado y/o título del documento una expresión que resulte prototípica de un informe psicopedagógico, como es precisamente ese misma secuencia de palabras. Como veremos, la prueba de esta fórmula resultó adecuada, pero fue necesario corregirla para evitar falsos negativos, ya que un número relativamente importante de informes tienen un título ligeramente diferente: Informe de Evaluación Psicopedagógica, por lo que fue necesario ampliar los referentes a buscar. Este es el script:



import os
import docx
import re
import unicodedata
from pathlib import Path

def normalizar(texto):
    """Elimina tildes y pasa a minúsculas para evitar fallos de coincidencia."""
    texto = unicodedata.normalize('NFD', texto)
    return "".join(c for c in texto if unicodedata.category(c) != 'Mn').lower()

def procesar_archivos(ruta_raiz):
    patrones_nombre = ['inf', 'info', 'informe']
    # Expresión regular: 'informe' seguido de cualquier cosa (.*?) y luego 'psicopedagogic'
    # El 'ic' al final cubre psicopedagógico, psicopedagógica, psicopedagógicos...
    patron_contenido = r"informe.*?psicopedagogic"
    
    conteo_por_carpeta = {}
    
    print(f"Buscando informes en: {ruta_raiz}\n")

    for raiz, dirs, archivos in os.walk(ruta_raiz):
        for nombre in archivos:
            if not nombre.lower().endswith('.docx'):
                continue
            
            # 1. Filtro por nombre de archivo
            if any(p in nombre.lower() for p in patrones_nombre):
                try:
                    doc = docx.Document(os.path.join(raiz, nombre))
                    # Unimos los primeros 10 párrafos (título/encabezado) en un solo bloque
                    texto_inicio = " ".join([p.text for p in doc.paragraphs[:10]])
                    texto_ready = normalizar(texto_inicio)
                    
                    # 2. Filtro por contenido (incluye 'de evaluación' automáticamente)
                    if re.search(patron_contenido, texto_ready):
                        print(f"[OK] ENCONTRADO: {nombre}")
                        if raiz not in conteo_por_carpeta:
                            conteo_por_carpeta[raiz] = []
                        conteo_por_carpeta[raiz].append(nombre)
                except:
                    print(f"[!] Error leyendo: {nombre}")

    # Escribir el informe TXT final
    ruta_txt = Path(__file__).parent / "carpetas_con_dos_informes.txt"
    with open(ruta_txt, "w", encoding="utf-8") as f:
        f.write("LISTADO DE CARPETAS CON EXACTAMENTE 2 INFORMES\n")
        f.write("="*50 + "\n")
        for carpeta, lista in conteo_por_carpeta.items():
            if len(lista) == 2:
                f.write(f"Directorio: {carpeta}\n")
                f.write(f"Archivos: {', '.join(lista)}\n\n")

    print(f"\nProceso finalizado. Informe guardado en: {ruta_txt}")

if __name__ == "__main__":
    ruta = input("Introduce la ruta absoluta de los archivos: ").strip().replace('"', '')
    procesar_archivos(ruta)


Primero aseguramos que se cumple el criterio 1: que el nombre del archivo indique que puede tratarse de un informe. Para ello analizamos si esos términos patrones_nombre = ['inf', 'info', 'informe'] forman parte del nombre del archivo...

... y condicionamos if any(p in nombre.lower() for p in patrones_nombre) el análisis de la segunda condición al cumplimiento de la primera. Esta segunda condición (que el texto contenga determinadas palabras) requiere de instrucciones más complejas...


# Unimos los primeros 10 párrafos (título/encabezado) en un solo bloque
texto_inicio = " ".join([p.text for p in doc.paragraphs[:10]])
texto_ready = normalizar(texto_inicio)

# 2. Filtro por contenido (incluye 'de evaluación' automáticamente)
if re.search(patron_contenido, texto_ready):

... lo que da muestra de la complejidad que conlleva el estudio del contenidos de un documento no estructurado; pero esta es otra cuestión: para lo que ahora nos intereresa, la solución encontrada es suficiente.

Suficiente y funcional, ya que nos permite localizar todos los archivos que contienen una expresión competible con que sean un informe psicopedagógico y confirmar que efectivamente lo son, o que no lo son. El resultado en términos de expedientes es el esperado: sólo cuatro de los 38 pueden ser considerados expedientes-incipientes; tres son identificados por el script de automatización y uno resulta del análisis visual de una coincidencia que indica el listado de los archivos potenciales candidatos a informe.

Aunque es posible que los datos no sean totalmenten correctos, principalmente por quedar pendiente el análisis de los archivos .pdf, el procedimiento seguido, apoyado en Python sí lo es y los resultados obtenidos se aproximan mucho a los esperados:

Concepto Valores Porcentajes
Directorios con dos archivos 64 100%
Expedientes fallidos 26 40.6%
No-expedientes 34 53.1%
Expedientes-incipientes 4 6.3

En cierto modo sorprende la importancia cuantitativa del grupo de expedientes fallidos (cerca del 40%), debido en gran medida al peso de los archivos .pdf, lo que sugiere que puede ser necesario analizarlos de forma específica (pero no en estos momentos), pero sí es ajustado a pronóstico la escasa incidencia de los expedientes incipientes (6% del total), lo que confirma como correcta la consideración inicial de este bloque de directorios como no-expedientes y su aislamiento (cuanto menos para un análisis diferencial) del conjunto de potenciales expedientes SEO. Toda esta coherencia se basa en el criterio cuantitativo: es necesario un mínimo para que un expedientes sea algo más que una colección de documentos; a partir de ese mínimo, además, deben cumplirse criterios de tipología y relevancia para que se confirmen como tales expedientes; pero ese mínimo es fundamental.

Precisamente en grupo de no-expdientes que nos falta por analizar es el que se encuentra en el límite del criterio cuantitativo. Esperemos a ver qué resulta de su análisis en una próxima entrada.