Mostrando entradas con la etiqueta docx. Mostrar todas las entradas
Mostrando entradas con la etiqueta docx. Mostrar todas las entradas

martes, 5 de mayo de 2026

DATOS. Archivos de texto

Archivos .docx

Acceso a tablas y textos

Dado que ya hemos hablado de los archivos de texto .docx (por ejemplo, aquí), resulta un poco complicado plantear esta entrada sin que resulte repetitiva, aunque el intento no carece de sentido. En aquel momento hablamos de los documentos .docx desde una perspectiva de generación de los mismos (aunque se trataran también otros temas), y ahora nuestro interés se centra en la obtención de datos de documentos ya creados, fundamentalmente datos textuales y contenidos contenidos en tablas, lo que equivale a decir datos no estructurados y datos que aspiran a ser considerados como datos estructurados, pero que aun no alcanzan este nivel de organización (datos semi-estructurados).

La biblioteca python-docx, ya presentada en relación con la temática antes señalada será también la herramienta que nos ayude a acceder al contenido de los documentos .docx, que son una evolución compleja de los .doc primitivos, implementando la arquitectura XML, lo que facilita el manejo del contenido mediate script Python, que se hace precisamente manejando esos componentes xml.

A nosotros lo que realmente nos importa ahora, desde la perspectiva de esta subsección, es el acceso al contenido, diferenciando si este se presenta en formato de tabla (mejor tabla-formulario), que, como dije, se considera como dato semi-estructurado, aunque no siempre lo sea, frente al texto no-tabla (párrafos), que se consideran datos no estructurados. Respecto a las tablas, dado que también se emplean para formatear la presentación del contenido, en algunas ocasiones éste no es en realidad un dato semi-estructurado; se trata de un segmento textual no estructuardo y como tal debe ser tratado. A veces esta diferencia es de detalle, lo que da lugar a confusiones en el planeamiento de cómo debe ser tratado en los procesos de limpieza y de análisis.

No es esta una cuestión que sólo afecte a los documentos .docx, pero lo trataremos aquí para avanzar propuestas, dado que es una cuestión de importancia dentro de la subsección en la que nos encontramos en este punto.Pero antes de abordar esta cuestión resolvamos primero las más básicas: el acceso al contenido en su conjunto y el acceso al contenido de forma diferenciada: tablas y textos.



import docx
import os

def leer_contenido_docx(ruta_archivo):
    if not os.path.exists(ruta_archivo):			    		# Verificar si el archivo existe
        print(f"Error: El archivo '{ruta_archivo}' no existe.")
        return
    doc = docx.Document(ruta_archivo)					  		# Cargar el documento

    print(f"--- INICIO DEL CONTENIDO: {ruta_archivo} ---\n")
    
    for elemento in doc.element.body:							# Acceso secuencial a todos los elementos del cuerpo del documento
        if isinstance(elemento, docx.oxml.text.paragraph.CT_P): # 1. Si el elemento es un párrafo (CT_P)
            p = docx.text.paragraph.Paragraph(elemento, doc)
            if p.text.strip(): # Evitar mostrar líneas vacías si se desea
                print(f"[PÁRRAFO]: {p.text}")
        elif isinstance(elemento, docx.oxml.table.CT_Tbl):		# 2. Si el elemento es una tabla (CT_Tbl)
            tabla = docx.table.Table(elemento, doc)
            print(f"\n[TABLA INICIO - {len(tabla.rows)} filas x {len(tabla.columns)} columnas]")
            for fila in tabla.rows:
                contenido_fila = [celda.text.strip() for celda in fila.cells]  # Extraemos el texto de cada celda
                print(" | ".join(contenido_fila))
            print("[TABLA FIN]\n")

    print(f"\n--- FIN DEL DOCUMENTO ---")
    
# --- Llamada a la función ---

