前言
工具:python-docx == 0.8.11
环境:Linux/windows
需求:使用python自动生成word文档时,生成目录。
先放结论:如果项目需求必须要基于linux环境,不能基于win32com等依赖于windows系统的库,目前没有找到完美的方案直接自动生成带标题页码的目录,只能通过一些折中或者间接的方式,尽可能简单实现,且“像”一个完整的目录。
背景-使用python-docx生成报告思路简述
使用python-docx生成word报告一般可以有两种思路:
直接使用python-docx逐段生成内容,如:
from docx import Document
doc = Document()
doc.add_paragraph('文档标题')
doc.add_paragraph('第一部分',style='Heading 1')
doc.add_paragraph('1.二级标题',style='Heading 2', )
# 任意生成些段落
for i in range(15):
doc.add_paragraph(str(i))
doc.add_paragraph('第二部分', style='Heading 1')
doc.add_paragraph('1.二级标题', style='Heading 2')
for i in range(15):
doc.add_paragraph(str(i))
doc.add_paragraph('2.二级标题', style='Heading 2')
for i in range(15):
doc.add_paragraph(str(i))
doc.add_paragraph( '3.二级标题', style='Heading 2')
doc.save('result.docx')
基于docx文件,事先准备.docx模板, 可采用特定的占位标记,遍历文档的paragraphs对象,向文件中填充内容。该方法适用于word内容大纲相对固定的报告生成,优点是方便设置文档的排版及内容格式等,因此在目录生成上可以直接在模板文档中插入目录,需要解决的问题是页码更新。
*.docx模板文档示例如下:
image.png
生成内容代码如下:
from docx import Document
doc = Document('template.docx') # 参数为.docx模板文件路径
def write_to_paragraph(paragraph, text):
# 该方法替换的文字内容可保持原段落格式
paragraph.runs[0].text = text
for i in par.runs[1:]:
i.clear()
for p in doc.paragraphs:
if p.text == '<<p1>>':
# write_to_paragraph(p, text)
p.text = 'replace p1 text'
elif p.text == '<<p2>>':
# write_to_paragraph(p, text)
p.text = 'replace p2 text'
# 其他段落略
doc.save('result.docx')
生成目录方法
使用python-docx生成目录(或者说基于修改xml的方式生成或处理docx文档的工具)的难点主要在于页码的生成和更新,目录需要获取的标题所在的页码,是通过布局引擎提供的分页功能实现的,布局引擎是Word 客户端中内置的一个非常复杂的软件,用 Python 编写页面布局引擎并不是一个好主意。
因此,简化折中的方式可以包括:
只包含各级标题,无页码;
包含各级标题且可点击链接至标题所在位置,无页码;
包含各级标题和页码,但需手动或半自动更新目录域。
不包含页码
1.遍历Document对象的paragraph列表,通过paragraph对象的style.name属性判断标题级别,并获取标题文字,生成目录。
from docx import Document
doc = Document('result.docx')
for paragraph in doc.paragraphs:
if 'Heading' in paragraph.style.name:
text = paragraph.text
# level = int(paragraph.style.name[-1])
new_p = doc.add_paragraph('text')
doc.save('result1.docx')
2.标题增加链接:标题添加bookmark书签,生成目录时添加超链接至书签位置。
方式一:使用python-docx生成标题
from docx import Document
def add_title_with_bookmark(doc, text, style, bookmark_id):
paragraph = doc.add_paragraph(text, style=style)
run = paragraph.add_run()
tag = run._r
start = OxmlElement('w:bookmarkStart')
start.set(qn('w:id'), str(bookmark_id))
start.set(qn('w:name'), bookmark_text)
tag.append(start)
tr = OxmlElement('w:r')
tr.text = ''
tag.append(tr)
end = OxmlElement('w:bookmarkEnd')
end.set(qn('w:id'), str(bookmark_id))
end.set(qn('w:name'), bookmark_text)
tag.append(end)
doc = Document()
doc_title = doc.add_paragraph('文档标题')
add_title_with_bookmark('第一部分',style='Heading 1', bookmark_id='1')
add_title_with_bookmark('1.二级标题',style='Heading 2', , bookmark_id='2')
for i in range(15):
doc.add_paragraph(str(i))
add_title_with_bookmark('第二部分', style='Heading 1', bookmark_id='3')
add_title_with_bookmark('1.二级标题', style='Heading 2', bookmark_id='4')
for i in range(15):
doc.add_paragraph(str(i))
add_title_with_bookmark('2.二级标题', style='Heading 2', bookmark_id='5')
for i in range(15):
doc.add_paragraph(str(i))
add_title_with_bookmark('3.二级标题', style='Heading 2', bookmark_id='6')
for paragraph in doc.paragraphs:
if 'Heading' in paragraph.style.name:
b = paragraph._element.findall('.//' + qn('w:bookmarkStart'))
bookmark_name = b[0].get(qn('w:name'))
text = paragraph.text
level = int(paragraph.style.name[-1])
print(text, bookmark_name)
toc_paragraph = doc.add_paragraph()
hyperlink = OxmlElement('w:hyperlink')
hyperlink.set(qn('w:anchor'), bookmark_name)
hyperlink.set(qn('w:history'), '1')
hr2 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
fldChar = OxmlElement('w:fldChar')
fldChar.set(qn('w:fldCharType'), 'begin')
hr2.append(rPr)
hr2.append(fldChar)
hyperlink.append(hr2)
hr3 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
instrText = OxmlElement('w:instrText')
instrText.set(qn('xml:space'), 'preserve')
instrText.text = ' PAGEREF {} \h '.format(bookmark_name)
hr3.append(rPr)
hr3.append(instrText)
hyperlink.append(hr3)
hr4 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
fldChar = OxmlElement('w:fldChar')
fldChar.set(qn('w:fldCharType'), 'separate')
hr4.append(rPr)
hr4.append(fldChar)
hyperlink.append(hr4)
hr5 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
hr5.text = ''
hr5.append(rPr)
hyperlink.append(hr5)
hr6 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
fldChar = OxmlElement('w:fldChar')
fldChar.set(qn('w:fldCharType'), 'end')
hr6.append(rPr)
hr6.append(fldChar)
hyperlink.append(hr6)
toc_paragraph._p.append(hyperlink)
doc.save('result.docx')
方式二:使用docx模板设置好标题及标题级别,通常标题已经包含书签中,可以参考方式一遍历段落,通过paragraph.style.name判断获取标题及其标签。
包含页码
一些网上查阅到的方案:
- 对于word文档中已添加目录(如使用基于模板生成的方法,事先插入目录),通过更改setting.xml设置,在末尾加上 <w:updateFields w:val="true"/>,打开word文档时弹出对话框询问是否更新域,需手动点击“是”,完成更新。
方法一:引用网上查到的方法,使用lxml库
import lxml
from docx import Document
doc = Document('**.docx') # 待更新目录
name_space = "http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
update_name_space = "%supdateFields" % name_space
val_name_space = "%sval" % name_space
element_update_field_obj = xml.etree.SubElement(doc.settings.element, update_name_space)element_update_field_obj.set(val_name_space,"true")
doc.save('result.docx')
方法二:使用python-docx库的方法
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
update = OxmlElement('w:updateFields')
update.set(qn('w:val'), 'true')
doc.settings.element.append(update)
doc.save('result.docx')
个人测试效果:若word中包含其他域,打开word后会弹出提示框询问是否更新域,点击“是”后,继续询问更新目录“只更新页码”或”更新整个目录“。目录确实可以更新,但是保存后下次打开文档依然会询问是否更新,另存为也会提示,体验并不友好。需要在文档的【文件】-【选项】-【高级】选项卡的常规项中,取消勾选”打开时更新自动链接“。(可能不同word版本或wps会有差异)
总结下来就是这个方法稍显鸡肋,正常打开文档后再点击更新目录的操作跟该方法复杂度差别不大。
image.png
image.png
- 从stackoverflow和github搬运的方法:使用python-docx写入TOC域代码。
stackoverflow链接: https://stackoverflow.com/questions/18595864/python-create-a-table-of-contents-with-python-docx-lxml
github链接: https://github.com/python-openxml/python-docx/issues/36
该方法前提是word中已经定义好各级标题。
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
paragraph = self.document.add_paragraph()
run = paragraph.add_run()
fldChar = OxmlElement('w:fldChar') # creates a new element
fldChar.set(qn('w:fldCharType'), 'begin') # sets attribute on element
instrText = OxmlElement('w:instrText')
instrText.set(qn('xml:space'), 'preserve') # sets attribute on element
instrText.text = 'TOC \\o "1-3" \\h \\z \\u' # change 1-3 depending on heading levels you need
fldChar2 = OxmlElement('w:fldChar')
fldChar2.set(qn('w:fldCharType'), 'separate')
fldChar3 = OxmlElement('w:t')
fldChar3.text = "右击更新目录" # 文字内容可调整
fldChar2.append(fldChar3)
fldChar4 = OxmlElement('w:fldChar')
fldChar4.set(qn('w:fldCharType'), 'end')
r_element = run._r
r_element.append(fldChar)
r_element.append(instrText)
r_element.append(fldChar2)
r_element.append(fldChar4)
p_element = paragraph._p
效果如下图:
image.png
该方法不能直接生成目录列表,需要右击弹出菜单,选择【更新域】后,可生成目录。或可与方法一结合,只需打开文档是选择更新域或更新目录,打开后即为完整目录。
综合实践案例
目标:使用python-docx生成标题和不带页码的目录,目录按层级缩进,打开文档后可手动更新整个目录。
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
from docx.shared import RGBColor
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
doc = Document()
# 添加标题和书签
def add_title_with_bookmark(doc, text, style, bookmark_id):
paragraph = doc.add_paragraph(text, style=style)
run = paragraph.add_run()
tag = run._r
start = OxmlElement('w:bookmarkStart')
start.set(qn('w:id'), str(bookmark_id))
start.set(qn('w:name'), text)
tag.append(start)
tr = OxmlElement('w:r')
tr.text = ''
tag.append(tr)
end = OxmlElement('w:bookmarkEnd')
end.set(qn('w:id'), str(bookmark_id))
end.set(qn('w:name'), text)
tag.append(end)
# 开始写入文档
doc.add_paragraph('文档标题')
doc.add_paragraph('目录')
# 标记目录的位置
catalog_p = doc.add_paragraph('')
# 写入标题和段落
add_title_with_bookmark(doc, '第一部分',style='Heading 1', bookmark_id='1')
add_title_with_bookmark(doc, '1.二级标题',style='Heading 2', bookmark_id='2')
for i in range(15):
doc.add_paragraph('<<p{}>>'.format(str(i)))
add_title_with_bookmark(doc, '第二部分', style='Heading 1', bookmark_id='3')
add_title_with_bookmark(doc, '1.二级标题', style='Heading 2', bookmark_id='4')
for i in range(15):
doc.add_paragraph('<<p{}>>'.format(str(i)))
add_title_with_bookmark(doc, '2.二级标题', style='Heading 2', bookmark_id='5')
for i in range(15):
doc.add_paragraph('<<p{}>>'.format(str(i)))
add_title_with_bookmark(doc, '3.二级标题', style='Heading 2', bookmark_id='6')
# 开始写入目录
# 目录开头增加值域,方便手动更新整个目录。自动生成的目录标题包含在值域中。
toc_paragraph = catalog_p.insert_paragraph_before() # 在标记的目录位置前添加段落
r1 = toc_paragraph.add_run()
toc_field = OxmlElement('w:fldChar')
toc_field.set(qn('w:fldCharType'), 'begin')
r1._r.append(toc_field)
r2 = toc_paragraph.add_run()
toc_field = OxmlElement('w:instrText')
toc_field.set(qn('xml:space'), 'preserve')
toc_field.text = 'TOC \\o "1-3" \\h \\z '
r2._r.append(toc_field)
r3 = toc_paragraph.add_run()
toc_field = OxmlElement('w:fldChar')
toc_field.set(qn('w:fldCharType'), 'separate')
r3._r.append(toc_field)
# 自动生成目录内容,不包含页码
for paragraph in doc.paragraphs:
if 'Heading' in paragraph.style.name:
b = paragraph._element.findall('.//' + qn('w:bookmarkStart'))
bookmark_name = b[0].get(qn('w:name'))
text = paragraph.text
level = int(paragraph.style.name[-1])
# print(text, bookmark_name)
toc_paragraph = catalog_p.insert_paragraph_before(style='Normal')
# 二级标题设置缩进
if level == 2:
toc_paragraph.paragraph_format.first_line_indent = Pt(24)
# 设置制表符,可显示页码前的"…………"
tabs = OxmlElement('w:tabs')
tab1 = OxmlElement('w:tab')
tab1.set(qn('w:val'), "left")
tab1.set(qn('w:leader'), "dot")
tab1.set(qn('w:pos'), "8400")
tabs.append(tab1)
toc_paragraph._p.pPr.append(tabs)
# toc_paragraph若未设定style,toc_paragraph._p没有pPr属性,需注释前一句代码,使用以下语句
# pPr = OxmlElement('w:pPr')
# pPr.append(tabs)
# toc_paragraph._p.append(pPr)
hyperlink = OxmlElement('w:hyperlink')
hyperlink.set(qn('w:anchor'), bookmark_name)
hyperlink.set(qn('w:history'), '1')
hr1 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
rStyle = OxmlElement('w:rStyle')
rStyle.set(qn('w:val'), "a4")
rPr.append(rStyle)
hr1.text = text
hr1.append(rPr)
hyperlink.append(hr1)
hr2 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
fldChar = OxmlElement('w:fldChar')
fldChar.set(qn('w:fldCharType'), 'begin')
hr2.append(rPr)
hr2.append(fldChar)
hyperlink.append(hr2)
hr3 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
instrText = OxmlElement('w:instrText')
instrText.set(qn('xml:space'), 'preserve')
instrText.text = ' PAGEREF {} \h '.format(bookmark_name)
hr3.append(rPr)
hr3.append(instrText)
hyperlink.append(hr3)
hr4 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
fldChar = OxmlElement('w:fldChar')
fldChar.set(qn('w:fldCharType'), 'separate')
hr4.append(rPr)
hr4.append(fldChar)
hyperlink.append(hr4)
hrt = OxmlElement('w:r')
tab = OxmlElement('w:tab')
hrt.append(tab)
hyperlink.append(hrt)
hr5 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
hr5.text = ''
hr5.append(rPr)
hyperlink.append(hr5)
hr6 = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
fldChar = OxmlElement('w:fldChar')
fldChar.set(qn('w:fldCharType'), 'end')
hr6.append(rPr)
hr6.append(fldChar)
hyperlink.append(hr6)
toc_paragraph._p.append(hyperlink)
# 目录结尾的值域
toc_paragraph = catalog_p.insert_paragraph_before()
r4 = toc_paragraph.add_run()
toc_field = OxmlElement('w:fldChar')
toc_field.set(qn('w:fldCharType'), 'end')
r4._r.append(toc_field)
# 分页
break_page_p = catalog_p.insert_paragraph_before()
break_page_p.add_run().add_break(WD_BREAK.PAGE)
doc.save(r'result.docx')
效果:
image.png原文链接:https://blog.csdn.net/weixin_42927998/article/details/130192913
网友评论