每周把 Excel 项目进度、负责人和日期重复填进 Word 周报,既费时也容易漏项;Python 可以按模板批量生成文档,但占位符必须与表头完全对应,若被 Word 拆成多个文本片段,程序可能无法识别。文章介绍用 openpyxl 和 python-docx 读取表格、替换模板字段,并按每行数据生成报告,同时检查缺失字段和未替换占位符。自动化能减少重复录入,却不能代写正式结论,生成后该如何确保内容与格式可靠?
— 此摘要由AI分析文章内容生成,仅供参考。
如果你每周都要把 Excel 中的项目进度、负责人和日期填进同一份 Word 周报,可以用 Python 按模板批量生成文档。下面的做法适合字段固定、版式重复的报告、业务通知和数据摘要;生成后仍需人工检查内容与格式,不会替你自动判断或撰写正式结论。

一、准备模板和 Excel 数据
先明确每份文档需要哪些字段,再让 Word 模板中的占位符与 Excel 表头一一对应。例如,Word 模板里可以写:
项目周报
项目名称:{{项目名称}}
负责人:{{负责人}}
统计日期:{{统计日期}}
本周进展:
{{本周进展}}
下周计划:
{{下周计划}}
Excel 表格的首行填写对应字段名,每一行代表一份报告:
| 项目名称 | 负责人 | 统计日期 | 本周进展 | 下周计划 |
|---|---|---|---|---|
| 门店改版 | 林青 | 2025-03-14 | 完成首页评审 | 开始联调 |
制作模板时注意以下几点:
- 占位符名称要与 Excel 表头完全一致,包括空格和标点。建议统一使用
{{字段名}}的格式。 - 占位符尽量保持为连续文本。 在 Word 中,选中占位符后不要单独改变其中某个字符的字体或样式;否则 Word 可能把它拆成多个文本片段,程序就不一定能识别。
- 把固定说明留在模板中。 例如报告标题、免责声明和固定章节无需放进 Excel。
- 确定空值规则。 某些字段允许留空,另一些字段缺失就不应发布。先约定好规则,避免生成后才发现关键信息为空。
二、安装 Python 库
在终端或命令提示符中执行:
python -m pip install openpyxl python-docx
这里用 openpyxl 读取 .xlsx 文件,用 python-docx 打开、修改和保存 .docx 文件。建议先准备三个文件:
项目目录/
├── template.docx
├── data.xlsx
└── generate_reports.py
下面的示例读取 Excel 第一个工作表,以每行数据生成一份 Word 文档,并把结果保存到 output 文件夹。
三、编写批量生成脚本
将以下代码保存为 generate_reports.py:
from datetime import date, datetime
from pathlib import Path
import re
from docx import Document
from openpyxl import load_workbook
EXCEL_PATH = Path("data.xlsx")
TEMPLATE_PATH = Path("template.docx")
OUTPUT_DIR = Path("output")
SHEET_NAME = None # None 表示使用第一个工作表;也可填写工作表名称
FILENAME_FIELD = "项目名称" # 用于命名文件的字段
def iter_paragraphs(container):
"""遍历正文、表格以及嵌套表格中的段落。"""
for paragraph in container.paragraphs:
yield paragraph
for table in container.tables:
for row in table.rows:
for cell in row.cells:
yield from iter_paragraphs(cell)
def iter_document_paragraphs(document):
"""遍历正文、表格、页眉和页脚中的段落。"""
yield from iter_paragraphs(document)
for section in document.sections:
yield from iter_paragraphs(section.header)
yield from iter_paragraphs(section.footer)
def to_text(value):
"""把 Excel 单元格值转换成适合填入 Word 的文本。"""
if value is None:
return ""
if isinstance(value, datetime):
if value.time().isoformat() == "00:00:00":
return value.date().isoformat()
return value.isoformat(sep=" ")
if isinstance(value, date):
return value.isoformat()
if isinstance(value, float) and value.is_integer():
return str(int(value))
return str(value).strip()
def safe_filename(value):
"""移除常见文件名禁用字符,避免保存失败。"""
name = to_text(value)
name = re.sub(r'[<>:"/\|?*x00-x1f]', "_", name)
name = name.strip(" .")
return name or "未命名报告"
def replace_tokens(document, row_data):
"""替换所有单个文本片段中的占位符。"""
for paragraph in iter_document_paragraphs(document):
for run in paragraph.runs:
for field, value in row_data.items():
run.text = run.text.replace(
"{{" + field + "}}",
to_text(value),
)
def find_tokens(document):
"""找出模板正文中尚未填充的占位符。"""
tokens = set()
for paragraph in iter_document_paragraphs(document):
tokens.update(re.findall(r"{{([^{}]+)}}", paragraph.text))
return tokens
def unique_output_path(base_name):
"""遇到重名时追加序号,避免覆盖已有文件。"""
path = OUTPUT_DIR / f"{base_name}.docx"
number = 2
while path.exists():
path = OUTPUT_DIR / f"{base_name}_{number}.docx"
number += 1
return path
def main():
if not EXCEL_PATH.exists():
raise FileNotFoundError(f"找不到 Excel 文件:{EXCEL_PATH}")
if not TEMPLATE_PATH.exists():
raise FileNotFoundError(f"找不到 Word 模板:{TEMPLATE_PATH}")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
workbook = load_workbook(
EXCEL_PATH,
data_only=True,
read_only=True,
)
try:
worksheet = (
workbook[SHEET_NAME]
if SHEET_NAME
else workbook[workbook.sheetnames[0]]
)
rows = worksheet.iter_rows(values_only=True)
header_row = next(rows, None)
if not header_row:
raise ValueError("工作表为空,找不到表头。")
headers = [
str(value).strip() if value is not None else ""
for value in header_row
]
if any(not header for header in headers):
raise ValueError("表头中存在空字段名,请补齐后再运行。")
if len(headers) != len(set(headers)):
raise ValueError("表头中存在重名字段,请修改为唯一名称。")
template = Document(TEMPLATE_PATH)
template_tokens = find_tokens(template)
missing_headers = template_tokens - set(headers)
if missing_headers:
names = "、".join(sorted(missing_headers))
raise ValueError(f"Excel 缺少模板所需字段:{names}")
if FILENAME_FIELD not in headers:
raise ValueError(
f"用于命名文件的字段“{FILENAME_FIELD}”不在 Excel 表头中。"
)
generated = 0
for row_number, values in enumerate(rows, start=2):
if not values or all(value is None for value in values):
continue
row_data = dict(zip(headers, values))
# 空单元格会填成空文本;若业务不允许空值,可在这里改为报错。
document = Document(TEMPLATE_PATH)
replace_tokens(document, row_data)
leftovers = find_tokens(document)
if leftovers:
names = "、".join(sorted(leftovers))
raise ValueError(
f"Excel 第 {row_number} 行生成后仍有未替换字段:{names}。"
"请检查模板占位符是否被 Word 拆分,或字段名是否一致。"
)
filename = safe_filename(row_data.get(FILENAME_FIELD))
output_path = unique_output_path(filename)
document.save(output_path)
generated += 1
print(f"已生成:{output_path}")
print(f"完成,共生成 {generated} 份文档。")
finally:
workbook.close()
if __name__ == "__main__":
main()
运行脚本:
python generate_reports.py
生成的文档会放进 output 文件夹。代码默认使用第一个工作表,并以“项目名称”作为文件名;如果你想按报告编号命名,把 FILENAME_FIELD 改成 Excel 中对应的表头即可。如果要使用指定工作表,把 SHEET_NAME = None 改成工作表名称,例如:
SHEET_NAME = "周报数据"
四、处理缺失字段和特殊内容
示例会把空单元格填成空文本。如果某个字段是必填项,可以在生成前检查每一行,发现空值就停止,而不是输出不完整的报告。例如,将以下检查放到循环内、replace_tokens 之前:
required_fields = ["项目名称", "负责人", "统计日期"]
empty_fields = [
field for field in required_fields
if not to_text(row_data.get(field))
]
if empty_fields:
names = "、".join(empty_fields)
raise ValueError(f"Excel 第 {row_number} 行必填字段为空:{names}")
普通中文、换行和常见标点可以作为文本填入文档。需要留意的是:
- Excel 公式值可能为空或过期。 示例使用
data_only=True读取公式的缓存结果;如果工作簿尚未重新计算,读到的结果可能不符合预期。先用 Excel 打开文件并重新计算、保存,再运行脚本。 - 检查 Word 模板中的占位符是否被拆分。 如果脚本提示某个字段没有替换,先在 Word 中删除该占位符并重新连续输入,不要对其中一部分单独设置格式。
- 特殊排版要单独验证。 示例处理正文、表格、页眉和页脚中的段落,但不负责复杂的版式重排;对于文本框等对象,应先用实际模板测试是否能正常替换。
- 文件名中的禁用字符会被替换为下划线。 如果多个项目名称相同,脚本会在文件名后追加序号,避免覆盖已有文件。
五、发布前逐份复核
批量生成只负责把字段写进文档,不代表内容已经准确或适合发布。至少检查:
- 数据对应关系: 项目名称、负责人、日期和进度是否来自正确的 Excel 行。
- 关键信息是否为空: 尤其是标题、金额、日期、联系人等不能遗漏的字段。
- 格式是否完整: 表格、分页、页眉页脚和长段落是否出现错位、截断或多余空白。
- 输出数量与命名: 文件数量是否符合预期,文件名是否清晰且没有误覆盖。
- 内容是否需要人工判断: 数据摘要或正式结论应由负责人审核,不要把模板填充结果直接当作未经复核的发布内容。
用固定模板和一致的表头,才能让 Python 稳定地处理重复性文档填充。先拿少量测试数据跑通模板、空值规则和输出命名,再用于完整批次,可以更早发现字段不匹配或格式问题。

评论列表 (2条):
加载更多评论 Loading...