if __name__ == "__main__":
    nombre_archivo = r"Directorio_de_mi_archivo.docx" 	# Aquí la ruta de tu_archivo.docx'
    leer_contenido_docx(nombre_archivo)


Este script permite acceder a un documento .docx, identifica su contenido, diferenciando tablas de párrafos y muestra todo ese contenido en el CMD señalando la diferencia entre el procedente de una tabla del identificado como párrafo. Esta diferenciación ya señala la posibilidad de acceder a datos semi-estructurado (tablas y tablas-formulario) y a datos no estructurados (párrafos) de forma diferenciada. El siguiente script permite el acceso a las tablas del documento y a su contenido. A partir de aquí podríamos desarrollar procedimientos específicos de acceso a datos concretos. La idea a posteriori es facilitar la transformación de estos datos semi-estructurados en datos estructurados, para tratarlo con los procedimiento del organización y limpieza, y de análisis acordes con los datos de esa naturaleza.



import docx
import os

#Función principal: exploración de las tablas del documento

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

    doc = docx.Document(ruta_archivo)

    print(f"--- RELACIÓN DE TABLAS ENCONTRADAS ---")
    
    tablas_limpias = []
    
    for i, tabla in enumerate(doc.tables):
        # Extraemos el nombre/título desde las propiedades XML de la tabla (tblPr)
        nombre_tabla = None
        try:
            # Buscamos en el elemento XML de la tabla el tag de título (tblCaption)
            nombre_tabla = tabla._element.xpath('.//w:tblCaption/@w:val')
            if nombre_tabla:
                nombre_tabla = nombre_tabla[0]
            else:
            # Intentamos con la descripción si el título falla
                nombre_tabla = tabla._element.xpath('.//w:tblDescription/@w:val')
                if nombre_tabla: nombre_tabla = nombre_tabla[0]
        except:
            nombre_tabla = None

        if not nombre_tabla:
            nombre_tabla = f"Tabla_{i+1}"
        
        tablas_limpias.append((nombre_tabla, tabla))
        print(f"ID: {i+1} | Nombre en Navegador: {nombre_tabla}")
    
    print("-" * 40 + "\n")

    for nombre, tabla in tablas_limpias:		# Mostramos el contenido de las tablas
        print(f"\n[CONTENIDO TABLA]: {nombre}")
        print("-" * (18 + len(nombre)))
        
        for fila in tabla.rows:
            textos_celdas = []			# Usamos un set para evitar celdas duplicadas en caso de celdas combinadas
            for celda in fila.cells:
                texto = celda.text.strip().replace('\n', ' ')
                textos_celdas.append(texto) 
            print(" | ".join(textos_celdas))
        print("-" * 20)
        
# --- Llamada a la función ---

if __name__ == "__main__":
    nombre_archivo = r"ruta_de_tu_archivo.docx" 	# Aquí tu ruta de archivo
    leer_tablas_docx(nombre_archivo)				# Llamada a la función


Mediante este otro script mostramos los párrafos (datos no estructurados), los cuales posteriormente deberan ser tratados con los procedimientos correspondiente. Es interesante observar que cada párrafo queda identificado por su posición, lo que puede facilitar su localización, aunque como procedimiento es poco funcional si pensamos en una obtención masiva de determinado contenido. De momento, algo es algo.



import docx
import os

def leer_texto_no_tabular(ruta_archivo):
    if not os.path.exists(ruta_archivo):				# Verificar si el archivo existe
        print(f"Error: El archivo '{ruta_archivo}' no existe.")
        return

    doc = docx.Document(ruta_archivo)		 			# Cargar el documento

    print(f"--- INICIO DEL CONTENIDO NO TABULAR: {ruta_archivo} ---\n")

    contador_parrafos = 0						 		# doc.paragraphs solo itera por los párrafos
    
    for i, para in enumerate(doc.paragraphs):
        texto = para.text.strip()						# Limpiamos espacios en blanco al inicio y final
        
        if texto:										# Omitir párrafos vacíos para una salida más limpia en CMD
            contador_parrafos += 1
            print(f"[{contador_parrafos}]: {texto}")	# Mostramos el índice del párrafo para facilitar el seguimiento
            print("-" * 10) # Separador visual opcional

    if contador_parrafos == 0:
        print("No se encontró texto fuera de tablas en este documento.")

    print(f"\n--- FIN DEL DOCUMENTO (Total párrafos: {contador_parrafos}) ---")

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

