一、高级表格设计
python-docx 的表格功能远不止简单的行列填充。在实际办公场景中,我们经常需要创建具有合并单元格、自定义边框、单元格着色、跨页标题行等复杂布局的表格。本节将深入讲解 python-docx 中表格的高级操作技巧。
1.1 合并与拆分单元格
合并单元格是表格布局中最常见的需求。python-docx 提供了 merge() 方法,可以将指定范围内的单元格合并为一个。合并时需传入起始和结束单元格对象,table.cell(row1, col1).merge(table.cell(row2, col2))。需要注意的是,合并操作会保留起始单元格的内容,被合并区域其他单元格的内容将被丢弃。虽然 python-docx 没有内置的拆分单元格方法,但我们可以通过删除合并标记(删除 XML 中的 vMerge 和 gridSpan 属性)来"拆分"已合并的单元格。跨列合并使用 gridSpan 属性,跨行合并使用 vMerge 属性。
from docx import Document
from docx.shared import Inches, Pt, Cm, RGBColor
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn
doc = Document()
table = doc.add_table(rows=5, cols=4)
table.alignment = WD_TABLE_ALIGNMENT.CENTER
# 合并第一行的所有列作为标题行
cell_a = table.cell(0, 0)
cell_b = table.cell(0, 3)
cell_a.merge(cell_b)
cell_a.text = "合并后的标题行(跨4列)"
# 合并第一列的2-4行
cell_c = table.cell(1, 0)
cell_d = table.cell(3, 0)
cell_c.merge(cell_d)
cell_c.text = "合并后的左侧单元格(跨3行)"
# 通过XML底层访问gridSpan实现单元格跨列
cell_e = table.cell(4, 1)
tc = cell_e._tc
tcPr = tc.get_or_add_tcPr()
# 设置gridSpan为2,跨2列
gridSpan = tcPr.makeelement(qn('w:gridSpan'), {qn('w:val'): '2'})
tcPr.append(gridSpan)
cell_e.text = "通过XML设置gridSpan跨2列"
doc.save('table_merge_demo.docx')
1.2 表格样式自定义与单元格格式化
python-docx 内置了超过40种表格样式(如 'Light Grid Accent 1'、'Medium Shading 1 Accent 2'),但很多时候我们需要更精细的样式控制。单元格级别的样式设置包括:设置单元格宽度(cell.width)、垂直对齐方式(cell.vertical_alignment)、单元格边距(cell.margin)、背景色(通过XML操作shd元素)、字体和段落格式。通过操作单元格的段落对象,还可以对每个单元格内部的文本进行精细化排版,包括设置行距、缩进、项目符号等。
from docx import Document
from docx.shared import Inches, Pt, RGBColor, Emu
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml
doc = Document()
table = doc.add_table(rows=3, cols=3)
table.style = 'Table Grid'
for row_idx in range(3):
for col_idx in range(3):
cell = table.cell(row_idx, col_idx)
cell.text = f"R{row_idx+1}C{col_idx+1}"
# 设置单元格宽度
cell.width = Inches(2)
# 设置垂直居中
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
# 通过XML设置单元格背景色
shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{"DAF7A6" if (row_idx + col_idx) % 2 == 0 else "FFEAA7"}" w:val="clear"/>')
cell._tc.get_or_add_tcPr().append(shading_elm)
# 设置单元格内边距(通过XML)
tcPr = cell._tc.get_or_add_tcPr()
tcMar = parse_xml(f'<w:tcMar {nsdecls("w")}>'
f'<w:top w:w="60" w:type="dxa"/>'
f'<w:left w:w="120" w:type="dxa"/>'
f'<w:bottom w:w="60" w:type="dxa"/>'
f'<w:right w:w="120" w:type="dxa"/>'
f'</w:tcMar>')
tcPr.append(tcMar)
# 设置表格边框样式(细线灰色边框)
tbl = table._tbl
tblPr = tbl.tblPr if tbl.tblPr is not None else parse_xml(f'<w:tblPr {nsdecls("w")}/>')
borders = parse_xml(
f'<w:tblBorders {nsdecls("w")}>'
f'<w:top w:val="single" w:sz="4" w:space="0" w:color="999999"/>'
f'<w:left w:val="single" w:sz="4" w:space="0" w:color="999999"/>'
f'<w:bottom w:val="single" w:sz="4" w:space="0" w:color="999999"/>'
f'<w:right w:val="single" w:sz="4" w:space="0" w:color="999999"/>'
f'<w:insideH w:val="single" w:sz="4" w:space="0" w:color="999999"/>'
f'<w:insideV w:val="single" w:sz="4" w:space="0" w:color="999999"/>'
f'</w:tblBorders>')
tblPr.append(borders)
# 标题行加粗
for cell in table.rows[0].cells:
for paragraph in cell.paragraphs:
for run in paragraph.runs:
run.bold = True
run.font.color.rgb = RGBColor(0x2E, 0x7D, 0x32)
doc.save('table_styling_demo.docx')
1.3 跨页表格与重复标题行
当表格跨越多页时,默认情况下标题行不会在每一页顶部重复,这会严重影响表格的可读性。python-docx 通过设置 row heading 属性可以轻松实现标题行重复。使用 row.is_header = True 可以将指定行标记为标题行,Word 会在每一页自动重复这些行。此外,还可以通过 XML 操作设置表格的"允许跨页断行"属性(allowOverlap/allowAutomaticSplit),控制表格是否允许在行中间分页。对于需要"底部标题"的特殊场景,可以手动在每页底部插入汇总行。建议跨页表格打开"标题行重复"功能的同时,对标题行应用区别于正文行的背景色,确保视觉上的清晰区分。
from docx import Document
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml
doc = Document()
table = doc.add_table(rows=25, cols=5)
table.style = 'Table Grid'
# 填写标题行
headers = ["序号", "姓名", "部门", "职位", "入职日期"]
for i, h in enumerate(headers):
table.cell(0, i).text = h
# 标记第一行为标题行(跨页重复)
table.rows[0].is_header = True
# 填写数据行
for row_idx in range(1, 25):
data = [str(row_idx), "员工" + str(row_idx), "技术部", "工程师", "2025-01-01"]
for col_idx in range(5):
table.cell(row_idx, col_idx).text = data[col_idx]
# 设置表格允许行间分页
tbl = table._tbl
tblPr = tbl.tblPr
if tblPr is None:
tblPr = parse_xml(f'<w:tblPr {nsdecls("w")}/>')
tbl.insert(0, tblPr)
# 添加allowAutomaticSplit属性
cantSplit = tblPr.find(qn('w:cantSplit'))
if cantSplit is not None:
tblPr.remove(cantSplit)
doc.save('跨页表格标题行重复.docx')
1.4 表格中的公式计算
虽然 python-docx 不直接支持在表格中插入 Word 域公式(如 SUM、AVERAGE 等),但我们可以通过插入 Word 域代码(Field Codes)来实现。核心思路是操作 XML 在单元格中插入 fldChar 和 instrText 元素。常见的公式包括:=SUM(ABOVE) 计算上方数值之和、=AVERAGE(LEFT) 计算左侧平均值、=MAX(RIGHT) 取右侧最大值等。更复杂的场景可以先将数据在 Python 中计算好,再将结果填入表格,这是更可靠的方式。
from docx import Document
from docx.oxml.ns import qn, nsdecls
from docx.oxml import OxmlElement
doc = Document()
table = doc.add_table(rows=6, cols=4)
table.style = 'Table Grid'
# 填入数据
headers = ["月份", "销售额", "成本", "利润"]
for i, h in enumerate(headers):
table.cell(0, i).text = h
data = [("一月", 10000, 6000), ("二月", 12000, 7000),
("三月", 15000, 8500), ("四月", 13000, 7500)]
for r, (month, sales, cost) in enumerate(data, start=1):
table.cell(r, 0).text = month
table.cell(r, 1).text = str(sales)
table.cell(r, 2).text = str(cost)
# 在Python中直接计算结果,比Word公式更可靠
table.cell(r, 3).text = str(sales - cost)
# 最后一行:合计 - 使用Python计算而非Word域
total_sales = sum(d[1] for d in data)
total_cost = sum(d[2] for d in data)
table.cell(5, 0).text = "合计"
table.cell(5, 1).text = str(total_sales)
table.cell(5, 2).text = str(total_cost)
table.cell(5, 3).text = str(total_sales - total_cost)
# 标记最后一行为汇总行
for cell in table.rows[5].cells:
for paragraph in cell.paragraphs:
for run in paragraph.runs:
run.bold = True
doc.save('table_formula_demo.docx')
最佳实践:在设计表格时,建议先规划好行数和列数,再执行合并操作。对于数据量大的表格,分批写入比逐单元格写入效率更高。表格样式尽量使用内置样式作为基础,再通过 XML 操作微调,避免完全从零定义样式。
二、图片精确控制
在 Word 文档中插入图片看似简单,但要做到精确控制位置、大小、环绕方式和裁剪效果,需要深入理解 python-docx 的图片处理机制。python-docx 基于 OPC(Open Packaging Convention)协议操作图片,图片被作为独立资源文件打包在 docx 文件中。
2.1 图片插入与内联/浮动布局
python-docx 支持两种图片布局模式:内联式(Inline)和浮动式(Anchor)。内联式图片被当作一个"大字符"处理,随文本流移动,使用 add_picture() 方法即可添加。浮动式图片可以精确指定页面坐标,文字可以环绕在图片周围,需要使用 add_picture() + XML 操作转换为 anchor 元素。浮动图片支持设置绝对位置(相对于页面、段落或页边距)和相对位置(如百分比)。此外,还可以控制图片的 z-index(重叠顺序)和文本环绕方式(上下型、四方形、紧密型、穿越型等)。
from docx import Document
from docx.shared import Inches, Pt, Cm, Emu
from docx.enum.text import WD_ALIGN_PARAGRAPH
import docx.oxml.ns as ns
from lxml import etree
doc = Document()
# 内联式图片(跟随文本流)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run()
# 插入图片并指定大小
inline_pic = run.add_picture('chart.png', width=Inches(5.5))
# 浮动式图片:通过操作XML实现文字环绕
def add_float_picture(doc, img_path, left, top, width, height):
"""添加浮动式图片,支持精确位置和文字环绕"""
paragraph = doc.add_paragraph()
run = paragraph.add_run()
inline_shape = run.add_picture(img_path, width=width, height=height)
# 获取内联图片的XML
inline = inline_shape._inline
# 创建浮动图片的anchor XML结构
anchor_xml = (
f'<w:drawing xmlns:wp="{ns.WML_NS_MAIN}">'
f'<wp:anchor simplePos="0" relativeHeight="251658240"'
f' behindDoc="0" locked="0" layoutInCell="1" allowOverlap="1">'
f'<wp:simplePos x="0" y="0"/>'
f'<wp:positionH relativeFrom="column">'
f'<wp:posOffset>{left}</wp:posOffset>'
f'</wp:positionH>'
f'<wp:positionV relativeFrom="paragraph">'
f'<wp:posOffset>{top}</wp:posOffset>'
f'</wp:positionV>'
f'<wp:extent cx="{inline.extent.cx}" cy="{inline.extent.cy}"/>'
f'<wp:wrapSquare wrapText="both"/>'
f'<wp:docPr id="2" name="浮动图片"/>'
f'<wp:cNvGraphicFramePr/>'
f'<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
f'<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">'
f'{etree.tostring(inline.findall(".//" + qn("a:graphicData"))[0][0]).decode()}</a:graphicData></a:graphic>'
f'</wp:anchor></w:drawing>'
)
return anchor_xml
# 使用浮动图片
try:
anchor = add_float_picture(doc, 'logo.png',
left=Emu(Cm(2)), top=Emu(Cm(5)),
width=Cm(4), height=Cm(3))
doc.paragraphs[-1]._p.append(anchor)
except FileNotFoundError:
doc.add_paragraph("[图片占位:logo.png]")
doc.save('图片布局演示.docx')
2.2 图片裁剪与效果
python-docx 原生不直接支持图片裁剪 API,但我们可以通过操作底层的 DrawingML 元素来实现。图片裁剪本质上是修改图片的 srcRect 属性,该属性定义了可视区域在原图中的坐标范围(l=left, t=top, r=right, b=bottom,单位为 emu)。你也可以通过 blipFill 元素的 stretch 或 tile 属性来控制图片填充方式。此外,还可以在 DrawingML 层面添加图片效果,如阴影(outerShdw)、反射(reflection)、发光(glow)和柔化边缘(softEdge)等。这些效果通过操作 a:effectLst 元素实现。
from docx import Document
from docx.shared import Inches, Pt, Emu
from docx.oxml.ns import qn
from lxml import etree
doc = Document()
p = doc.add_paragraph()
run = p.add_run()
# 插入图片
pic = run.add_picture('photo.jpg', width=Inches(4))
# 获取图片的blip元素(图片引用)
inline = pic._inline
graphic = inline.findall('.//' + qn('a:blip'))[0]
# 添加裁剪区域:从左边裁剪10%,从顶部裁剪5%
# 使用srcRect属性:l=left, t=top, r=right, b=bottom (单位:千分之一)
src_rect = etree.SubElement(graphic, qn('a:srcRect'))
src_rect.set('l', '10000') # 左裁剪10%
src_rect.set('t', '5000') # 顶部裁剪5%
src_rect.set('r', '5000') # 右裁剪5%
src_rect.set('b', '5000') # 底部裁剪5%
# 添加图片阴影效果
spPr = inline.findall('.//' + qn('a:spPr'))[0]
effectLst = etree.SubElement(spPr, qn('a:effectLst'))
outerShdw = etree.SubElement(effectLst, qn('a:outerShdw'))
outerShdw.set('blurRad', '50000') # 阴影模糊半径
outerShdw.set('dist', '100000') # 阴影距离
outerShdw.set('dir', '2700000') # 阴影方向(角度)
outerShdw.set('algn', 'tl')
srgbClr = etree.SubElement(outerShdw, qn('a:srgbClr'))
srgbClr.set('val', '888888') # 灰色阴影
etree.SubElement(srgbClr, qn('a:alpha')).set('val', '50000') # 透明度50%
doc.save('图片裁剪与阴影.docx')
2.3 批量插入图片与图片占位
在实际项目中,经常需要从一个文件夹中批量读取所有图片并插入 Word 文档。批量处理时需要注意图片排序、错误处理(损坏的图片文件)、以及内存管理(大图片可以先缩放再插入)。图片占位技术可用于首先生成文档框架,后续再替换为实际图片。占位可以使用特定的文本标记(如 {{placeholder:chart1}}),或者使用一个占位图(如全白或全灰的图片),后续通过替换图片引用来完成。
from docx import Document
from docx.shared import Inches, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
import os, glob
doc = Document()
doc.add_heading('批量图片插入示例', level=1)
# 批量从目录读取并插入图片
image_dir = './images'
image_files = sorted(glob.glob(os.path.join(image_dir, '*.png')) +
glob.glob(os.path.join(image_dir, '*.jpg')))
if not image_files:
doc.add_paragraph("未找到图片文件,请确保 images 目录存在且包含 PNG 或 JPG 图片。")
else:
for idx, img_path in enumerate(image_files, 1):
filename = os.path.basename(img_path)
doc.add_heading(f"图{idx}: {filename}", level=2)
# 获取图片尺寸,宽图限制宽度,高图限制高度
# 这里使用固定宽度插入,保持一致性
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
try:
run = p.add_run()
run.add_picture(img_path, width=Inches(5))
except Exception as e:
doc.add_paragraph(f"[图片加载失败:{filename} - {str(e)}]")
# 使用占位标记替换技术
# 先插入文本占位,后续再替换为真实图片
doc.add_heading('图片占位示例', level=2)
p = doc.add_paragraph("下方是图表区域:")
p.add_run('\n {{chart:revenue_2025}}').font.color.rgb = '#999999'
# 后续替换占位文本为图片的代码
# for p in doc.paragraphs:
# if '{{chart:revenue_2025}}' in p.text:
# p.clear()
# run = p.add_run()
# run.add_picture('revenue_chart.png', width=Inches(5.5))
doc.save('批量插入图片.docx')
三、XML底层操作
python-docx 是一个高层封装库,底层操作实际上围绕 Open XML 标准(ISO 29500)展开。理解 python-docx 的内部 XML 结构,可以帮助我们突破 API 的限制,实现任意复杂的文档操作。每个文档元素(段落、表格、图文框等)本质上都是 XML 树的节点。
3.1 python-docx 内部结构与 lxml 操作
python-docx 的每个核心对象都持有 _element 属性,直接指向底层的 lxml 元素。例如,paragraph._p 指向 w:p 元素,run._r 指向 w:r 元素,table._tbl 指向 w:tbl 元素。通过这个 _element 属性,我们可以直接使用 lxml 库的 API(如 find()、findall()、makeelement()、append()、remove() 等)来操作文档底层 XML。这意味着 python-docx API 做不到的事情,都可以通过直接操作 XML 来完成。建议在操作前先使用 etree.tostring() 打印当前元素的 XML 结构,了解文档的内部表示。
from docx import Document
from lxml import etree
from docx.oxml.ns import qn, nsdecls, NSMAP
doc = Document()
# 1. 查看段落内部XML结构
p = doc.add_paragraph("Hello, python-docx!")
run = p.add_run(" 这是一个粗体文本")
run.bold = True
# 打印段落XML
print(etree.tostring(p._p, pretty_print=True).decode())
# 输出示例(简化):
# <w:p>
# <w:r><w:t>Hello, python-docx!</w:t></w:r>
# <w:r>
# <w:rPr><w:b/></w:rPr>
# <w:t> 这是一个粗体文本</w:t>
# </w:r>
# </w:p>
# 2. 通过lxml查找特定元素
# 查找所有w:r(run)元素
runs = p._p.findall(qn('w:r'))
print(f"段落中包含 {len(runs)} 个run元素")
# 3. 直接创建XML元素
# 在run中手工添加高亮标记
for r in runs:
rPr = r.find(qn('w:rPr'))
if rPr is None:
rPr = etree.SubElement(r, qn('w:rPr'))
# 添加黄色高亮
highlight = etree.SubElement(rPr, qn('w:highlight'))
highlight.set(qn('w:val'), 'yellow')
# 4. 直接删除XML元素
# 删除第二个run(索引1)
if len(runs) > 1:
p._p.remove(runs[1])
doc.save('xml_operation_demo.docx')
3.2 自定义 XML 元素与高级属性
通过 lxml 直接创建和插入 XML 元素,我们可以实现 python-docx API 不支持的功能。例如:自定义文档属性(Custom XML Data)、添加内容控件标签、设置段落边框和底纹、创建自动编号列表、插入公式(Office Math)、添加 ActiveX 控件等。这些功能都只需要按照 OOXML 规范构造对应的 XML 树并插入到正确位置即可。关键是理解 w:body 下的子元素顺序和关系,以及使用正确的命名空间。
from docx import Document
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml, OxmlElement
from lxml import etree
doc = Document()
p = doc.add_paragraph()
# 1. 自定义段落边框
# 使用parse_xml从字符串创建XML
pBdr = parse_xml(
f'<w:pBdr {nsdecls("w")}>'
f'<w:top w:val="single" w:sz="12" w:space="4" w:color="2E7D32"/>'
f'<w:left w:val="single" w:sz="12" w:space="4" w:color="2E7D32"/>'
f'<w:bottom w:val="single" w:sz="12" w:space="4" w:color="2E7D32"/>'
f'<w:right w:val="single" w:sz="12" w:space="4" w:color="2E7D32"/>'
f'</w:pBdr>'
)
pPr = p._p.get_or_add_pPr()
pPr.append(pBdr)
# 2. 自定义段落底纹
shd = parse_xml(f'<w:shd {nsdecls("w")} w:val="clear" w:color="auto" w:fill="E8F5E9"/>')
pPr.append(shd)
# 3. 设置行距(精确值)
spacing = parse_xml(f'<w:spacing {nsdecls("w")} w:line="360" w:lineRule="auto"/>')
pPr.append(spacing)
# 4. 添加制表位
tabs = parse_xml(
f'<w:tabs {nsdecls("w")}>'
f'<w:tab w:val="center" w:pos="4680"/>'
f'<w:tab w:val="right" w:pos="9360"/>'
f'</w:tabs>'
)
pPr.append(tabs)
# 5. 在段落中插入一个字段(如日期域)
run = p.add_run("当前日期:")
fldChar_begin = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>')
fldCode = parse_xml(f'<w:instrText {nsdecls("w")} xml:space="preserve"> DATE \\@ "yyyy年M月d日" </w:instrText>')
fldChar_separate = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>')
fldChar_end = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>')
r1 = OxmlElement('w:r')
r1.append(fldChar_begin)
p._p.append(r1)
r2 = OxmlElement('w:r')
r2.append(fldCode)
p._p.append(r2)
r3 = OxmlElement('w:r')
r3.append(fldChar_separate)
p._p.append(r3)
r4 = OxmlElement('w:r')
r4.append(fldChar_end)
p._p.append(r4)
doc.save('自定义段落样式.docx')
3.3 访问底层属性与文档部件
一个 docx 文件本质上是一个 ZIP 包,包含多个 XML 文件(文档主体、样式、页眉页脚、脚注、图片资源等)。python-docx 的 doc.part 对象提供了对文档部件的访问。通过 doc_part 可以访问 styles.xml、settings.xml、fontTable.xml 等。使用 doc.settings 可以控制文档的设置(如自动更正选项、拼写检查、密码保护等)。通过 doc.core_properties 可以访问和修改文档元数据(作者、标题、标签、评论等)。这些底层 API 为高级文档自动化提供了无限可能。
from docx import Document
from lxml import etree
from docx.oxml.ns import qn
doc = Document()
# 1. 访问和修改文档核心属性
props = doc.core_properties
props.title = "python-docx进阶教程"
props.author = "学习笔记"
props.category = "Python办公自动化"
props.comments = "表格、图片与样式美化专题"
props.keywords = "python-docx,表格,图片,样式"
props.last_modified_by = "Claude Code"
# 2. 访问文档设置
# 查看默认字体设置
styles_xml = doc.styles.element
# 获取默认段落样式中的字体设置
default_style = doc.styles['Normal']
print(f"默认字体: {default_style.font.name}")
print(f"默认字号: {default_style.font.size}")
# 3. 直接访问文档的XML body
body = doc.element.body
print(f"body元素标签: {body.tag}")
# 统计段落数
paragraphs_xml = body.findall(qn('w:p'))
print(f"XML层段落数: {len(paragraphs_xml)}")
# 4. 添加自定义XML数据(Custom XML Part)
# 这允许我们在文档中嵌入机器可读的结构化数据
custom_xml = etree.fromstring(
'<root xmlns="http://example.com/custom">'
'<document>'
' <title>python-docx高级教程</title>'
' <version>1.0</version>'
' <createdBy>自动化系统</createdBy>'
'</document>'
'</root>'
)
# 注意:添加Custom XML Part需要通过part的底层API
doc_part = doc.part
# doc_part.dropdown_part() 实际开发中用 doc_part.related_parts 访问
# 更簡單的方式是直接操作ZIP包
doc.save('文档属性设置演示.docx')
四、样式深度定制
Word 的样式系统是文档格式化的核心。python-docx 提供了对 Word 样式系统的完整支持,包括字符样式、段落样式、表格样式、列表样式和编号样式。理解样式继承链(LinkedStyle → Paragraph → Character → Table → List)是进行深度样式定制的关键。
4.1 样式继承链与覆盖规则
Word 样式存在严格的优先级规则:直接格式化(直接设置在 run 或段落上的属性)优先级最高,其次是段落样式,再是默认段落样式,最后是文档默认设置。当多个样式作用于同一文本时,属性覆盖规则遵循"最近优先"原则。python-docx 的 style.font 和 style.paragraph_format 分别控制字符级和段落级属性。创建新样式时可以指定基于哪个现有样式(style.base_style),后续如果修改基样式,派生样式会自动继承变化。
from docx import Document
from docx.shared import Pt, Inches, RGBColor, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml
doc = Document()
# 1. 查看样式继承链
normal = doc.styles['Normal']
print(f"Normal样式基于: {normal.base_style.name if normal.base_style else '无'}")
print(f"Normal样式的类型: {normal.type}")
# 2. 创建自定义段落样式
# 基于Normal创建代码块样式
code_style = doc.styles.add_style('CodeBlock', 1) # 1 = 段落样式
code_style.base_style = doc.styles['Normal']
code_style.font.name = 'Consolas'
code_style.font.size = Pt(9.5)
code_style.font.color.rgb = RGBColor(0x1E, 0x1E, 0x1E)
code_style.paragraph_format.space_before = Pt(6)
code_style.paragraph_format.space_after = Pt(6)
code_style.paragraph_format.line_spacing = 1.2
# 添加段落底纹(通过XML操作)
pPr = code_style.element.get_or_add_pPr()
shd = parse_xml(f'<w:shd {nsdecls("w")} w:val="clear" w:color="auto" w:fill="F5F5F5"/>')
pPr.append(shd)
# 3. 创建字符样式
# 字符样式只影响字体属性,不包含段落格式
highlight_style = doc.styles.add_style('HighlightText', 2) # 2 = 字符样式
highlight_style.font.name = 'Microsoft YaHei'
highlight_style.font.size = Pt(11)
highlight_style.font.bold = True
highlight_style.font.color.rgb = RGBColor(0x2E, 0x7D, 0x32)
# 添加下划线
highlight_style.font.underline = True
# 4. 使用自定义样式
p1 = doc.add_paragraph("这是一段Normal样式的文本")
p2 = doc.add_paragraph("这是一段CodeBlock样式的代码文本", style='CodeBlock')
p3 = doc.add_paragraph()
run = p3.add_run("高亮文本部分")
run.style = doc.styles['HighlightText']
doc.save('自定义样式演示.docx')
4.2 创建表格样式与列表样式
表格样式控制整个表格的视觉呈现,包括边框、底纹、字体和对齐方式。python-docx 支持创建基于内置样式的新表格样式。列表样式则控制编号和项目符号的格式,包括多级列表。创建列表样式相对复杂,需要操作 XML 定义 numFmt 和 numPr 元素。对于大多数使用场景,推荐先设置好列表的基础样式,再通过 add_paragraph() 的 style 参数应用。
from docx import Document
from docx.shared import Pt, Inches, RGBColor, Emu
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml, OxmlElement
from docx.enum.style import WD_STYLE_TYPE
doc = Document()
# 1. 创建自定义表格样式
tbl_style = doc.styles.add_style('CustomTableStyle', WD_STYLE_TYPE.TABLE)
tbl_style.base_style = doc.styles['Table Grid']
# 设置表格属性
tbl_pr = tbl_style.element.find(qn('w:tblPr'))
if tbl_pr is None:
tbl_pr = OxmlElement('w:tblPr')
tbl_style.element.append(tbl_pr)
# 设置表格边框
borders = parse_xml(
f'<w:tblBorders {nsdecls("w")}>'
f'<w:top w:val="single" w:sz="12" w:space="0" w:color="2E7D32"/>'
f'<w:left w:val="single" w:sz="12" w:space="0" w:color="2E7D32"/>'
f'<w:bottom w:val="single" w:sz="12" w:space="0" w:color="2E7D32"/>'
f'<w:right w:val="single" w:sz="12" w:space="0" w:color="2E7D32"/>'
f'<w:insideH w:val="single" w:sz="4" w:space="0" w:color="CCCCCC"/>'
f'<w:insideV w:val="single" w:sz="4" w:space="0" w:color="CCCCCC"/>'
f'</w:tblBorders>'
)
tbl_pr.append(borders)
# 2. 通过XML创建多级列表样式
# 添加编号定义(需要操作numbering.xml)
numbering_part = doc.part.numbering_part
# 注意:创建真正的多级列表需要在numbering.xml中添加abstractNum和num元素
# 这里展示一个简化的方法:使用python-docx的API
# 使用内置列表样式创建列表
p1 = doc.add_paragraph("第一级列表项一", style='List Number')
p2 = doc.add_paragraph("第一级列表项二", style='List Number')
p3 = doc.add_paragraph("第一级列表项三", style='List Number')
# 3. 应用自定义表格样式
table = doc.add_table(rows=4, cols=3)
table.style = doc.styles['CustomTableStyle']
headers = ["项目", "数量", "金额"]
for i, h in enumerate(headers):
table.cell(0, i).text = h
for row in range(1, 4):
table.cell(row, 0).text = f"项目{row}"
table.cell(row, 1).text = str(row * 10)
table.cell(row, 2).text = str(row * 100)
doc.save('表格与列表样式.docx')
4.3 主题颜色与样式复制
Word 主题颜色(Theme Colors)提供了一组相互协调的颜色方案,包括六种强调色、四种文字/背景色和两种超链接色。通过样式复制,可以将一个文档中的样式导入到另一个文档,实现样式的跨文档复用。样式复制需要操作 styles.xml,将源文档的 style 元素复制到目标文档中。更实用的方法是使用样式模板(.dotx 或 .dotm 文件),将所有自定义样式保存在模板中,新文档只需基于该模板创建即可自动获得所有样式。
from docx import Document
from docx.shared import Pt, RGBColor
from lxml import etree
from docx.oxml.ns import qn
import copy
doc = Document()
# 1. 主题颜色索引常量
# w:val 的取值范围:
# dark1-dark2, light1-light2, accent1-accent6,
# hyperlink, followedHyperlink
# 使用主题颜色(而非RGB)
def apply_theme_color(run, theme_color='accent1', shade='light'):
"""为run应用主题颜色"""
rPr = run._r.get_or_add_rPr()
# 先清除现有颜色
for color_elem in rPr.findall(qn('w:color')):
rPr.remove(color_elem)
# 创建schemeClr元素
from docx.oxml import OxmlElement
color = OxmlElement('w:color')
color.set(qn('w:val'), 'auto')
rPr.append(color)
# 使用schemeClr
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
scheme = parse_xml(f'<w:schemeClr {nsdecls("w")} w:val="{theme_color}"/>')
rPr.append(scheme)
# 2. 样式复制函数
def copy_style(source_doc, target_doc, style_name, new_name=None):
"""从一个文档复制样式到另一个文档"""
source_styles = source_doc.styles
target_styles = target_doc.styles
if style_name not in [s.name for s in source_styles]:
raise ValueError(f"样式'{style_name}'在源文档中不存在")
source_style = source_styles[style_name]
target_name = new_name or style_name
# 在目标文档中创建对应类型的样式
new_style = target_styles.add_style(target_name, source_style.type)
# 复制字体属性
if source_style.font.name:
new_style.font.name = source_style.font.name
if source_style.font.size:
new_style.font.size = source_style.font.size
if source_style.font.bold is not None:
new_style.font.bold = source_style.font.bold
if source_style.font.color.rgb:
new_style.font.color.rgb = source_style.font.color.rgb
# 复制段落格式
if source_style.paragraph_format.alignment:
new_style.paragraph_format.alignment = source_style.paragraph_format.alignment
if source_style.paragraph_format.space_before:
new_style.paragraph_format.space_before = source_style.paragraph_format.space_before
if source_style.paragraph_format.space_after:
new_style.paragraph_format.space_after = source_style.paragraph_format.space_after
if source_style.paragraph_format.line_spacing:
new_style.paragraph_format.line_spacing = source_style.paragraph_format.line_spacing
return new_style
# 在文档里创建并应用带主题颜色的文本
p = doc.add_paragraph()
run = p.add_run("使用主题颜色accent1的文本")
apply_theme_color(run, 'accent1')
p2 = doc.add_paragraph()
run2 = p2.add_run("使用主题颜色accent3的文本")
apply_theme_color(run2, 'accent3')
doc.save('主题颜色与样式复制.docx')
五、内容控件
内容控件(Content Controls)是 Word 中强大的交互式元素,也称为结构化文档标签(SDT)。它们可以为文档添加可编辑区域、下拉列表、日期选择器、复选框等功能,非常适合创建模板文档和表单。python-docx 通过操作 sdt(Structured Document Tag)XML 元素来创建和管理内容控件。
5.1 纯文本控件与下拉列表控件
纯文本内容控件(wdContentControlText)提供了一个可编辑的文本区域,可以限制字符数、设置占位文本、控制是否允许换行。下拉列表控件(wdContentControlDropdownList)允许用户从预定义的选项中选择,每个选项包含显示文本和存储值。创建下拉列表需要构造 sdt 元素并添加 listItem 子元素。这两种控件都支持设置标题标签(tag)和标题名称(title),便于程序化识别和取值。
from docx import Document
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml, OxmlElement
from lxml import etree
doc = Document()
doc.add_heading('内容控件演示', level=1)
# 1. 纯文本内容控件
p_text = doc.add_paragraph("姓名:")
def add_text_sdt(paragraph, tag, title, placeholder="请输入...", max_length=0):
"""添加纯文本内容控件"""
sdt = parse_xml(f'<w:sdt {nsdecls("w")}>'
f'<w:sdtPr>'
f'<w:tag w:val="{tag}"/>'
f'<w:id w:val="0"/>'
f'<w:showingPlcHdr/>'
f'</w:sdtPr>'
f'<w:sdtContent>'
f'<w:r><w:t>{placeholder}</w:t></w:r>'
f'</w:sdtContent>'
f'</w:sdt>')
paragraph._p.append(sdt)
return sdt
add_text_sdt(p_text, 'name', '姓名输入', '请输入您的姓名')
# 2. 下拉列表内容控件
p_dropdown = doc.add_paragraph("部门:")
def add_dropdown_sdt(paragraph, tag, title, items):
"""添加下拉列表内容控件"""
# 构造listItems XML
list_items_xml = ''
for display_text, value in items:
list_items_xml += f'<w:listItem {nsdecls("w")} w:displayText="{display_text}" w:value="{value}"/>'
sdt = parse_xml(f'<w:sdt {nsdecls("w")}>'
f'<w:sdtPr>'
f'<w:tag w:val="{tag}"/>'
f'<w:id w:val="1"/>'
f'<w:dropDownList>'
f'{list_items_xml}'
f'</w:dropDownList>'
f'<w:showingPlcHdr/>'
f'</w:sdtPr>'
f'<w:sdtContent>'
f'<w:r><w:t>{items[0][0] if items else ""}</w:t></w:r>'
f'</w:sdtContent>'
f'</w:sdt>')
paragraph._p.append(sdt)
return sdt
departments = [
("-- 请选择部门 --", ""),
("技术部", "tech"),
("销售部", "sales"),
("财务部", "finance"),
("人力资源部", "hr"),
]
add_dropdown_sdt(p_dropdown, 'dept', '部门选择', departments)
doc.save('内容控件演示.docx')
5.2 日期选择器与复选框控件
日期选择器控件(wdContentControlDate)提供了一个日历界面,用户可以选择日期。可以设置日期的显示格式(如 yyyy-MM-dd、yyyy年M月d日)和默认值。复选框控件(wdContentControlCheckBox)提供了一个可勾选的方框,支持设置选中和未选中状态的符号。复选框在创建模板清单、确认声明等场景中非常实用。对于较新版本的 Word,还支持"复选框内容控件"(与旧版窗体域不同),可以锁定为只读或可编辑状态。
from docx import Document
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml
doc = Document()
doc.add_heading('日期与复选框控件', level=1)
# 1. 日期选择器内容控件
p_date = doc.add_paragraph("入职日期:")
sdt_date = parse_xml(
f'<w:sdt {nsdecls("w")}>'
f'<w:sdtPr>'
f'<w:tag w:val="entry_date"/>'
f'<w:id w:val="10"/>'
f'<w:date w:fullDate="2025-01-01T00:00:00">'
f'<w:placeholder><w:docPart w:val="DefaultPlaceholder_226757571616"/></w:placeholder>'
f'<w:dateFormat w:val="yyyy-MM-dd"/>'
f'<w:lid w:val="zh-CN"/>'
f'</w:date>'
f'<w:showingPlcHdr/>'
f'</w:sdtPr>'
f'<w:sdtContent>'
f'<w:r><w:t>点击选择日期</w:t></w:r>'
f'</w:sdtContent>'
f'</w:sdt>'
)
p_date._p.append(sdt_date)
# 2. 复选框内容控件
p_check = doc.add_paragraph()
def add_checkbox(paragraph, label_text, tag, checked=False):
"""添加复选框内容控件(含标签文本)"""
# 添加标签文本run
run = paragraph.add_run(label_text)
# 创建复选框SDT
sdt = parse_xml(
f'<w:sdt {nsdecls("w")}>'
f'<w:sdtPr>'
f'<w:tag w:val="{tag}"/>'
f'<w:id w:val="20"/>'
f'<w:checkBox>'
f'<w:checked/>' if checked else ''
f'</w:checkBox>'
f'</w:sdtPr>'
f'<w:sdtContent>'
f'<w:r><w:sym w:font="Wingdings 2" w:char="{"£" if checked else "¨"}"/></w:r>'
f'</w:sdtContent>'
f'</w:sdt>'
)
paragraph._p.append(sdt)
add_checkbox(p_check, " 已阅读并同意以上条款", 'agree', checked=False)
# 批量创建清单复选框
items = ["完成需求分析", "设计数据库结构", "实现核心功能", "编写单元测试", "部署到测试环境"]
for item in items:
p = doc.add_paragraph()
add_checkbox(p, " " + item, item[:4], checked=False)
doc.save('日期与复选框控件.docx')
5.3 内容控件映射与数据绑定
内容控件的真正威力在于与自定义 XML 数据绑定。通过将内容控件映射到文档中的自定义 XML 部件(Custom XML Part),可以实现数据的双向同步:用户修改控件内容,底层的 XML 数据自动更新;代码修改 XML 数据,控件显示的内容也自动刷新。这种机制非常适合"动态模板"场景。映射通过 sdtPr 中的 dataBinding 元素实现,指定存储 XML 部件的 xpath 和 storeItemID。
from docx import Document
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml
from lxml import etree
import uuid
doc = Document()
# 1. 创建自定义XML部件
doc_part = doc.part
custom_xml = etree.fromstring(
'<root xmlns="http://example.com/template">'
' <employee>'
' <name>张三</name>'
' <department>技术部</department>'
' <position>高级工程师</position>'
' </employee>'
'</root>'
)
# 添加Custom XML Part到文档包中
# 注意:实际使用时需要通过docx的底层API操作ZIP包
# 这里展示概念和映射结构
# 2. 创建绑定到自定义XML的内容控件
store_item_id = '{' + str(uuid.uuid4()).upper() + '}'
p = doc.add_paragraph("员工姓名:")
sdt_mapped = parse_xml(
f'<w:sdt {nsdecls("w")}>'
f'<w:sdtPr>'
f'<w:tag w:val="mapped_name"/>'
f'<w:id w:val="100"/>'
f'<w:dataBinding w:xpath="/root/employee/name" '
f' w:storeItemID="{store_item_id}" w:prefixMappings=""/>'
f'<w:showingPlcHdr/>'
f'</w:sdtPr>'
f'<w:sdtContent>'
f'<w:r><w:t>张三</w:t></w:r>'
f'</w:sdtContent>'
f'</w:sdt>'
)
p._p.append(sdt_mapped)
doc.save('内容控件数据绑定.docx')
六、邮件合并
邮件合并(Mail Merge)是 Word 最强大的功能之一,可以在模板文档中插入占位字段,然后从数据源批量替换生成多个文档。常见的应用场景包括批量生成合同、通知书、证书、标签、信封等。python-docx 天然支持 Word 邮件合并字段的检测和替换操作。
6.1 邮件合并原理与字段映射
邮件合并的核心思想是"模板 + 数据 = 批量输出"。模板文档中插入 MERGEFIELD 域(合并域),每个域对应数据源中的一个列。数据源可以是数据库、Excel 表格、CSV 文件或自定义 XML。当执行合并时,Word 会从数据源中逐行读取数据,替换模板中的合并域,生成新的文档或直接打印。python-docx 可以识别文档中的合并域(MERGEFIELD),通过遍历文档中的字段代码来获取所有占位字段列表。字段映射是指将模板中的字段名映射到数据源的列名,两者可能不同。
from docx import Document
from docx.oxml.ns import qn
import re
doc = Document()
# 1. 创建邮件合并模板
# 在文档中添加文本形式的合并字段标记(后续替换)
p1 = doc.add_paragraph()
p1.add_run("合同编号:").bold = True
p1.add_run("《{{contract_no}}》")
p2 = doc.add_paragraph()
p2.add_run("甲方(委托方):").bold = True
p2.add_run("{{party_a}}")
p3 = doc.add_paragraph()
p3.add_run("乙方(受托方):").bold = True
p3.add_run("{{party_b}}")
p4 = doc.add_paragraph()
p4.add_run("项目名称:").bold = True
p4.add_run("{{project_name}}")
p5 = doc.add_paragraph()
p5.add_run("合同金额:").bold = True
p5.add_run("¥{{amount}}元(大写:{{amount_cn}})")
p6 = doc.add_paragraph()
p6.add_run("签订日期:").bold = True
p6.add_run("{{sign_date}}")
doc.save('邮件合并模板.docx')
# 2. 检测模板中的合并字段
def detect_merge_fields(doc_path):
"""检测文档中的所有合并占位字段"""
doc = Document(doc_path)
fields = set()
pattern = re.compile(r'\{\{(\w+)\}\}')
for paragraph in doc.paragraphs:
matches = pattern.findall(paragraph.text)
fields.update(matches)
return list(fields)
fields = detect_merge_fields('邮件合并模板.docx')
print(f"检测到合并字段: {fields}")
# 输出: 检测到合并字段: ['contract_no', 'party_a', 'party_b',
# 'project_name', 'amount', 'amount_cn', 'sign_date']
6.2 批量生成文档
批量生成文档的核心逻辑是:准备模板文档、准备数据源、逐行替换占位字段、保存为独立文档或合并为一个文档。对于大量文档的生成(如上千份证书),推荐使用"单文档多节"的方式,即在同一个文档中为每条数据创建一个独立的节(Section),这样生成一个文件更便于管理和分发。如果每条数据需要独立的文件,则每条数据生成一个独立的 docx 文件,可以使用 ZipFile 缓存模板结构,避免重复读取模板。
from docx import Document
import copy, os, csv
# 1. 准备模拟数据源
records = [
{"contract_no": "HT2025001", "party_a": "北京某某科技有限公司", "party_b": "上海佼艾信息技术有限公司",
"project_name": "企业管理系统开发项目", "amount": "500,000", "amount_cn": "伍拾万圆整", "sign_date": "2025年6月1日"},
{"contract_no": "HT2025002", "party_a": "深圳某某网络有限公司", "party_b": "上海佼艾信息技术有限公司",
"project_name": "数据分析平台建设项目", "amount": "320,000", "amount_cn": "叁拾贰万圆整", "sign_date": "2025年7月15日"},
]
# 2. 批量替换生成文档
def generate_from_template(template_path, records, output_dir='./output'):
"""根据模板和数据批量生成文档"""
os.makedirs(output_dir, exist_ok=True)
generated_files = []
for record in records:
# 重新加载模板
doc = Document(template_path)
# 遍历所有段落进行替换
for paragraph in doc.paragraphs:
for key, value in record.items():
placeholder = f'{{{{{key}}}}}'
if placeholder in paragraph.text:
# 完整替换段落文本
for run in paragraph.runs:
if placeholder in run.text:
run.text = run.text.replace(placeholder, value)
# 同样处理表格中的占位符
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for key, value in record.items():
if f'{{{{{key}}}}}' in cell.text:
cell.text = cell.text.replace(f'{{{{{key}}}}}', value)
# 保存生成的文档
output_path = os.path.join(output_dir, f"合同_{record['contract_no']}.docx")
doc.save(output_path)
generated_files.append(output_path)
print(f"已生成: {output_path}")
return generated_files
# 执行批量生成
generated = generate_from_template('邮件合并模板.docx', records)
print(f"共生成 {len(generated)} 份合同")
6.3 从外部数据源读取
实际应用中的邮件合并数据源通常是 Excel 文件、CSV 文件或数据库。使用 pandas 可以轻松读取这些数据源并转换为字典列表格式。对于大数据量的场景(万级以上),推荐使用迭代器逐批读取,避免一次性将所有数据加载到内存。从 CSV 数据源读取时需特别注意编码问题(UTF-8 带 BOM vs UTF-8),建议统一使用 UTF-8 编码保存数据源文件。
from docx import Document
import csv
import os
# 1. 从CSV文件读取数据源
def read_csv_data(csv_path):
"""从CSV文件读取数据源"""
records = []
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
records.append(row)
return records
# 2. 使用pandas读取Excel数据源(更强大)
try:
import pandas as pd
def read_excel_data(excel_path, sheet_name=0):
"""从Excel文件读取数据源"""
df = pd.read_excel(excel_path, sheet_name=sheet_name, dtype=str)
# 填充NaN为空字符串
df = df.fillna('')
return df.to_dict(orient='records')
# 使用方法:
# records = read_excel_data('数据源.xlsx', sheet_name='Sheet1')
# 然后传递给generate_from_template函数
except ImportError:
print("pandas未安装,请执行: pip install pandas openpyxl")
# 3. 高级:支持嵌套占位和条件内容
# 通过自定义函数实现条件插入(if/else逻辑)
def smart_replace(doc, data):
"""高级替换:支持条件占位符"""
for paragraph in doc.paragraphs:
full_text = paragraph.text
# {{if:amount > 0}}显示金额{{endif}}
# 简单的自定义条件语法
if '{{if:' in full_text:
# 提取条件并评估(简化示例,实际应用需更安全)
# 这里只做基本的字段存在性检查
pass
# 普通占位替换
for key, value in data.items():
if f'{{{{{key}}}}}' in paragraph.text:
for run in paragraph.runs:
run.text = run.text.replace(f'{{{{{key}}}}}', str(value))
return doc
print("邮件合并准备就绪,支持CSV和Excel数据源")
七、批注与修订
批注和修订是协作文档处理中的关键功能。python-docx 允许我们通过 XML 操作添加和读取批注,管理文档的修订历史。这些功能在文档审阅流程自动化中非常有用。
7.1 添加与读取批注
批注(Comment)在 Word 中是通过 w:comments 元素存储的,每个批注包含作者、日期、文本内容和引用的文档位置。添加批注需要:1)在文档的 comments.xml 部分创建 comment 元素;2)在文档正文中插入 commentReference 标记。读取批注则需要解析 comments.xml 中的所有 comment 元素。批注可以关联到特定的文本范围(通过 commentRangeStart 和 commentRangeEnd 标记),也可以关联到光标位置。
from docx import Document
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml, OxmlElement
from lxml import etree
import datetime
doc = Document()
p = doc.add_paragraph("这是一段需要审阅的文档内容。请仔细检查每一句话,确认无误后批准。")
# 1. 添加批注
comment_id = 0
def add_comment(paragraph, text, author="审阅者", initials="SY"):
"""为指定段落添加批注"""
global comment_id
comment_id += 1
now = datetime.datetime.now()
date_str = now.strftime("%Y-%m-%dT%H:%M:%SZ")
# 获取文档的comments部件
# 注意:实际使用中需要先确保comments部件存在
document = paragraph._parent
body = document._body
# 创建批注引用标记(开始)
comment_ref = parse_xml(
f'<w:commentRangeStart {nsdecls("w")} w:id="{comment_id}"/>'
)
paragraph._p.insert(0, comment_ref)
# 创建批注引用标记(结束)
comment_end = parse_xml(
f'<w:commentRangeEnd {nsdecls("w")} w:id="{comment_id}"/>'
)
paragraph._p.append(comment_end)
# 创建引用标记(在段落末尾显示批注标记)
ref_mark = parse_xml(
f'<w:r {nsdecls("w")}>'
f'<w:commentReference w:id="{comment_id}"/>'
f'</w:r>'
)
paragraph._p.append(ref_mark)
# 返回评论ID,评论内容需要写入comments.xml
return {
'id': comment_id,
'author': author,
'initials': initials,
'date': date_str,
'text': text
}
# 添加批注
comment_data = add_comment(p, "请核实这段文字是否准确,特别是数据部分。", author="项目经理", initials="PM")
# 2. 读取批注
def read_comments(doc):
"""读取文档中的所有批注"""
comments = []
# 尝试从文档部件获取comments
try:
comments_part = doc.part.package.part_related_by(
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments'
)
if comments_part is not None:
comments_xml = comments_part._element
for comment_elem in comments_xml.findall(qn('w:comment')):
comment = {
'id': comment_elem.get(qn('w:id')),
'author': comment_elem.get(qn('w:author')),
'initials': comment_elem.get(qn('w:initials')),
'date': comment_elem.get(qn('w:date')),
}
# 提取批注文本
texts = []
for t_elem in comment_elem.iter(qn('w:t')):
if t_elem.text:
texts.append(t_elem.text)
comment['text'] = ''.join(texts)
comments.append(comment)
except Exception as e:
print(f"读取批注时出错: {e}")
return comments
doc.save('批注演示文档.docx')
7.2 修订标记与文档比较
修订标记(Revision Markers)跟踪文档的每一次修改,包括插入、删除、格式修改等。每个修订由 w:ins(插入)、w:del(删除)或 w:rPrChange(格式修改)等元素表示。启用修订跟踪后,所有修改都会被自动标记。程序化文档比较可以通过逐段落比对两个版本的文本来实现,标记出差异内容。接受或拒绝修订则需要操作 pPr 中的 rPr 元素或直接移除修订标记。python-docx 不直接提供"比较文档"的 API,但我们可以通过差异算法(如 difflib)实现基本的文档比较功能。
from docx import Document
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml, OxmlElement
import difflib, datetime
doc = Document()
# 1. 标记插入修订
p = doc.add_paragraph()
run = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
rPr.append(parse_xml(f'<w:ins {nsdecls("w")} w:id="1" '
f'w:author="修订者" w:date="{datetime.datetime.now().isoformat()}Z"/>'))
run.append(rPr)
t = OxmlElement('w:t')
t.text = "这是通过修订标记插入的新文本"
run.append(t)
p._p.append(run)
# 2. 简单的文档比较功能
def compare_documents(doc1_path, doc2_path):
"""比较两个文档的内容差异(基于段落文本)"""
doc1 = Document(doc1_path)
doc2 = Document(doc2_path)
texts1 = [p.text for p in doc1.paragraphs]
texts2 = [p.text for p in doc2.paragraphs]
differ = difflib.HtmlDiff()
diff_html = differ.make_file(texts1, texts2,
fromdesc='原始版本',
todesc='修订版本')
# 返回差异详情(简洁模式)
diff = list(difflib.unified_diff(texts1, texts2,
fromfile='原始',
tofile='修订',
lineterm=''))
return diff
# 3. 模拟接受修订(移除ins/del标记)
def accept_all_revisions(doc):
"""接受文档中的所有修订"""
# 处理插入修订
for ins_elem in doc.element.body.iter(qn('w:ins')):
parent = ins_elem.getparent()
if parent is not None:
# 将ins内的run元素提升到父级
idx = list(parent).index(ins_elem)
for child in list(ins_elem):
parent.insert(idx, child)
idx += 1
parent.remove(ins_elem)
# 处理删除修订(保留标记但移除删除内容的显示)
for del_elem in doc.element.body.iter(qn('w:del')):
parent = del_elem.getparent()
if parent is not None:
parent.remove(del_elem)
return doc
# 保存模拟修订后的文档
doc.save('修订标记演示.docx')
print("批注与修订功能演示文档已生成。")
八、页眉页脚进阶
页眉页脚在长文档中扮演着重要的导航和装饰作用。python-docx 提供了对页眉页脚的基本支持,但很多高级功能(奇偶页不同、首页不同、章节独立等)需要深入理解 Word 的分节机制。每个节(Section)都可以有自己独立的页眉页脚设置。
8.1 奇偶页不同与首页不同
在书籍和正式报告中,奇数页和偶数页的页眉页脚通常不同(如奇数页显示章节名,偶数页显示书名)。首页通常不需要显示页眉或使用特殊页眉。python-docx 通过 section.different_first_page_header_footer 和 section.odd_even_page_header_footer 属性来控制这些行为。当启用后,每个节会有三个页眉/页脚组:default(默认,用于奇偶页相同时)、even(偶数页)、first(首页)。
from docx import Document
from docx.enum.section import WD_HEADER_FOOTER
from docx.shared import Pt, Inches, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
doc = Document()
# 1. 设置第一个节的页眉页脚
section = doc.sections[0]
# 启用奇偶页不同和首页不同
section.different_first_page_header_footer = True
section.odd_even_page_header_footer = True
# 首页页眉(通常为空或显示"目录")
first_header = section.first_page_header
first_header.is_linked_to_previous = False
first_para = first_header.paragraphs[0]
first_para.text = "" # 首页不显示页眉
# 首页页脚(显示页码,居中)
first_footer = section.first_page_footer
first_footer.is_linked_to_previous = False
fp = first_footer.paragraphs[0]
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = fp.add_run()
# 插入页码域
fldChar1 = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>')
instrText = parse_xml(f'<w:instrText {nsdecls("w")} xml:space="preserve"> PAGE </w:instrText>')
fldChar2 = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>')
fldChar3 = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>')
run._r.append(fldChar1)
run._r.append(instrText)
run._r.append(fldChar2)
run._r.append(fldChar3)
# 奇数页页眉(显示章节名)
odd_header = section.header
odd_header.is_linked_to_previous = False
p_odd = odd_header.paragraphs[0]
p_odd.alignment = WD_ALIGN_PARAGRAPH.RIGHT
run_odd = p_odd.add_run("python-docx进阶教程")
run_odd.font.size = Pt(9)
run_odd.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
# 添加右对齐制表位(章节名靠右)
# 偶数页页眉(显示文档标题)
even_header = section.even_page_header
even_header.is_linked_to_previous = False
p_even = even_header.paragraphs[0]
p_even.alignment = WD_ALIGN_PARAGRAPH.LEFT
run_even = p_even.add_run("自动化办公学习笔记")
run_even.font.size = Pt(9)
run_even.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
# 添加分隔线(通过XML给页眉段落添加下边框)
from docx.oxml.ns import nsdecls
pBdr = parse_xml(f'<w:pBdr {nsdecls("w")}><w:bottom w:val="single" w:sz="4" w:space="1" w:color="CCCCCC"/></w:pBdr>')
p_odd._p.get_or_add_pPr().append(pBdr)
p_even._p.get_or_add_pPr().append(copy.deepcopy(pBdr))
doc.add_heading('第一章 概述', level=1)
doc.add_paragraph("这是第一章的内容。通过上面的设置,奇数页眉会显示\"python-docx进阶教程\",偶数页眉会显示\"自动化办公学习笔记\",首页则不显示页眉。")
doc.save('页眉页脚进阶.docx')
8.2 分节与独立页眉
大型文档通常需要分成多个章节,每个章节可能有独立的页眉页脚。通过添加分节符(section break),可以为每个章节设置不同的页眉页脚。关键属性是 header.is_linked_to_previous,设置为 False 后,当前节的页眉页脚将独立于前一节,可以自由定制。此外,还可以在每个节中设置不同的页码格式(数字格式、起始页码等)。
from docx import Document
from docx.enum.section import WD_ORIENT, WD_HEADER_FOOTER
from docx.shared import Pt, Inches, RGBColor
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml
from docx import Document
from copy import deepcopy
doc = Document()
# 第一章内容(默认节)
section1 = doc.sections[0]
section1.different_first_page_header_footer = False
header1 = section1.header
header1.is_linked_to_previous = False
h1_p = header1.paragraphs[0]
h1_p.add_run("第一章:基础介绍").font.size = Pt(9)
doc.add_heading('第一章 基础介绍', level=1)
doc.add_paragraph("本章介绍python-docx的基础知识。" * 5)
doc.add_page_break()
# 第二章:添加分节符
new_section = doc.add_section()
new_section.different_first_page_header_footer = False
# 设置第二章的独立页眉(不链接到前一节)
header2 = new_section.header
header2.is_linked_to_previous = False
h2_p = header2.paragraphs[0]
h2_p.add_run("第二章:高级表格操作").font.size = Pt(9)
# 设置第二章的页码从1开始
sectPr = new_section._sectPr
pgNumType = parse_xml(f'<w:pgNumType {nsdecls("w")} w:start="1"/>')
sectPr.append(pgNumType)
doc.add_heading('第二章 高级表格操作', level=1)
doc.add_paragraph("本章深入讲解表格的高级功能。" * 5)
doc.add_page_break()
# 附录:横向页面
appendix_section = doc.add_section()
appendix_section.orientation = WD_ORIENT.LANDSCAPE
# 需要同时交换宽度和高度
appendix_section.page_width = Inches(11.69)
appendix_section.page_height = Inches(8.27)
header3 = appendix_section.header
header3.is_linked_to_previous = False
h3_p = header3.paragraphs[0]
h3_p.add_run("附录:数据图表").font.size = Pt(9)
doc.add_heading('附录 数据图表', level=1)
doc.add_paragraph("附录内容使用横向页面,适合展示宽表格和图表。" * 3)
doc.save('分节与独立页眉.docx')
8.3 页码格式定制
页码格式包括数字样式(阿拉伯数字、罗马数字、字母等)、页码位置(左、中、右、内外侧)和页码前后缀。python-docx 通过操作 sectPr 中的 pgNumType 元素来设置页码格式。页码域使用 PAGE 字段,总页数使用 NUMPAGES 字段。中文文档常用"第 X 页 / 共 Y 页"的格式,也可以通过域代码实现。
from docx import Document
from docx.enum.section import WD_HEADER_FOOTER
from docx.shared import Pt, Inches, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml
doc = Document()
section = doc.sections[0]
# 1. 设置页码格式:大写罗马数字
sectPr = section._sectPr
pgNumType = parse_xml(f'<w:pgNumType {nsdecls("w")} w:fmt="upperRoman"/>')
# 移除已有的pgNumType
old = sectPr.find(qn('w:pgNumType'))
if old is not None:
sectPr.remove(old)
sectPr.append(pgNumType)
# 2. 页脚中插入"第X页/共Y页"格式
footer = section.footer
footer.is_linked_to_previous = False
p = footer.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 创建"第 "部分
run1 = p.add_run("第 ")
run1.font.size = Pt(9)
# 插入 PAGE 域代码
def insert_field(paragraph, field_code):
"""在段落中插入Word域代码"""
run = paragraph.add_run()
fldChar_begin = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>')
instrText = parse_xml(f'<w:instrText {nsdecls("w")} xml:space="preserve">{field_code}</w:instrText>')
fldChar_separate = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>')
fldChar_end = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>')
for elem in [fldChar_begin, instrText, fldChar_separate, fldChar_end]:
run._r.append(elem)
return run
insert_field(p, 'PAGE')
run2 = p.add_run(" 页 / 共 ")
run2.font.size = Pt(9)
insert_field(p, 'NUMPAGES')
run3 = p.add_run(" 页")
run3.font.size = Pt(9)
# 添加一些正文内容以便看到多页效果
for i in range(20):
doc.add_paragraph(f"这是第{i+1}段正文内容,用于测试页码显示效果。")
doc.save('页码格式定制.docx')
九、实战案例
理论知识需要通过实践来巩固。本节将介绍三个典型的实战案例,涵盖合同模板、数据报告和批量证书生成等常见场景,展示 python-docx 高级功能在实际项目中的组合应用。
9.1 复杂合同模板
合同模板是办公自动化的经典场景,通常包含:表格(条款明细)、内容控件(填写区域)、页眉页脚(合同编号)、图片(公司印章/LOGO)以及条件内容(不同的条款根据条件显示)。通过将模板设计与数据填充分离,可以实现一套模板生成所有类型的合同。关键技巧包括:使用表格控制布局而非空格/Tab、使用内容控件标记填写区域、使用图片占位技术处理印章和签名。
from docx import Document
from docx.shared import Pt, Inches, RGBColor, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
import datetime, re
# 创建合同模板
doc = Document()
# 设置默认字体
style = doc.styles['Normal']
style.font.name = 'SimSun'
style.font.size = Pt(10.5)
style.paragraph_format.line_spacing = 1.5
# 合同标题
title = doc.add_paragraph()
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = title.add_run("技术开发合同")
run.bold = True
run.font.size = Pt(22)
run.font.color.rgb = RGBColor(0x2E, 0x7D, 0x32)
# 合同编号和日期
info_table = doc.add_table(rows=1, cols=2)
info_table.alignment = WD_TABLE_ALIGNMENT.CENTER
info_table.style = 'No Spacing'
# 隐藏表格边框
for cell in info_table.rows[0].cells:
cell.paragraphs[0].add_run("合同编号:HT-{{contract_no}}")
# 另一方
info_table.cell(0, 1).paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.RIGHT
info_table.cell(0, 1).paragraphs[0].add_run("签订日期:{{sign_date}}")
# 合同主体表格
contract_table = doc.add_table(rows=8, cols=2)
contract_table.style = 'Table Grid'
contract_table.alignment = WD_TABLE_ALIGNMENT.CENTER
# 定义表格行数据
rows_data = [
("甲方(委托方)", "{{party_a}}"),
("统一社会信用代码", "{{credit_code_a}}"),
("法定代表人", "{{legal_rep_a}}"),
("乙方(受托方)", "{{party_b}}"),
("统一社会信用代码", "{{credit_code_b}}"),
("法定代表人", "{{legal_rep_b}}"),
("项目名称", "{{project_name}}"),
("合同金额", "¥{{amount}}元"),
]
for i, (label, value) in enumerate(rows_data):
cell_label = contract_table.cell(i, 0)
cell_value = contract_table.cell(i, 1)
cell_label.width = Inches(1.5)
cell_value.width = Inches(4.5)
cell_label.paragraphs[0].add_run(label).bold = True
cell_value.paragraphs[0].add_run(value)
# 条款文本
clauses = [
"一、项目内容:{{project_content}}",
"二、交付期限:{{delivery_date}}",
"三、付款方式:{{payment_terms}}",
"四、保密条款:双方应对本合同内容及项目过程中知悉的对方商业秘密严格保密。",
"五、违约责任:{{penalty_clause}}",
"六、争议解决:凡因本合同引起的争议,双方应友好协商解决;协商不成的,提交签约地人民法院诉讼解决。",
]
for clause in clauses:
doc.add_paragraph(clause)
# 签章区域
doc.add_paragraph()
sign_table = doc.add_table(rows=1, cols=2)
sign_table.alignment = WD_TABLE_ALIGNMENT.CENTER
sign_data = [
("甲方(盖章):\n\n授权代表:{{signatory_a}}\n日期:",
"乙方(盖章):\n\n授权代表:{{signatory_b}}\n日期:")
]
sign_table.cell(0, 0).text = sign_data[0][0]
sign_table.cell(0, 1).text = sign_data[0][1]
doc.save('合同模板.docx')
print("合同模板已创建,包含表格、占位符和签章区域。")
9.2 带图表的数据报告
数据报告通常包含:数据表格、统计图表(图片格式)、条件格式(如超过阈值标红)、以及专业的排版。统计图表可以通过 matplotlib 生成图片后插入文档。条件格式通过遍历单元格,根据数值大小设置不同的背景色或字体颜色。一个完整的报告自动生成系统可以将数据采集、分析、可视化和报告生成整个流程串联起来。
from docx import Document
from docx.shared import Pt, Inches, RGBColor
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH
import os, matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
doc = Document()
doc.add_heading('2025年Q2销售数据报告', level=0)
# 1. 生成图表(使用matplotlib)
months = ['4月', '5月', '6月']
sales = [120000, 150000, 180000]
targets = [130000, 140000, 160000]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
# 柱状图
colors = ['#2ecc71' if s >= t else '#e74c3c' for s, t in zip(sales, targets)]
bars = ax1.bar(months, sales, color=colors, width=0.6)
ax1.plot(months, targets, 'r--', marker='o', label='目标')
ax1.set_title('月度销售额对比')
ax1.set_ylabel('金额(元)')
ax1.legend()
# 饼图
ax2.pie(sales, labels=months, autopct='%1.1f%%',
colors=['#3498db', '#2ecc71', '#f39c12'],
startangle=90)
ax2.set_title('各月销售额占比')
plt.tight_layout()
chart_path = 'chart_temp.png'
plt.savefig(chart_path, dpi=150)
plt.close()
# 2. 插入图表到文档
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.add_run().add_picture(chart_path, width=Inches(6))
# 3. 创建数据明细表(含条件格式)
doc.add_heading('销售明细表', level=2)
table = doc.add_table(rows=5, cols=5)
table.style = 'Table Grid'
table.alignment = WD_TABLE_ALIGNMENT.CENTER
headers = ["月份", "销售额", "目标", "完成率", "状态"]
for i, h in enumerate(headers):
cell = table.cell(0, i)
cell.paragraphs[0].add_run(h).bold = True
data = [
("4月", 120000, 130000, 92.3, "未达标"),
("5月", 150000, 140000, 107.1, "达标"),
("6月", 180000, 160000, 112.5, "达标"),
("合计", 450000, 430000, 104.7, "整体达标"),
]
for r, row_data in enumerate(data, start=1):
for c, val in enumerate(row_data):
cell = table.cell(r, c)
cell.paragraphs[0].add_run(str(val))
# 条件格式:未达标标红,达标标绿
if c == 4: # 状态列
for paragraph in cell.paragraphs:
for run in paragraph.runs:
if "未达标" in run.text:
run.font.color.rgb = RGBColor(0xE7, 0x4C, 0x3C)
elif "达标" in run.text:
run.font.color.rgb = RGBColor(0x27, 0xAE, 0x60)
doc.save('数据报告模板.docx')
# 清理临时图片文件
if os.path.exists(chart_path):
os.remove(chart_path)
print("数据报告已生成,包含matplotlib图表和条件格式表格。")
9.3 批量生成奖状/证书
批量生成证书/奖状是最能体现 python-docx 邮件合并价值的场景之一。核心要素包括:精美的证书模板(含背景图片、边框装饰)、合并字段(姓名、奖项、日期)、批量替换生成、以及输出管理。对于高并发生成需求,可以使用多线程或进程池加速。证书模板通常使用表格来精确定位各元素(标题居中、姓名大号字体、奖项说明、日期和签章),然后将表格边框隐藏,只显示内容区域。
from docx import Document
from docx.shared import Pt, Inches, RGBColor, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml
import os, csv
# 1. 创建证书模板
doc = Document()
# 设置页面为横向
section = doc.sections[0]
section.page_width = Inches(11.69)
section.page_height = Inches(8.27)
section.top_margin = Cm(1.5)
section.bottom_margin = Cm(1.5)
section.left_margin = Cm(2)
section.right_margin = Cm(2)
# 证书装饰边框(通过表格实现)
border_table = doc.add_table(rows=1, cols=1)
border_table.alignment = WD_TABLE_ALIGNMENT.CENTER
cell = border_table.cell(0, 0)
cell.width = Inches(10)
# 设置表格边框为双线金色边框
tbl = border_table._tbl
tblPr = tbl.tblPr
borders = parse_xml(
f'<w:tblBorders {nsdecls("w")}>'
f'<w:top w:val="double" w:sz="24" w:space="0" w:color="D4AF37"/>'
f'<w:left w:val="double" w:sz="24" w:space="0" w:color="D4AF37"/>'
f'<w:bottom w:val="double" w:sz="24" w:space="0" w:color="D4AF37"/>'
f'<w:right w:val="double" w:sz="24" w:space="0" w:color="D4AF37"/>'
f'</w:tblBorders>'
)
tblPr.append(borders)
# 证书内容
cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
# 标题
run_title = cell.paragraphs[0].add_run('\n\n荣 誉 证 书\n')
run_title.bold = True
run_title.font.size = Pt(36)
run_title.font.color.rgb = RGBColor(0xD4, 0xAF, 0x37)
run_title.font.name = 'STXingkai' # 华文行楷
# 内容段落
p_body = cell.add_paragraph()
p_body.alignment = WD_ALIGN_PARAGRAPH.CENTER
run_line1 = p_body.add_run('\n 兹授予\n\n')
run_line1.font.size = Pt(16)
run_name = p_body.add_run(' {{recipient_name}} \n\n')
run_name.bold = True
run_name.font.size = Pt(32)
run_name.font.color.rgb = RGBColor(0x8B, 0x00, 0x00) # 暗红色
run_line2 = p_body.add_run('在 "{{project_name}}" 项目中表现优异,'
f'荣获\n\n')
run_line2.font.size = Pt(16)
run_award = p_body.add_run(' {{award_title}} \n\n')
run_award.bold = True
run_award.font.size = Pt(28)
run_award.font.color.rgb = RGBColor(0xD4, 0xAF, 0x37)
run_line3 = p_body.add_run('特发此证,以资鼓励!\n\n\n\n')
run_line3.font.size = Pt(16)
# 颁发单位和日期
p_sign = cell.add_paragraph()
p_sign.alignment = WD_ALIGN_PARAGRAPH.RIGHT
run_sign = p_sign.add_run('颁发单位:{{issuer}}\n颁发日期:{{issue_date}}')
run_sign.font.size = Pt(14)
doc.save('证书模板.docx')
# 2. 批量生成证书
def batch_generate_certificates(template_path, recipients, output_dir='./certificates'):
"""批量生成证书"""
os.makedirs(output_dir, exist_ok=True)
for recipient in recipients:
doc = Document(template_path)
# 替换占位符
for paragraph in doc.paragraphs:
for key, value in recipient.items():
placeholder = f'{{{{{key}}}}}'
if placeholder in paragraph.text:
for run in paragraph.runs:
run.text = run.text.replace(placeholder, str(value))
# 也替换表格中的占位符
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for key, value in recipient.items():
if f'{{{{{key}}}}}' in cell.text:
cell.text = cell.text.replace(f'{{{{{key}}}}}', str(value))
output_path = os.path.join(output_dir,
f"证书_{recipient['recipient_name']}.docx")
doc.save(output_path)
print(f"已生成: {output_path}")
# 示例数据
recipients = [
{"recipient_name": "张三", "project_name": "企业管理系统",
"award_title": "优秀项目经理奖", "issuer": "上海佼艾科技有限公司",
"issue_date": "2025年12月1日"},
{"recipient_name": "李四", "project_name": "数据分析平台",
"award_title": "最佳技术创新奖", "issuer": "上海佼艾科技有限公司",
"issue_date": "2025年12月1日"},
]
# batch_generate_certificates('证书模板.docx', recipients)
print("证书模板已创建,并准备了批量生成函数。"
"执行 batch_generate_certificates() 即可批量生成证书。")
最佳实践总结:在实际项目中使用 python-docx 时,建议遵循以下原则:1)模板与代码分离,将文档设计放在模板中,代码只负责数据填充;2)善用底层 XML 操作突破 API 限制;3)使用内容控件和自定义 XML 数据绑定实现复杂的模板交互;4)对于大型文档,合理使用分节和样式系统保持一致性;5)批量操作时注意性能,采用缓存和并行处理策略。