Python avec DOCX
Résolumamiemando Messages postés 33336 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 5 novembre 2024 - 25 janv. 2024 à 10:48
- Python docx
- Comment ouvrir un fichier docx ? - Guide
- Docx - Guide
- Citizen code python - Accueil - Outils
- Docx repair - Télécharger - Récupération de données
3 réponses
18 déc. 2023 à 16:52
Bonjour Stéphane,
Quelques questions préalables pour mieux cerner ton problème et ce que tu cherches à faire.
- Pourquoi stocker un tableau dans un fichier docx, ce n'est pas plutôt un fichier excel que tu devrais créer ? Si oui, on utilises généralement pandas et on crée une dataframe, et on la sauve au format excel (voir ici).
- Il semble exister une version plus récente de python-docx, pourquoi ne pas l'utiliser ?
- As-tu regardé la documentation de python-docx ? En particulier l'API de l'objet Table et l'index ?
- As-tu commencé à écrire un script python ? Si oui que contient-il ?
Bonne chance
Bonjour,
J'ai énormément d'images à importer dans un rapport de type fichier word.
Pour ce faire j'utilise une hiérarchie de répertoires dans lesquels je place mes images à importer. Mon développement Python scrute les répertoires et crée le fichier word en prenant chaque nom de répertoire et en lui attribuant un style de Titre avec le niveau hiérarchique correspondant et importe toutes les images s'y trouvant dans un tableau de 2 colonnes (en modifiant les dpi et les tailles).
Je suis sur un réseau totalement déconnecté du WEB, c'est pour ça que j'utilise la version 0.8.11 de docx. Je ne pense pas que cela vienne de la version, c'est juste que je ne sais pas bien me servir de python.
J'ai regardé la documentation mais je bloque avec la fonction identifiée à cette Page (property : AllowBreakAcrossPages)
Voici mon code actuel sans l'interface utilisateur :
Ma tentative se situe à la ligne 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()
J'espère avoir été suffisamment clair.
Merci de l'attention accordée
Stéphane
Modifié le 19 déc. 2023 à 18:11
Bonjour,
Je pense avoir compris. Le seul soucis c'est que je ne connais pas docx et donc j'aurais tendance à t'inspirer de cette solution.
Ceci dit, je n'aurais pas procédé comme toi. En effet, tu pourrais te contenter de créer un fichier HTML, bien plus simple à générer, et y inclure tes images.
En admettant que cette solution t'intéresse, je te propose donc cette autre solution, tu n'as plus besoin ni de docx, ni de PIL. Le document s'affichera de manière "responsive" aux dimensions de ton navigateur, et tu n'auras plus de soucis liés aux pages inhérentes aux documents word. Enfin, tu pourras facilement imprimer ton document depuis ton navigateur.
#!/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 tu veux prendre tous les fichiers de chaque dossier make_html(photos_dir, output_filename, num_columns) main()
Pour améliorer ce programme, tu peux utiliser le module argparse afin que ton exécutable prenne des paramètres, ce qui évitera de les mettre en dur dans la fonction main.
Quelques remarques par rapport à ce programme :
- L'énorme avantage de HTML/CSS comparé à DOCX, c'est qu'il est portable.
- Dans le cas général on n'a pas forcément word ou libreoffice installé sur son ordinateur/tablette/portable, mais on a toujours un navigateur.
- Ici, mon fichier HTML stocke des références vers les images.
- L'avantage, c'est que le fichier HTML est très petit, puisque les images ne sont pas stockées dedans.
- En contrepartie, le rendu ne peut se faire qu'en présence de cette hiérarchie d'image.
- Si tu veux générer un fichier "standalone", une solution consisterait à encoder chaque image en base64 (voir ici).
- En CSS, la bonne manière de faire pour afficher les image est d'utiliser une grille.
- L'avantage, c'est qu'on n'a pas besoin de se préoccuper des éventuelles cases vides à ajouter pour compléter la dernière ligne du tableau.
- Le principe est simple. On définit une grille, on énumère les éléments sans se préoccuper de leur nombre, et le navigateur se débrouille pour les placer et les redimensionner. Le nombre de colonne de ladite grille est défini dans le style de grid-container (un "auto" par colonne). Pour plus de détails, voir ici.
- Mon code python utilise des f-strings. Celles-ci sont standard depuis python 3.6 et allègent considérablement le code.
- Cela permet d'injecter du code (entre accolades) au moment de créer la chaîne. Par exemple, je génère dans le style CSS autant de "auto" que la valeur définie dans num_columns.
- En contrepartie, il faut échapper les accolades en les doublant.
- Afin de simplifier les manipulations sur les chemins, j'utilise pathlib qui est un module standard dans les versions modernes de python3.
- Ma recommandation est d'utiliser autant que possible pathlib en python quand tu manipules des chemins, ça permet d'avoir un code portable entre windows et Linux et souvent plus élégant.
- Si tu veux supporter que le programme considère plusieurs extensions de fichiers (par exemple ".jpg", ".png", etc), il faut adapter légèrement le programme en s'inspirant de ce message.
Bonne chance
Bonjour,
Merci beaucoup pour toutes ces explications, mais j'ai vraiment besoin d'activer la propriété "AllowBreakAcrossPages" de toutes mes tables contenues dans le fichier Word généré par mon petit développement.
La génération de ce document avec les styles utilisés viens s'insérer dans un autre document word (copié collé) qui est en fait un rapport obligatoirement au format Word.
J'ai le nom de la propriété de la table dans Word mais je ne sais pas la renseigner en Python.
Merci encore pour tes infos, je vais continuer à creuser mais c'est difficile pour un néophyte :)
Stéphane
20 déc. 2023 à 11:41
Ok, du coup si le format Word est imposé, ma solution tombe effectivement à l'eau. Du coup, avec python-docx, as-tu essayé ceci ?
Bonne chance
10 janv. 2024 à 14:18
Après plusieurs tests, j'ai abandonné la piste XML via Python-docx.
J'ai solutionné mon problème en modifiant le style des tableaux dans le fichier Word, ce qui a eu pour effet de modifier tous les tableaux en rendant les lignes non fractionnables.
Objectif atteint de manière détournée car je ne suis pas informaticien.
Merci pour votre participation.
Stéphane
25 janv. 2024 à 10:48
Merci d'avoir pris le temps de nous expliquer comment tu t'en étais sorti. Ton retour d'information aidera probablement les autres personnes confrontées à ce besoin et qui tomberont sur cette discussion. Bonne continuation