if __name__ == "__main__":
    nombre_archivo = r"ruta_de_mi_documento.docx" 	 # Aquí tu archivo
    leer_texto_no_tabular(nombre_archivo)			 # Llamada a la función


miércoles, 22 de abril de 2026

DATOS. Limpieza de datos

Tablas Word (.docx) (II)

Conversión a Excel (xlsx)

Una vez que obtenemos las tablas de un documento .docx, una de las opciones de dar continuidad al procedimiento es transformar dichas tablas en un archivo .xlsx. Otra es hacerlo como.csv, pero la primera opción presenta ventajas sustantivas, como crear una estructura tabular clara para visualización directa, además de manejable desde un servicio de hoja de cálculo. Además incluye una ventaja más: los datos se almacenan como tipos de datos con formatos específicos (string, numéricos, fecha...), dado que así han sido procesados en el script (ver entrada anterior). Por todo esto, esta entrada es mera continuación de la precedente y aporta a aquella el archivo de las tablas y sus contenidos como hoja de cálculo .xlsx. Este es el script que lo hace posible:



# 0. Importamos la bibliotecas necesarias

import pandas as pd
from docx import Document
import os
import unicodedata
import warnings         #Importamos avisos para controlar...

warnings.simplefilter(action='ignore', category=FutureWarning)  # ... que los ocultamos explícitamente

# 1. Función para normalizar las cabeceras de las tablas y facilitar su tratamiento posterior

def normalizar_cabeceras(lista_cabeceras):
    nuevas_cabeceras = []
    for i, nombre in enumerate(lista_cabeceras):
        nombre = str(nombre).strip().lower()
        nombre = "".join(c for c in unicodedata.normalize('NFD', nombre) if unicodedata.category(c) != 'Mn')     # Limpieza básica
        nombre = nombre.replace(" ", "_").replace("/", "_") or f"columna_{i}"
        
        count = 0                            # Código para evitar duplicados (y el error 'duplicate keys') en el nombre de los campos
        temp_nombre = nombre
        while temp_nombre in nuevas_cabeceras:
            temp_nombre = f"{nombre}_{count}"
            count += 1
        nuevas_cabeceras.append(temp_nombre)
    return nuevas_cabeceras

# 2. Función para extraer los datos de las tablas evitando alteraciones previsibles (celdas combinadas)

def extraer_matriz_segura(tabla):
    matriz = []
    for fila in tabla.rows:                 # Extraer contenidos de las tablas evitando errores de celdas combinadas repetidas
        fila_datos = []
        for celda in fila.cells:
            fila_datos.append(celda.text.strip())
        matriz.append(fila_datos)
    
    if not matriz: return []                # Equilibrado de filas, dado que Pandas exige que todas midan lo mismo
    max_cols = max(len(f) for f in matriz)
    for f in matriz:
        while len(f) < max_cols:
            f.append("")                    # Rellenar huecos por celdas combinadas
    return matriz

# 3. Función para obtener las tablas del documento y su contenido

