Python con DOCX

Resuelto
Steph -  
mamiemando Mensajes publicados 33228 Fecha de registro   Estado Moderador Última intervención   -

Hola,

Soy un nuevo usuario de Python.

Utilizo python 3.11 con python-docx-0.8.11 con el cual creo un documento WORD que contiene una tabla de 2 columnas y X filas.

Me gustaría poder usar la propiedad de la tabla "Permitir que las filas se dividan en varias páginas" para no tener filas en múltiples páginas.

¿Alguien sabe cómo acceder a esta propiedad?

Gracias

Stéphane


3 respuestas

mamiemando Mensajes publicados 33228 Fecha de registro   Estado Moderador Última intervención   7 940
 

Hola Stéphane,

Algunas preguntas previas para entender mejor tu problema y lo que buscas hacer.

  • ¿Por qué almacenar una tabla en un archivo docx? ¿No deberías crear un archivo excel? Si es así, generalmente usamos pandas y creamos un dataframe, y lo guardamos en formato excel (ver aquí).
  • Parece que existe una versión más reciente de python-docx, ¿por qué no utilizarla?
  • ¿Has revisado la documentación de python-docx? En particular, la API del objeto Table y el índice?
  • ¿Has comenzado a escribir un script en python? Si es así, ¿qué contiene?

Buena suerte

0
Steph
 

Hola,

Tengo una gran cantidad de imágenes que importar en un informe en formato de archivo Word.

Para ello, utilizo una jerarquía de directorios donde coloco mis imágenes para importar. Mi desarrollo en Python explora los directorios y crea el archivo Word asignando a cada nombre de directorio un estilo de Título con el nivel jerárquico correspondiente e importando todas las imágenes que se encuentran en una tabla de 2 columnas (modificando los dpi y los tamaños).

Estoy en una red completamente desconectada de la WEB, por eso estoy usando la versión 0.8.11 de docx. No creo que sea por la versión, simplemente no sé muy bien utilizar Python.

He revisado la documentación, pero me bloqueo con la función identificada en esta Página (propiedad: AllowBreakAcrossPages)

A continuación, mi código actual sin la interfaz de usuario:

Mi intento se encuentra en la línea 110.

