Install any skill in seconds. Free to start, no credit card required.
Get Started Free →当用户需要基于模板填充 Word 文档(.docx)、从模板生成报告、创建包含动态数据的合同,或自动化文档生成时使用此技能。包括替换普通占位符 {name} 替换文本、使用 {name|r:x,c:y} 格式标记的智能表格填充(支持从标记行开始向下填充,保留上方内容)、插入图片、批量生成文档等。如果用户提及 .docx 模板、邮件合并功能或以编程方式填写 Word 表单,请使用此技能。
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 143% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 133% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 152% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 206% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 315% | 0% |
本指南介绍如何使用 Python 向 Word(.docx)模板填充动态数据。支持:
{variable_name} 替换文本{name|r:x,c:y} 标记在表格左侧任意位置,自动从标记行向下填充,保留上方内容,并自动调整表格行列数表格并非总是"提前自动扩展" - 这取决于占位符是否被正确识别:
{name|r:x,c:y} 格式完全正确且位于该行的最左侧单元格(第一列)时,表格会在 DocxTemplateFiller 初始化时立即调整为声明的行列数{name | r:5, c:4})、或不在第一列,表格将保持原样,不会自动扩展或收缩pythonfrom docx import Document from docx.shared import Inches from docx.oxml import OxmlElement from docx.oxml.ns import qn import re import os class DocxTemplateFiller: def __init__(self, template_path): if not os.path.exists(template_path): raise FileNotFoundError(f"模板文件不存在: {template_path}") self.doc = Document(template_path) self.template_path = template_path self.named_tables = {} # 存储表格信息 # 初始化时扫描表格占位符 self._process_table_placeholders() def _process_table_placeholders(self): """ 扫描所有表格的每一行第一个单元格,查找 {name|r:x,c:y} 格式 例如:{products|r:5,c:4} 表示从当前行开始,共5行,4列 """ pattern = re.compile(r'\{(\w+)\|r:(\d+),c:(\d+)\}') for table_idx, table in enumerate(self.doc.tables): for row_idx, row in enumerate(table.rows): if len(row.cells) == 0: continue # 检查每行的第一个单元格(最左侧) first_cell = row.cells[0] text = first_cell.text.strip() match = pattern.search(text) if match: name = match.group(1) target_rows = int(match.group(2)) target_cols = int(match.group(3)) # 保存表格配置信息 self.named_tables[name] = { 'table': table, 'start_row': row_idx, # 占位符所在行(从此行开始填充) 'target_rows': target_rows, # 需要填充的总行数(包括占位符行) 'target_cols': target_cols # 目标列数 } # 调整表格大小:上方保留 row_idx 行,从 row_idx 开始有 target_rows 行 self._resize_table(table, row_idx, target_rows, target_cols) # 清除占位符文本(可选,因为填充时会覆盖,但清除更干净) # 保留单元格其他可能的文本(占位符前后文本) new_text = pattern.sub('', first_cell.text).strip() if new_text: first_cell.text = new_text else: first_cell.text = "" # 清空等待填充 def _resize_table(self, table, start_row, target_rows, target_cols): """调整表格大小:确保从 start_row 开始有 target_rows 行,总列数为 target_cols""" total_needed_rows = start_row + target_rows current_rows = len(table.rows) current_cols = len(table.columns) if table.columns else 0 # 调整行数 if total_needed_rows > current_rows: # 添加行 for _ in range(total_needed_rows - current_rows): table.add_row() elif total_needed_rows < current_rows: # 删除多余行(从末尾删除,保留前面的) self._delete_rows_from_end(table, current_rows - total_needed_rows) # 调整列数 if target_cols != current_cols: self._resize_columns(table, target_cols) def _delete_rows_from_end(self, table, num_rows): """从表格末尾删除指定行数""" tbl = table._tbl for _ in range(num_rows): if len(table.rows) > 0: tr = table.rows[-1]._tr tbl.remove(tr) def _resize_columns(self, table, target_cols): """调整表格列数""" current_cols = len(table.columns) tbl = table._tbl tblGrid = tbl.find(qn('w:tblGrid')) if target_cols > current_cols: # 添加列定义 for _ in range(target_cols - current_cols): gridCol = OxmlElement('w:gridCol') tblGrid.append(gridCol) # 为每一行添加单元格 for row in table.rows: for _ in range(target_cols - current_cols): tc = OxmlElement('w:tc') tcPr = OxmlElement('w:tcPr') tc.append(tcPr) p = OxmlElement('w:p') tc.append(p) row._tr.append(tc) elif target_cols < current_cols: # 删除多余列定义 for _ in range(current_cols - target_cols): if len(tblGrid) > 0: tblGrid.remove(tblGrid[-1]) # 从每行删除多余单元格 for row in table.rows: for _ in range(current_cols - target_cols): tcs = row._tr.findall(qn('w:tc')) if len(tcs) > target_cols: row._tr.remove(tcs[-1]) def fill_placeholders(self, data_dict): """替换普通占位符 {key}(不包括表格定义格式)""" # 匹配普通占位符,排除表格定义格式 pattern = re.compile(r'\{(\w+)\}(?!\|r:\d+,c:\d+)') # 处理段落 for para in self.doc.paragraphs: self._replace_in_paragraph(para, pattern, data_dict) # 处理表格内的普通占位符(排除已识别的表格占位符单元格) for table in self.doc.tables: for row in table.rows: for cell in row.cells: for para in cell.paragraphs: self._replace_in_paragraph(para, pattern, data_dict) def _replace_in_paragraph(self, paragraph, pattern, data_dict): """在段落中执行替换""" text = paragraph.text matches = pattern.findall(text) if not matches: return new_text = text for key in matches: if key in data_dict: placeholder = f'{{{key}}}' value = str(data_dict[key]) new_text = new_text.replace(placeholder, value) if new_text != text and paragraph.runs: paragraph.runs[0].text = new_text for run in paragraph.runs[1:]: run.text = "" def fill_named_table(self, table_name, data): """ 填充指定名称的表格 从占位符所在行开始(包括该行)向下填充,保留上方内容 :param table_name: 表格名称(来自占位符 {name|r:x,c:y}) :param data: 二维列表,如 [['产品A', '规格1', '10'], [...]] """ if table_name not in self.named_tables: raise KeyError(f"未找到名为 '{table_name}' 的表格,请确保模板中存在 '{{{table_name}|r:x,c:y}}' 格式的占位符在最左侧单元格") table_info = self.named_tables[table_name] table = table_info['table'] start_row = table_info['start_row'] target_rows = table_info['target_rows'] target_cols = table_info['target_cols'] # 确保数据不超过声明的行数 if len(data) > target_rows: data = data[:target_rows] # 从 start_row 开始填充(包括该行) for row_offset, row_data in enumerate(data): actual_row_idx = start_row + row_offset if actual_row_idx >= len(table.rows): break # 确保不超出列数 if len(row_data) > target_cols: row_data = row_data[:target_cols] # 填充该行的每一列 for col_idx, value in enumerate(row_data): if col_idx >= len(table.columns): break table.cell(actual_row_idx, col_idx).text = str(value) def fill_all(self, text_data=None, table_data=None): """ 一键填充所有内容 :param text_data: 普通占位符字典,如 {'company': 'ABC公司'} :param table_data: 表格数据字典,如 {'products': [[...], [...]]} """ if text_data: self.fill_placeholders(text_data) if table_data: for name, data in table_data.items(): self.fill_named_table(name, data) def insert_paragraph_at(self, index, text, style=None): """在指定位置插入段落""" if index == -1 or index >= len(self.doc.paragraphs): p = self.doc.add_paragraph(text) else: p = self.doc.paragraphs[index].insert_paragraph_before(text) if style: p.style = style return p def insert_image(self, paragraph_index, image_path, width=None): """在指定段落后插入图片""" if not os.path.exists(image_path): raise FileNotFoundError(f"图片不存在: {image_path}") para = self.doc.paragraphs[paragraph_index] run = para.add_run() if width: run.add_picture(image_path, width=Inches(width)) else: run.add_picture(image_path) def save(self, output_path): self.doc.save(output_path) print(f"✅ 文档已生成: {os.path.abspath(output_path)}")
使用 {variable_name} 格式:
甲方(购方):{company}
签署日期:{date}
合同编号:{contract_no}
格式:{name|r:x,c:y} 位置:表格最左侧的任意单元格(通常是某一行的第一列) 行为:
示例:
| 序号 | 产品名称 | 规格 | 数量 | 单价 | 金额 |
| ------ | -------- | --- | --- | --- | ---- |
| 1 | 产品A | 规格1 | 10 | 100 | 1000 |
| {items|r:3,c:6} | | | | | |
| | | | | | |
说明:
{items|r:3,c:6} 放在第3行第1列(索引从0开始则为第2行)场景:合同中有两个表格,第一个表格上方有静态说明行
采购合同
甲方:{company}
乙方:{seller}
产品列表(常规采购):
| 产品名称 | 型号 | 数量 | 单价 |
|----------|------|------|------|
| {regular|r:4,c:4} | | | |
| | | | |
| | | | |
| | | | |
紧急采购项(如有):
| 产品名称 | 型号 | 数量 | 要求 |
|----------|------|------|------|
| 说明:紧急采购需24小时内到货 | | | |
| {urgent|r:2,c:4} | | | |
| | | | |
总计金额:{total_amount}
填充代码:
pythonfiller = DocxTemplateFiller("template.docx") filler.fill_all( text_data={ 'company': '北京科技', 'seller': '上海贸易', 'total_amount': '¥50,000' }, table_data={ 'regular': [ ['办公椅', '人体工学', '10', '¥800'], ['办公桌', '1.2米', '5', '¥1500'], ['文件柜', '铁皮', '3', '¥600'] # 第4行不会填充,因为只声明了r:3,但提供了3行数据 ], 'urgent': [ ['投影仪', '4K激光', '1', '急需'], ['幕布', '100寸', '1', '配套'] ] } ) filler.save("contract.docx")
结果:
占位符所在行上方的所有行(包括表头、说明文字、静态数据)完全不会被修改。
python# 模板: # 第0行:表头 | 名称 | 价格 | # 第1行:说明 | 这是说明文字 | # 第2行:占位符 {data|r:2,c:2} | | # 第3行:空行 | | # 填充 data = [['A', '100'], ['B', '200']] # 结果: # 第0行:表头 | 名称 | 价格 | (不变) # 第1行:说明 | 这是说明文字 | (不变) # 第2行:A | 100 | (覆盖占位符) # 第3行:B | 200 | (填充)
如果模板中占位符下方没有足够的行,程序会自动添加:
python# 模板只有3行,占位符在第2行,声明 {data|r:5,c:3} # 程序会自动添加行,使从第2行开始有5行(总行数至少为2+5=7行)
如果模板中占位符下方行数过多,程序会删除多余行(从末尾删除,保留上方内容)。
无论原表格有多少列,程序会调整为占位符声明的列数:
pythonfiller = DocxTemplateFiller("contract.docx") # 表格占位符在模板中位于第2行(索引2,即第3行),声明 {products|r:5,c:4} # 第0-1行是表头和说明,会被保留 filler.fill_named_table('products', [ ['笔记本电脑', 'ThinkPad X1', '10', '¥5000'], # 填充到第2行(覆盖占位符) ['显示器', 'Dell 27寸', '20', '¥1500'], # 填充到第3行 ['键盘', '机械键盘', '30', '¥300'] # 填充到第4行 # 第5-6行保持为空(声明了5行,只提供3行数据) ]) filler.save("output.docx")
pythonfiller = DocxTemplateFiller("report.docx") # 模板中有: # 表1:左上角某行有 {sales|r:10,c:5} # 表2:左上角某行有 {expenses|r:5,c:3} filler.fill_all( table_data={ 'sales': [ ['一月', '产品A', '100', '¥50', '¥5000'], ['二月', '产品A', '120', '¥50', '¥6000'], # ... 最多10行 ], 'expenses': [ ['办公费', '¥2000', '行政部'], ['差旅费', '¥5000', '销售部'], # ... 最多5行 ] } )
即使模板中的表格只有占位符那一行,声明 r:20 后也会自动扩展:
python# 模板表格: # | 项目 | 数量 | 金额 | # | {items|r:20,c:3} | | | filler = DocxTemplateFiller("template.docx") # 自动扩展到20行数据+表头 filler.fill_named_table('items', large_data_list) # 最多填充20行
{name|r:数字,c:数字},不能有空格提供的数据行数超过声明的 r:x 时,多余数据会被截断:
python# 声明 {data|r:3,c:2},表示从占位符行开始只有3行空间 filler.fill_named_table('data', [ ['A', '1'], ['B', '2'], ['C', '3'], ['D', '4'] # 这一行会被忽略,因为只声明了3行 ])
如果占位符所在行存在合并单元格,填充行为可能不符合预期。建议占位符所在行及下方行为标准行列结构。
填充时会替换单元格的 .text 属性,这可能清除单元格内的特殊格式(如加粗、颜色)。如果需要保留格式,建议使用 python-docx 的低级 API 直接操作 run 对象。
| 功能 | 方法/说明 | | --------- | ----------------------------------------------------------- | | 加载模板 | filler = DocxTemplateFiller("template.docx") | | 普通占位符 | {company} → filler.fill_placeholders({'company': '名称'}) | | 填充表格 | filler.fill_named_table('products', [...]) | | 一键填充 | filler.fill_all(text_dict, table_dict) | | 上方内容 | 占位符所在行上方的内容自动保留 | | 覆盖范围 | 从占位符行开始,向下填充 r:x 行 |
Other measured skills in the registry, with their headline benchmark lift.