def extractor_universal(ruta_docx):
    try:
        doc = Document(ruta_docx)
        nombre_base = os.path.splitext(ruta_docx)[0]
        ruta_excel = f"{nombre_base}_DB.xlsx"
        lista_dfs = []

        print(f"=== ANALIZANDO: {ruta_docx} ===")

        for i, tabla in enumerate(doc.tables):
            matriz = extraer_matriz_segura(tabla)               # Llamada a función 2
            if len(matriz) < 1: continue

            cabeceras = normalizar_cabeceras(matriz[0])         # Normalizar cabeceras con función 1
            cuerpo = matriz[1:] if len(matriz) > 1 else []

            df = pd.DataFrame(cuerpo, columns=cabeceras)        # Crear el DataFrame de forma más segura
            
            for col in df.columns:                              # Convertir los datos numéricos
                df[col] = pd.to_numeric(df[col], errors='ignore')

            lista_dfs.append(df)
            print(f"Tabla {i+1}: Extraída con éxito ({df.shape[0]} registros).")
            print(df.head(3).to_string(index=False))

        if lista_dfs:                                            # Exportar a Excel, formato xlsx
            with pd.ExcelWriter(ruta_excel, engine='openpyxl') as writer:
                for idx, df in enumerate(lista_dfs):
                    df.to_excel(writer, sheet_name=f"Tabla_{idx+1}", index=False)
            print(f"\n[OK] Guardado en: {ruta_excel}")

    except Exception as e:
        print(f"\n[ERROR]: {e}")

# 4. Llamada a función principal

extractor_universal("tablas.docx")


Además de mostrar los datos por CMD, este script crea una hoja de cálculo Excel en formato .xlsx y almacena en ella las tablas en formato filas-coumnas y los datos que contienen, respetando el tipo de dato supuesto (string, numérico...). Esto tiene varias consecuencias para el tratamiento posterior:

  • se puede acceder a las tablas directamente desde la hoja de cálculo, pero también desde script mediante Pandas trabajando con los datos en función de su tipología.
  • el acceso se hace directamente a datos estructurados, dada la configuración de las tablas resultantes como tales, lo que facilita la manipulación de los datos mediante funciones asociadas a la biblioteca Pandas (y otras)

Además, mediante la función 1 (def normalizar_cabeceras():) evitamos posibles errores en el posterior tratamiento de las tablas. La segunda función (def extraer_matriz_segura()) permite extraer el contenido evitando los errores que derivan de la combinación de tablas.

Pero el núcleo del script sigue siendo la función principal (la 3, def extractor_universal(), que...

  • es a la que llamamdos desde el segmento de ejecución (4, extractor_universal("tablas.docx")), instrucción donde se pasa por parámetro el archivo .docx con el que trabajamos.
  • Esta función principal ejecuta el recorrido del documento, obtiene las tablas que contiene y sus datos...
  • ... asegurando su viabilidad mediante llamada a funciones auxiliares...
    • matriz = extraer_matriz_segura(tabla) y
    • cabeceras = normalizar_cabeceras(matriz[0]))
  • ... generando el dataframe (df = pd.DataFrame(cuerpo, columns=cabeceras))...
  • ... y crea el archivo .xlsx (with pd.ExcelWriter(ruta_excel, engine='openpyxl') as writer:)

El resultado es un .xlsx plenamente operativo, estructurado en hojas (una por tabla) que contienen tablas organizadas para que sus datos puedan ser tratados directamente como datos estructurados y que sea posible trabajar con ellos (consultar y manejo) manualmente, mediante lenguajes de macros o mediante python y sus bibliotecas.

Dada la simplicidad de contenido, pero sobre todo de organización de las tablas, el procedimiento de acceso a tablas y extracción de su contenido ha resultado satisfactorio, haciendo innecesario un tratamiento posterior para que sea posible trabajar directamente con el archivo resultante (.xlsx) y con las tablas que contiene.

DATOS. Tratsamiento de datos

Tablas Word (.docx) (I)

Acceso a tablas simples

El trabajo con textos .docx ya ha sido tratado en otras entradas de este blog, pero desde la prespectiva de la composición de textos. Ahora nos interesa el proceso inverso: la obtención de datos. Empezaremos por la identificación de tablas simples (estructura tabular básica) y la obtención de los datos que contienen.

