Tablas complejas (.odt) (VI)
Optención automátizada de todos los datos de las tablas del documento
Con esta entrada finalizo la presentación de script de recuperación de datos semi-estructurados de tablas-formulario soportadas sobre archivos .odt. En este caso vamos a generar un procedimiento cuyo objetivo es obtener todos los datos de un conjunto de tablas del mismo documento, en teoría de todos los datos de todas las tablas, aunque en la práctica no es necesario y la mayoría de las veces puede que tampoco conveniente, abarcarlas todas.
Puede considerarse que este script es complementario del expuesto en la entrada anterior, y lo es en cuanto que ahora lo que pretendemos es obtener todos los datos de un único documento, mientras que antes lo que pretendíamos era obtener los mismos datos de muchos documentos diferentes. La cuestión es que este cambio de objetivo tiene varias implicaciones: en lugar de dirigir nuestros esfuerzos al estudio de una realida concreta y su manifestación colectiva en documentos individuales; ahora pretendemos recopilar exhaustivamete los datos de un único documento, aunque estos documentos comparten contener tablas-formulario y no tablas de datos en sentido estricto. De ahí que estemos hablando de datos semi-estructurados, frente a los datos estructurados que presentan las grandes (aunque no necesariamente, sí con frecuencia) bases de datos, como las que se manejan en la minería de datos y en la IA.
Una diferencia fácilmente observable que deriva del nuevo planteamiento es que antes el objetivo era obtener un archivo CSV por cada tabla que se analiza en la que cada registro (fila) es el contenido de dicha tabla en un documento; ahora vamos a generar un archivo .xlsx que contiene varias hojas, una por cada tabla del documento .odt. La elección de este soporte está justificada por nuestro interés por mantener todos los datos en un único archivo, y no tenerlos dispersos en varios, como obligaría el uso del formato csv. Esto facilita el manejo de los archivos, su consulta y posterior manipulación desde el servicio excel o Calc.
# --- 0. BIBLIOTECAS ---
import os
import pandas as pd
from odf.opendocument import load
from odf.table import Table, TableRow, TableCell
from odf import teletype
# --- 1. MOTOR DE LIMPIEZA ---
def limpiar_texto_odt(texto_bruto):
if not texto_bruto: return ""
texto = texto_bruto.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ')
return " ".join(texto.split()).lower()
# --- 2. TRADUCTOR DE COORDENADAS ---
def obtener_referencia_celda(fila_idx, col_idx):
letras = ""
temp_col = col_idx
while temp_col >= 0:
letras = chr(65 + (temp_col % 26)) + letras
temp_col = (temp_col // 26) - 1
return f"{letras}{fila_idx + 1}"
# --- 3. ANALIZADOR DE ESTRUCTURA (MEJORADO) ---
def extraer_datos_tabla(tabla):
filas_xml = tabla.getElementsByType(TableRow)
if not filas_xml: return [], [], "0x0"
max_cols = 0
for celda in filas_xml[0].getElementsByType(TableCell):
span = int(celda.getAttribute("numbercolumnsspanned") or 1)
max_cols += span
total_filas = len(filas_xml)
# MODIFICACIÓN (1): Generar el dato de estructura
tipo_tabla = f"{total_filas}x{max_cols}"
ocupada = [[False for _ in range(max_cols)] for _ in range(total_filas)]
ids, contenidos = [], []
for r_idx, fila in enumerate(filas_xml):
celdas_xml = fila.getElementsByType(TableCell)
c_xml_cursor = 0
for c_idx in range(max_cols):
if ocupada[r_idx][c_idx]: continue
if c_xml_cursor < len(celdas_xml):
celda_actual = celdas_xml[c_xml_cursor]
c_span = int(celda_actual.getAttribute("numbercolumnsspanned") or 1)
r_span = int(celda_actual.getAttribute("numberrowsspanned") or 1)
texto = teletype.extractText(celda_actual).strip()
ids.append(obtener_referencia_celda(r_idx, c_idx))
contenidos.append(texto)
for i in range(r_span):
for j in range(c_span):
if r_idx + i < total_filas and c_idx + j < max_cols:
ocupada[r_idx + i][c_idx + j] = True
c_xml_cursor += 1
return ids, contenidos, tipo_tabla
# --- 4. FUNCIÓN MAESTRA EVOLUCIONADA ---
def ejecutar_extraccion_a_excel(ruta_odt, mapa_claves):
if not os.path.exists(ruta_odt):
print(f"[!] El archivo no existe en: {ruta_odt}"); return
directorio_salida = r"C:\PROCESAMIENTO_DATOS_SEO\TABLAS_DOC"
if not os.path.exists(directorio_salida):
os.makedirs(directorio_salida, exist_ok=True)
doc = load(ruta_odt)
tablas = doc.body.getElementsByType(Table)
nombre_base = os.path.splitext(os.path.basename(ruta_odt))[0]
ruta_excel = os.path.join(directorio_salida, f"{nombre_base}_EXTRAIDO.xlsx")
# Control de duplicados por clave normalizada
claves_encontradas = {limpiar_texto_odt(item['termino']): False for item in mapa_claves}
with pd.ExcelWriter(ruta_excel, engine='openpyxl') as writer:
for t in tablas:
filas = t.getElementsByType(TableRow)
if not filas: continue
celdas_xml = filas[0].getElementsByType(TableCell)
if not celdas_xml: continue
# MODIFICACIÓN (2): Comprobar contra el mapa de claves y su posición (0 o 1)
for configuracion in mapa_claves:
termino_original = configuracion['termino']
posicion_busqueda = configuracion['posicion'] # 0 o 1
termino_norm = limpiar_texto_odt(termino_original)
# Seguridad: verificar que la tabla tenga la celda solicitada
if len(celdas_xml) <= posicion_busqueda: continue
valor_celda_ancla = limpiar_texto_odt(teletype.extractText(celdas_xml[posicion_busqueda]))
if termino_norm in valor_celda_ancla and not claves_encontradas[termino_norm]:
ids, valores, tipo_tabla = extraer_datos_tabla(t)
# Insertar tipo_tabla como primer campo (Punto 1)
final_ids = ["tipo_tabla"] + ids
final_valores = [tipo_tabla] + valores
df = pd.DataFrame([final_valores], columns=final_ids)
nombre_hoja = termino_original[:30]
df.to_excel(writer, sheet_name=nombre_hoja, index=False)
claves_encontradas[termino_norm] = True
print(f"[OK] Tabla '{termino_original}' ({tipo_tabla}) guardada.")
break
print(f"\nPROCESO COMPLETADO. Ruta: {ruta_excel}")
# --- Llamada a la función principal ---
if __name__ == "__main__":
ARCHIVO_TRABAJO = r"" #Introduce aquí la ruta absoluta del documento a procesar
MAPA_CLAVES = [ # Lista de diccionarios con término y posición (0=A1, 1=B1). Adáptala a tus necesidades
{'termino': 'fecha', 'posicion': 0},
{'termino': 'nie', 'posicion': 0},
{'termino': 'escolarizado', 'posicion': 1},
{'termino': 'admisión', 'posicion': 0},
{'termino': 'padre', 'posicion': 0},
{'termino': 'anteriores', 'posicion': 0},
{'termino': 'nueva', 'posicion': 1},
{'termino': 'discapacidad', 'posicion': 0},
{'termino': 'ordinario', 'posicion': 1},
{'termino': 'tipo', 'posicion': 0},
{'termino': 'personales', 'posicion': 0}
]
ejecutar_extraccion_a_excel(ARCHIVO_TRABAJO, MAPA_CLAVES)
Obsérvese que este script comparte la mayor parte de la estructura y la lógica con el precedente, pero presenta una diferencia de mucha importancia para el funcionamiento del script: el uso de la lista de diccionarios MAPA_CLAVES = [] que contiene la colección de términos de referencia para identificar la tabla y el código que establece la celda en la que buscar el término. La lista que aquí se presenta debe ser adaptada a las necesidades concretas que derivan del documento a procesar.
A pesar de que ahora obtenemos un .xlsx, tampoco obtenemos unas tablas que se puedan manejar directamente, siendo necesario procesarlas desde el servicio de hoja de cálculo que se desee o mediante código, aunque presentan una formulación compatible con lo que consideramos datos estructurados, lo que no impide que sea necesario organizarlos y proceder a su limpieza, si se da el caso.
Por último debo repetir aqui lo que dije al presentar el script complementario visto en su momento respecto al uso de la IA y a sus condiciones y limitaciones.
No hay comentarios:
Publicar un comentario
Comenta esta entrada