Archivos de texto
Acceso a contenidos específicos
No tengo muy claro si esta entrada debe situarse en esta subsección (Acceso a datos) o si debería ubicarse en la que sigue (Limpieza de datos), entendida en sentido amplio, pero posiblemente esto no es lo más importante. Lo que interesa es el concepto que se trata en ella y lo que implica. Antes de decir algo más al respecto, es necesario advertir que, en cualquier caso, ahora sólo trataremos estas cuestiones en términos muy básicos y limitados, ya que será necesario retomar el tema cuando hablemos de organización y limpieza de los datos no estructurados y, como no puede ser de otro modo, también en la subsección Uso de datos.
En las entradas anteriores hemos estado tratando cuestiones relativas al acceso a los datos de los textos como un todo, diferenciando si este contenido se encuentra ubicado en tablas o si conforma párrafos más o menos extensos. Esta diferención lo es también de datos vs. textos, o si se preferie (y yo sí lo prefiero) entre datos semi-estructurados frente a datos no estructurados. En sentido estricto, ningun documento contiene datos estructurados, así que lo más parecido a éstos son los datos semi-estructurados que encontramos en las tablas y en las tablas-formulario de los documentos. Pero ahora nos hemos alejado de estos entornos y nos hemos enfrentado al texto en sentido estricto, lo que equivale a decir a los datos no estructurados; y ante estos el mero acceso (y "captura") sólo puede ser una primera (y muy necesaria) fase de un proceso considerablemente complejo que requiere varios subprocesos. Lo que ahora abordamos sería un intento de concretar una opción de posible segunda fase.
Y es que de poco sirve capturar un texto si no obtenemos información de él. Pasa lo mismo cuando hablamos del tratamiento analógico de los textos, pero es mucho más definitorio (y difícil de llevar a cabo) cuando el tratamiento es digital. Podemos concretarlo de muchas formas, así que la que ahora voy a proponer es sólo una de ellas, y no precisamente de las de mayor complejidad.
En el marco del acceso al contenido de los textos de una colección de informes psicopedagógicos, deseamos identificar si en ellos se hace alguna mención al uso del test (batería) WISC. Esto implica interés por saber si este recurso se ha empleado, con qué peso cuantitativo en la muestra documental y qué información se asocia con ese uso.
El objetivo simple, identificar uso vs. no-uso del WISC, no parece ofrecer mayor dificultad, pero acceder al contenido textual asociado a esta etiqueta conlleva cierto nivel de complejidad, que nos proponemos abordar de la forma más directa y simple posible, lo que no garantiza que sea la mejor ni de mayor fiabilidad, aunque según cual sea nuestro objetivo puede ser suficiente. Sí es claramente suficiente para el objetivo que nos planteamos en esta entrada, en el marco de la temática que tratamos en la subsección en la que la ubicamos.
import os
import csv
from odf.opendocument import load
from odf import text
# --- Función secundaria 1: Extracción lineal de párrafos ignorando tablas. ---
def extraer_parrafos_no_tablas(ruta_archivo):
try:
doc = load(ruta_archivo)
parrafos_limpios = []
for p in doc.getElementsByType(text.P):
parent = p.parentNode # Filtro de tablas por ancestros
dentro_de_tabla = False
while parent is not None:
if parent.tagName == "table:table":
dentro_de_tabla = True
break
parent = parent.parentNode
if not dentro_de_tabla:
contenido = "".join(node.data for node in p.childNodes if node.nodeType == 3) # Unimos todos los nodos de texto
if contenido.strip():
parrafos_limpios.append(contenido.strip())
return parrafos_limpios
except Exception as e:
print(f" [!] Error de lectura en {os.path.basename(ruta_archivo)}: {e}")
return []
# --- Función secundaria 2: Captura el párrafo del hallazgo y los N siguientes para asegurar el contexto. . ---
def extraer_segmento_contiguo(lista_parrafos, termino, num_posteriores=4):
segmentos_completos = []
indices_ya_incluidos = set()
for i, texto in enumerate(lista_parrafos):
if termino.lower() in texto.lower() and i not in indices_ya_incluidos:
rango_fin = min(i + num_posteriores + 1, len(lista_parrafos)) # Iniciamos bloque
bloque = lista_parrafos[i:rango_fin]
for j in range(i, rango_fin): # Marcamos para no repetir si el término aparece en los párrafos de este bloque
indices_ya_incluidos.add(j)
segmentos_completos.append("\n\n".join(bloque))
return segmentos_completos
# --- Función principal ---
def procesar_informes(dir_entrada, csv_salida, palabra_clave):
if not os.path.exists(dir_entrada):
print(f"La ruta {dir_entrada} no existe.")
return
archivos = [f for f in os.listdir(dir_entrada) if f.lower().endswith('.odt')]
resultados = []
print(f"Procesando {len(archivos)} archivos...\n")
for nombre in archivos:
ruta = os.path.join(dir_entrada, nombre)
parrafos = extraer_parrafos_no_tablas(ruta) # Llamada a función secundaria 1
hallazgos = extraer_segmento_contiguo(parrafos, palabra_clave, num_posteriores=4) # Llamada a función secundaria 2
estado = "PRESENTE" if hallazgos else "AUSENTE"
print(f"[{estado}] {nombre}")
resultados.append({
'Archivo': nombre,
'Hallazgo': estado,
'Texto': " |SECCIÓN SIGUIENTE| ".join(hallazgos) if hallazgos else "N/A"
})
try:
with open(csv_salida, mode='w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['Archivo', 'Hallazgo', 'Texto'])
writer.writeheader()
writer.writerows(resultados)
print(f"\nCSV generado con éxito en: {csv_salida}")
except Exception as e:
print(f"Error al escribir CSV: {e}")
# --- Llamada a función ---
if __name__ == "__main__":
RUTA_ARCHIVOS = r"ruta_de_directiro_de_documentos" # Ruta de acceso a la colección de documento
RUTA_CSV = r"ruta_destino_y_nombre_archivo.csv" # Ruta para ubicación de csv e identificador del archivo
TERMINO = "WISC" # Término objeto de estudio
procesar_informes(RUTA_ARCHIVOS, RUTA_CSV, TERMINO)
Lo que hacemos con este script es, en primer lugar prescindir de las tablas del documento y de los datos que éstas contienen para centrarnos en la extracción de los textos (párrafos). Dentro de ellos, buscamos la aparición del término-diana (aquí WISC) y lo relacionamos con su contexto (texto inmediatamente próximo al término-diana) para capturar el contenido que se asocia al mismo- Esto nos permite acceder no sólo a información sobre la presencia vs. ausencia del término-diana en el documento, sino también al contenido asociado a dicho término, lo que supone una mayor complejidad del procedmiento de automatización, pero también que el script es capaz de facilitar información que incrementa significativamente nuestra capacidad de análisis de los documentos. No estamos en condiciones para asegurar que esto sea suficiente para objetivos complejos, pero sí para muchos otros más simples. En todo caso sólo estamos ante un procedimiento básico. En otras subsecciones trataremos cómo desarrollar otras.