# -*- coding: utf8 -*- """ Fichier script et fonctionnel pour l'écriture des documents docx """ from docx import Document from docx.shared import Inches from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.table import WD_TABLE_ALIGNMENT import PIL from PIL import Image import os import io import time import interface_pour_docx as itf #Formats supportés par Pillow : PNG,JPEG,PPM,TIFF,BMP def process_image(image_file,image_loc,table_for_dir_count,cell,dpi=220,inches=3,titres=True): """ Fonction permettant le traitement d'un fichier image rencontré dans un dossier Arguments (* pour les arguments obligatoires): *image_file : image au format tel qu'ouvert par Pillow *image_loc : emplacement de l'image *table_for_dir_count : compteur du nombre d'images déjà traitées pour la partie ou la sous-partie *cell : cellule dans laquelle ajouter l'image dpi : résolution en points par pouce inches : taille de l'image dans le docx (en pouces) titres : variable indiquant si l'on ajoute ou non les titres des images en tant que légende Renvoie : Le compteur du nombre d'images déjà traitées incrémenté """ # On récupère les informations de dpi et on recadre l'image selon le dpi objectif (w,h) = image_file.size w_obj = inches*dpi h_obj = h*w_obj/w image_out_shape = (int(w_obj),int(h_obj)) resized_img = image_file.resize(image_out_shape) #Sauvegarde de l'image sous forme de stream imdata = io.BytesIO() resized_img.save(imdata,format='png') # On ajoute l'image à la cellule, ainsi que son titre p = cell.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run() run.add_picture(imdata,width=Inches(inches)) if titres: p.add_run(image_loc.split(".")[0].split("/")[-1]) else : p.add_run(" ") # Enfin, on renvoie le compteur du nombre d'images traitées incrémenté table_for_dir_count+=1 del imdata return table_for_dir_count def explore_dir(document,directory,depth,f_part_count = "",dpi=220,avec_numeros=True,titres=True,niveau_initial=0): """ Fonction permettant l'exploration d'un dossier Arguments (* pour les arguments obligatoires): *document : le document de sortie (docx.Document) *directory : le répertoire à traiter *depth : la profondeur du titre f_part_count : un préfixe éventuel à rajouter pour le titre de la partie (pour l'affichage console uniquement) dpi : résolution des images en points par pouce avec_numeros : indique si les titres des sous dossiers ont des numéros (ex : 01 dossier-1, 02 dossier-2...) titres : variable indiquant si l'on ajoute ou non les titres des images en tant que légende niveau_initial : variable indiquant le niveau initial de traitement Renvoie : ne renvoie rien """ print(f"traitement de la partie {f_part_count} (dossier {directory})") # De façon récursive, on explore les différents dossiers et sous dossiers, en ajoutant le titre en tant que titre de partie/sous-partie, et en ajoutant les fichiers if depth != niveau_initial and avec_numeros: header = " ".join(directory.split("/")[-1].split(" ")[1:]) else: header = directory.split("/")[-1] heading = document.add_heading(header,level=depth) table_for_dir_count = 0 # Compteur du nombre d'images déjà ajoutées dans cette partie sub_fold_counter = 0 lst = os.listdir(directory) lst.sort() for element in lst: # On liste tout ce qui se trouve dans le répertoire dans l'ordre alphabétique if os.path.isdir(directory+"/"+element): sub_fold_counter += 1 if depth==0: new_f_part_count = f"{sub_fold_counter}." else: new_f_part_count = f_part_count+f"{sub_fold_counter}." # Si l'élément est un répertoire, on fouille dedans de façon récursive explore_dir(document,directory+"/"+element,depth=depth+1,f_part_count=new_f_part_count,dpi=dpi,avec_numeros=avec_numeros,titres=titres,niveau_initial=niveau_initial) else: # Sinon, on teste de l'ouvrir en tant qu'image, et si ça marche on la traîte try: img = Image.open(directory+"/"+element) # Selon le nombre d'images déjà présentes, on ajoute ou non une colonne, et on pointe vbers la première cellule vide if table_for_dir_count==0: table = document.add_table(rows=1,cols=2,style="Table Grid") #table.rows.AllowBreakAcrossPages=False cell = table.rows[0].cells[1] cell.width=Inches(3.15) cell = table.rows[0].cells[0] table.alignment = WD_TABLE_ALIGNMENT.CENTER cell.width=Inches(3.15) elif table_for_dir_count%2==0: cell = table.add_row().cells[0] else: cell = table.rows[-1].cells[1] table_for_dir_count = process_image(img,directory+"/"+element,table_for_dir_count,cell,dpi=dpi,titres=titres) except PIL.UnidentifiedImageError: continue def main(): """ Fonction principale : on ouvre la fenêtre pour récupérer les options du traitement, on crée le document et on lance le script d'exploration. Il ne reste plus qu'à sauvegarder et voilà ! """ name,dpi,in_directory,out_directory,dossiers_avec_numeros,titres,niveau_initial = itf.fenetre_analyse_docx() # Création du document document = Document() #Exploration explore_dir(document,in_directory,niveau_initial,dpi=dpi,avec_numeros=dossiers_avec_numeros,titres=titres,niveau_initial=niveau_initial) # Sauvegarde print("Sauvegarde du document") document.save(out_directory+f"/{name}.docx") print("Terminé !") time.sleep(1.) if __name__=="__main__": main()

Espero haber sido lo suficientemente claro.

Gracias por la atención prestada

Stéphane

0
mamiemando Mensajes publicados 33228 Fecha de registro   Estado Moderador Última intervención   7 940
 

Hola,

Creo que lo he entendido. El único problema es que no conozco docx y, por lo tanto, tiendo a inspirarme en esta solución.

Dicho esto, no lo habría hecho como tú. De hecho, podrías simplemente crear un archivo HTML, mucho más fácil de generar, e incluir tus imágenes allí.

Suponiendo que esta solución te interese, te propongo esta otra alternativa, ya no necesitas ni docx ni PIL. El documento se mostrará de manera "responsive" a las dimensiones de tu navegador, y ya no tendrás problemas relacionados con las páginas inherentes a los documentos de Word. Además, podrás imprimir tu documento fácilmente desde tu navegador.

#!/usr/bin/env python3 from pathlib import Path def make_html( photos_dir: Path, output_filename: Path, num_columns: int = 2, max_files_per_dir: int = None ): with open(output_filename, "w") as f: subdirs = photos_dir.glob("**/") print( f""" <html> <head> <style> h1 {{ color: blue; }} h2 {{ color: darkgreen; }} h3 {{ color: orange; }} h4 {{ color: red; }} h5 {{ color: purple; }} .grid-container {{ display: grid; grid-template-columns: {"auto " * num_columns}; padding: 2%; width: 100%; }} .grid-item {{ padding: 5%; }} </style> </head> <body>""", file=f ) for subdir in subdirs: n = len(str(subdir).split("/")) print( f""" <h{n}><pre>{subdir}</pre></h{n}> <div class="grid-container">""", file=f ) photos = subdir.glob("**/*jpg") for photo in list(photos)[:max_files_per_dir]: print( f""" <div class="grid-item"> <pre>{photo.name}</pre> <img src="{photo}" width="100%"/> </div>""", file=f ) print(""" </div>""", file=f ) print( """ </body> </html>""", file=f ) def main(): photos_dir = Path.home() / "Photos" output_filename = "toto.html" num_columns = 2 max_files_per_dir = 10 # None --si quieres tomar todos los archivos de cada carpeta make_html(photos_dir, output_filename, num_columns) main() 