El documento que nos sirve de modelo no tiene más que ese interés. Obviamente los datos son inventados y carecen de relevancia para cualquier otro objetivo. Se trata de un texto en soporte .docx, que contiene unos breves párrafos de texto (datos no estructurados) y tres tablas de estructura simple (asimilables al formato BD: fila (registro) - columna (campo/variable), que se comportan inicialmente como datos semi-estructurados, aunque sólo en función del soporte (de presentarse en un formato estructurado en campos podrían considerarse datos estructurados).

El primer objetivo que nos proponemos ahora es automatizar identificación de las tablas (obviando el texto) y la extracción de los datos que contienen. Para ello utilizamos el siguiente script Python:



# 0. Importamos las bibliotecas necesarias

import pandas as pd
from docx import Document
import warnings                     # Importamos el módulo de avisos

# 1. Instrucción para ignorar todos los avisos técnicos de las librerías

warnings.filterwarnings("ignore")

# 2. Función para identificación de los tipos de datos para posterior tratamiento

def identificar_tipos_datos(df):    # Se asume la posibilidad de distintos tipos de datos (fechas y numéricos)
    for col in df.columns:
        intentar_fecha = pd.to_datetime(df[col], errors='coerce')   # Fechas
        if intentar_fecha.notna().all():
            df[col] = intentar_fecha
            continue
        intentar_num = pd.to_numeric(df[col], errors='coerce')      # Numéricos
        if intentar_num.notna().sum() > len(df) * 0.8: 
            df[col] = intentar_num
    return df

# 3. Función para la extracción de datos de las tablas-docx

def extractor_universal(ruta_docx):
    try:
        doc = Document(ruta_docx)
        for i, tabla in enumerate(doc.tables):
            datos = []
            for fila in tabla.rows:
                datos.append([celda.text.strip() for celda in fila.cells])

            if not datos: continue

            cabecera = datos[0]
            cuerpo = datos[1:]
            df = pd.DataFrame(cuerpo, columns=cabecera)
            df_estructurado = identificar_tipos_datos(df)	 # Llamada a la función 2

            print(f"\n--- TABLA {i+1} ---")                  # Visualización de tablas en el CMD
            print(df_estructurado.to_string(index=False)) 
            print("-" * 40)

    except Exception as e:
        print(f"Error en el proceso: {e}")

# 4. Ejecución de la función principal

extractor_universal("tablas.docx")


Las pretensiones de este script son muchas, a pesar de lo limitado de su objetivo: se trata de:

  • Identificar las tablas presentes en el documento. Al tratarse de un documento docx se hace uso de la biblioteca python-docx
  • Se pretende crear un script de caracter universal, de ahí que se esperen distintos tipos de datos, los cuales se desean identificar para un posterior tratamiento que no se da en este caso. Esto explica la presencia de la función def identificar_tipos_datos(df) que se relaciona con el uso de la bibliteca import pandas as pd
  • Esto hace posible que se generen avisos no deseados y que carecen de utilidad inmediata, por eso se importa el módulo de control de avisos import warnings y se inhibe su aparición en el CMD con warnings.filterwarnings("ignore"), procedimiento que en otras ocasiones puede ser contraproducente, pero no en esta, ya que en realidad conocemos el contenido de las tablas
  • El núcleo central del script se plantea dentro del bucle for i, tabla in enumerate(doc.tables):, que es donde se obtiene el recuento de las tablas del documento, se recorren éstas, se extrae su contenido y se procesa, incluyendo su conversión de texto a diferentes tipos de datos. Además, finalmente, se muestran por pantalla.

    Dado que con esto finaliza el script, se pierden en realidad posibilidades de tratamiento que sí están disponibles en función de la conversión de datos y de la participación de Pandas; pero ahora lo que nos interesaba era la obtención de los datos, sólo los datos de las tablas, y este objetivo está conseguido. En posteriores entradas profundizaremos en el desarrollo de este proceso.