273 lines
9.0 KiB
Python
273 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Script de remplacement de texte dans des fichiers .docx
|
|
Lit un fichier YAML de configuration et applique les remplacements
|
|
dans tous les .docx d'un dossier en préservant le formatage.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import shutil
|
|
import yaml
|
|
from docx import Document
|
|
from copy import deepcopy
|
|
|
|
|
|
def get_full_text_from_runs(runs):
|
|
"""Reconstitue le texte complet à partir des runs."""
|
|
return "".join(r.text for r in runs)
|
|
|
|
|
|
def replace_in_runs(runs, ancien, nouveau):
|
|
"""
|
|
Remplace du texte dans une liste de runs en préservant le formatage.
|
|
Gère le cas où le texte est découpé sur plusieurs runs.
|
|
Retourne le nombre de remplacements effectués.
|
|
"""
|
|
count = 0
|
|
full_text = get_full_text_from_runs(runs)
|
|
|
|
if ancien not in full_text:
|
|
return 0
|
|
|
|
while ancien in full_text:
|
|
count += 1
|
|
start_idx = full_text.index(ancien)
|
|
end_idx = start_idx + len(ancien)
|
|
|
|
# Trouver quels runs sont concernés
|
|
char_pos = 0
|
|
start_run = None
|
|
end_run = None
|
|
|
|
for i, run in enumerate(runs):
|
|
run_start = char_pos
|
|
run_end = char_pos + len(run.text)
|
|
|
|
if start_run is None and run_end > start_idx:
|
|
start_run = i
|
|
start_offset = start_idx - run_start
|
|
|
|
if run_end >= end_idx:
|
|
end_run = i
|
|
end_offset = end_idx - run_start
|
|
break
|
|
|
|
char_pos = run_end
|
|
|
|
if start_run is None or end_run is None:
|
|
break
|
|
|
|
# Cas simple : tout dans un seul run
|
|
if start_run == end_run:
|
|
run = runs[start_run]
|
|
run.text = run.text[:start_offset] + nouveau + run.text[end_offset:]
|
|
else:
|
|
# Multi-run : mettre le nouveau texte dans le premier run,
|
|
# vider les runs intermédiaires, ajuster le dernier
|
|
runs[start_run].text = runs[start_run].text[:start_offset] + nouveau
|
|
|
|
for i in range(start_run + 1, end_run):
|
|
runs[i].text = ""
|
|
|
|
runs[end_run].text = runs[end_run].text[end_offset:]
|
|
|
|
full_text = get_full_text_from_runs(runs)
|
|
|
|
return count
|
|
|
|
|
|
def replace_in_paragraph(paragraph, ancien, nouveau):
|
|
"""Remplace du texte dans un paragraphe."""
|
|
if not paragraph.runs:
|
|
return 0
|
|
return replace_in_runs(paragraph.runs, ancien, nouveau)
|
|
|
|
|
|
def replace_in_table(table, ancien, nouveau):
|
|
"""Remplace du texte dans toutes les cellules d'un tableau."""
|
|
count = 0
|
|
for row in table.rows:
|
|
for cell in row.cells:
|
|
for paragraph in cell.paragraphs:
|
|
count += replace_in_paragraph(paragraph, ancien, nouveau)
|
|
# Tables imbriquées
|
|
for nested_table in cell.tables:
|
|
count += replace_in_table(nested_table, ancien, nouveau)
|
|
return count
|
|
|
|
|
|
def replace_in_headers_footers(doc, ancien, nouveau):
|
|
"""Remplace du texte dans les en-têtes et pieds de page."""
|
|
count = 0
|
|
for section in doc.sections:
|
|
for header in [section.header, section.first_page_header, section.even_page_header]:
|
|
if header and header.is_linked_to_previous is False:
|
|
for paragraph in header.paragraphs:
|
|
count += replace_in_paragraph(paragraph, ancien, nouveau)
|
|
for table in header.tables:
|
|
count += replace_in_table(table, ancien, nouveau)
|
|
|
|
for footer in [section.footer, section.first_page_footer, section.even_page_footer]:
|
|
if footer and footer.is_linked_to_previous is False:
|
|
for paragraph in footer.paragraphs:
|
|
count += replace_in_paragraph(paragraph, ancien, nouveau)
|
|
for table in footer.tables:
|
|
count += replace_in_table(table, ancien, nouveau)
|
|
return count
|
|
|
|
|
|
def process_docx(filepath, remplacements):
|
|
"""Applique tous les remplacements sur un fichier .docx."""
|
|
doc = Document(filepath)
|
|
rapport = {}
|
|
|
|
for remp in remplacements:
|
|
ancien = remp["ancien"]
|
|
nouveau = remp["nouveau"]
|
|
fichiers_cibles = remp.get("fichiers", None)
|
|
|
|
# Si des fichiers cibles sont spécifiés, vérifier que ce fichier en fait partie
|
|
if fichiers_cibles:
|
|
basename = os.path.basename(filepath)
|
|
if basename not in fichiers_cibles:
|
|
continue
|
|
|
|
count = 0
|
|
|
|
# Paragraphes du corps
|
|
for paragraph in doc.paragraphs:
|
|
count += replace_in_paragraph(paragraph, ancien, nouveau)
|
|
|
|
# Tableaux
|
|
for table in doc.tables:
|
|
count += replace_in_table(table, ancien, nouveau)
|
|
|
|
# En-têtes et pieds de page
|
|
count += replace_in_headers_footers(doc, ancien, nouveau)
|
|
|
|
if count > 0:
|
|
rapport[f"'{ancien}' -> '{nouveau}'"] = count
|
|
|
|
doc.save(filepath)
|
|
return rapport
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python3 replace_docx.py <fichier_config.yaml>")
|
|
sys.exit(1)
|
|
|
|
config_path = os.path.abspath(sys.argv[1])
|
|
config_dir = os.path.dirname(config_path)
|
|
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|
config = yaml.safe_load(f)
|
|
|
|
# Les chemins sont relatifs à la racine du projet (dossier contenant replace_docx.py)
|
|
project_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
dossier_source = config["dossier_source"]
|
|
if not os.path.isabs(dossier_source):
|
|
dossier_source = os.path.join(project_dir, dossier_source)
|
|
|
|
remplacements = config["remplacements"]
|
|
|
|
# Le dossier de travail est celui qui contient le fichier YAML
|
|
dossier_travail = config_dir
|
|
|
|
# Copier les .docx du template dans le dossier de travail
|
|
docx_copies = 0
|
|
for f in os.listdir(dossier_source):
|
|
if f.endswith(".docx") and not f.startswith("~$"):
|
|
src = os.path.join(dossier_source, f)
|
|
dst = os.path.join(dossier_travail, f)
|
|
if not os.path.exists(dst):
|
|
shutil.copy2(src, dst)
|
|
docx_copies += 1
|
|
|
|
if docx_copies > 0:
|
|
print(f"{docx_copies} fichier(s) .docx copie(s) depuis {dossier_source}")
|
|
else:
|
|
print(f"Fichiers .docx deja presents dans {dossier_travail}")
|
|
|
|
# Trouver tous les .docx du dossier de travail
|
|
docx_files = []
|
|
for f in os.listdir(dossier_travail):
|
|
if f.endswith(".docx") and not f.startswith("~$"):
|
|
docx_files.append(os.path.join(dossier_travail, f))
|
|
|
|
if not docx_files:
|
|
print("Aucun fichier .docx trouve.")
|
|
sys.exit(1)
|
|
|
|
print(f"\n{len(docx_files)} fichier(s) .docx trouve(s)\n")
|
|
|
|
# Appliquer les remplacements
|
|
rapport_global = {}
|
|
for filepath in sorted(docx_files):
|
|
rapport = process_docx(filepath, remplacements)
|
|
rel_path = os.path.relpath(filepath, dossier_travail)
|
|
rapport_global[rel_path] = rapport
|
|
|
|
# Identifier les remplacements non trouves
|
|
remplacements_non_trouves = set()
|
|
for remp in remplacements:
|
|
cle = f"'{remp['ancien']}' -> '{remp['nouveau']}'"
|
|
trouve = False
|
|
for fichier, rapport in rapport_global.items():
|
|
if cle in rapport:
|
|
trouve = True
|
|
break
|
|
if not trouve:
|
|
remplacements_non_trouves.add(cle)
|
|
|
|
# Generer le rapport markdown
|
|
from datetime import datetime
|
|
lignes = []
|
|
lignes.append(f"# Rapport de modifications")
|
|
lignes.append(f"")
|
|
lignes.append(f"**Date** : {datetime.now().strftime('%d/%m/%Y %H:%M')}")
|
|
lignes.append(f"**Template** : `{os.path.relpath(dossier_source, project_dir)}`")
|
|
lignes.append(f"**Fichiers traites** : {len(docx_files)}")
|
|
lignes.append(f"")
|
|
|
|
lignes.append(f"## Modifications par fichier")
|
|
lignes.append(f"")
|
|
for fichier, rapport in sorted(rapport_global.items()):
|
|
lignes.append(f"### {fichier}")
|
|
lignes.append(f"")
|
|
if rapport:
|
|
lignes.append(f"| Ancien | Nouveau | Occurrences |")
|
|
lignes.append(f"|---|---|---|")
|
|
for remp, count in rapport.items():
|
|
# remp est au format "'ancien' -> 'nouveau'"
|
|
parties = remp.split("' -> '")
|
|
ancien_txt = parties[0][1:] # enlever le ' du debut
|
|
nouveau_txt = parties[1][:-1] # enlever le ' de la fin
|
|
lignes.append(f"| {ancien_txt} | {nouveau_txt} | {count} |")
|
|
else:
|
|
lignes.append(f"Aucun remplacement effectue.")
|
|
lignes.append(f"")
|
|
|
|
if remplacements_non_trouves:
|
|
lignes.append(f"## Remplacements non trouves")
|
|
lignes.append(f"")
|
|
lignes.append(f"Les remplacements suivants n'ont ete trouves dans aucun fichier :")
|
|
lignes.append(f"")
|
|
for r in remplacements_non_trouves:
|
|
lignes.append(f"- {r}")
|
|
lignes.append(f"")
|
|
|
|
# Ecrire le rapport
|
|
rapport_path = os.path.join(dossier_travail, "rapport.md")
|
|
with open(rapport_path, "w", encoding="utf-8") as f:
|
|
f.write("\n".join(lignes))
|
|
|
|
print(f"Rapport genere : {rapport_path}")
|
|
print("Termine!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|