Para mejorar este programa, puedes utilizar el módulo argparse para que tu ejecutable acepte parámetros, lo que evitará tener que escribirlos en el código en la función main.

Algunas observaciones sobre este programa:

  • La gran ventaja de HTML/CSS en comparación con DOCX es que es portátil.
    • En términos generales, no siempre tenemos word o libreoffice instalado en nuestra computadora/tableta/portátil, pero siempre tenemos un navegador.
  • Aquí, mi archivo HTML almacena referencias a las imágenes.
    • La ventaja es que el archivo HTML es muy pequeño, ya que las imágenes no se almacenan dentro de él.
    • En contraposición, la visualización solo se puede realizar en presencia de esta jerarquía de imágenes.
    • Si deseas generar un archivo "independiente", una solución sería codificar cada imagen en base64 (ver aquí).
  • En CSS, la manera correcta de mostrar las imágenes es utilizando una cuadrícula.
    • La ventaja es que no necesitamos preocuparnos por las posibles casillas vacías que agregar para completar la última fila de la tabla.
    • El principio es sencillo. Definimos una cuadrícula, enumeramos los elementos sin preocuparnos por su número, y el navegador se encarga de colocarlos y redimensionarlos. El número de columnas de dicha cuadrícula se define en el estilo de grid-container (un "auto" por columna). Para más detalles, ver aquí.
  • Mi código Python utiliza f-strings. Estas son estándar desde Python 3.6 y simplifican considerablemente el código.
    • Esto permite inyectar código (entre llaves) al momento de crear la cadena. Por ejemplo, genero en el estilo CSS tantas "auto" como el valor definido en num_columns.
    • En contraposición, hay que escapar las llaves duplicándolas.
  • Para simplificar las manipulaciones de rutas, utilizo pathlib, que es un módulo estándar en las versiones modernas de Python 3.
    • Mi recomendación es utilizar tanto como sea posible pathlib en Python cuando manipules rutas, esto permite tener un código portátil entre Windows y Linux y, a menudo, más elegante.
  • Si deseas que el programa considere múltiples extensiones de archivo (por ejemplo, ".jpg", ".png", etc.), tendrás que adaptar ligeramente el programa inspirándote en este mensaje.

Buena suerte

0
Steph
 

Hola,

Muchas gracias por todas estas explicaciones, pero realmente necesito activar la propiedad "AllowBreakAcrossPages" de todas mis tablas que se encuentran en el archivo Word generado por mi pequeño desarrollo.

La generación de este documento con los estilos utilizados se inserta en otro documento Word (copiar y pegar) que es, de hecho, un informe que debe estar obligatoriamente en formato Word.

Tengo el nombre de la propiedad de la tabla en Word, pero no sé cómo configurarla en Python.

Gracias de nuevo por tu información, continuaré investigando, pero es difícil para un neófito :)

Stéphane

0
mamiemando Mensajes publicados 33228 Fecha de registro   Estado Moderador Última intervención   7 940 > Steph
 

Vale, entonces si el formato Word es obligatorio, mi solución efectivamente se viene abajo. Entonces, con python-docx, ¿has probado esto?

Buena suerte

0
Steph > mamiemando Mensajes publicados 33228 Fecha de registro   Estado Moderador Última intervención  
 

Después de varias pruebas, abandoné la opción XML a través de Python-docx.

He solucionado mi problema modificando el estilo de las tablas en el archivo de Word, lo que tuvo como efecto modificar todas las tablas haciendo que las filas no sean fraccionables.

Objetivo alcanzado de forma indirecta ya que no soy informático.

Gracias por su participación.

Stéphane

1
mamiemando Mensajes publicados 33228 Fecha de registro   Estado Moderador Última intervención   7 940 > Steph
 

Gracias por tomarte el tiempo de explicarnos cómo lo lograste. Tu retroalimentación probablemente ayudará a otras personas que enfrenten esta necesidad y que se encuentren con esta discusión. Buena continuación.

0