first commit
This commit is contained in:
272
replace_docx.py
Normal file
272
replace_docx.py
Normal file
@@ -0,0 +1,272 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user