#!/usr/bin/env python3 import argparse import json import re from pathlib import Path from zipfile import ZipFile, ZIP_DEFLATED def fill_docx_template(template_path: str, output_path: str, data: dict) -> int: with ZipFile(template_path, 'r') as zin: xml = zin.read('word/document.xml').decode('utf-8', 'ignore') replaced = 0 for key, value in data.items(): ph = '{{' + str(key) + '}}' val = str(value) count = xml.count(ph) if count: replaced += count xml = xml.replace(ph, val) # warn on unresolved placeholders unresolved = sorted(set(re.findall(r'\{\{[^{}]+\}\}', xml))) with ZipFile(template_path, 'r') as zin, ZipFile(output_path, 'w', ZIP_DEFLATED) as zout: for item in zin.infolist(): content = zin.read(item.filename) if item.filename == 'word/document.xml': content = xml.encode('utf-8') zout.writestr(item, content) print(f'written: {output_path}') print(f'replaced: {replaced}') if unresolved: print('unresolved_placeholders:') for u in unresolved: print(f' - {u}') else: print('unresolved_placeholders: none') return 0 def main(): ap = argparse.ArgumentParser() ap.add_argument('--template', required=True, help='Path to .docx template with {{PLACEHOLDER}}') ap.add_argument('--data', required=True, help='Path to JSON map: {"PLACEHOLDER":"value"}') ap.add_argument('--out', required=True, help='Output .docx path') args = ap.parse_args() template = Path(args.template) data_path = Path(args.data) out = Path(args.out) with open(data_path, 'r', encoding='utf-8') as f: data = json.load(f) out.parent.mkdir(parents=True, exist_ok=True) raise SystemExit(fill_docx_template(str(template), str(out), data)) if __name__ == '__main__': main()