前言

  • 工具:python-docx == 0.8.11
  • 环境:Linux/windows
  • 需求:使用python自动生成word文档时,生成目录。
  • 先放结论:如果项目需求必须要基于linux环境,不能基于win32com等依赖于windows系统的库,目前没有找到完美的方案直接自动生成带标题页码的目录,只能通过一些折中或者间接的方式,尽可能简单实现,且“像”一个完整的目录。

背景-使用python-docx生成报告思路简述

使用python-docx生成word报告一般可以有两种思路:

  1. 直接使用python-docx逐段生成内容,如:
from docx import Documentdoc = 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')
  1. 基于docx文件,事先准备.docx模板, 可采用特定的占位标记,遍历文档的paragraphs对象,向文件中填充内容。该方法适用于word内容大纲相对固定的报告生成,优点是方便设置文档的排版及内容格式等,因此在目录生成上可以直接在模板文档中插入目录,需要解决的问题是页码更新。
    *.docx模板文档示例如下:

    生成内容代码如下:
from docx import Documentdoc = Document('template.docx')  # 参数为.docx模板文件路径def write_to_paragraph(paragraph, text):# 该方法替换的文字内容可保持原段落格式paragraph.runs[0].text = textfor 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. 只包含各级标题,无页码;
  2. 包含各级标题且可点击链接至标题所在位置,无页码;
  3. 包含各级标题和页码,但需手动或半自动更新目录域。

不包含页码

1.遍历Document对象的paragraph列表,通过paragraph对象的style.name属性判断标题级别,并获取标题文字,生成目录。

from docx import Documentdoc = 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 Documentdef add_title_with_bookmark(doc, text, style, bookmark_id):paragraph = doc.add_paragraph(text, style=style)run = paragraph.add_run()tag = run._rstart = 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.textlevel = 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判断获取标题及其标签。

包含页码

一些网上查阅到的方案:
1. 对于word文档中已添加目录(如使用基于模板生成的方法,事先插入目录),通过更改setting.xml设置,在末尾加上 <w:updateFields w:val="true"/>,打开word文档时弹出对话框询问是否更新域,需手动点击“是”,完成更新。

# 方法一:引用网上查到的方法,使用lxml库
import lxml
from docx import Documentdoc = 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 qnupdate = OxmlElement('w:updateFields')
update.set(qn('w:val'), 'true')
doc.settings.element.append(update)doc.save('result.docx')

个人测试效果:若word中包含其他域,打开word后会弹出提示框询问是否更新域,点击“是”后,继续询问更新目录“只更新页码”或”更新整个目录“。目录确实可以更新,但是保存后下次打开文档依然会询问是否更新,另存为也会提示,体验并不友好。需要在文档的【文件】-【选项】-【高级】选项卡的常规项中,取消勾选”打开时更新自动链接“。(可能不同word版本或wps会有差异)
总结下来就是这个方法稍显鸡肋,正常打开文档后再点击更新目录的操作跟该方法复杂度差别不大。


2. 从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 OxmlElementparagraph = 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 needfldChar2 = 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

效果如下图:

该方法不能直接生成目录列表,需要右击弹出菜单,选择【更新域】后,可生成目录。或可与方法一结合,只需打开文档是选择更新域或更新目录,打开后即为完整目录。

综合实践案例

目标:使用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 OxmlElementdoc = Document()# 添加标题和书签
def add_title_with_bookmark(doc, text, style, bookmark_id):paragraph = doc.add_paragraph(text, style=style)run = paragraph.add_run()tag = run._rstart = 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.textlevel = 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 = texthr1.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')

效果

python-docx生成目录方法探索及汇总整理相关推荐

  1. python docx table 生成_从Python docx生成docx文件,pythondocx

    python-docx是python编程语言的一个库,可以对docx文档进行读,同时也可以进行生成docx文档,这篇文档主要是讲生成docx文档. 1.生成一个空docx文档 # coding:utf ...

  2. python学习 生成目录树

    第一种最速方式,cmd直接用tree命令,如果还要文件用 tree /f C:\Users\ds>tree d:\9月考勤 > d:\test.xlsC:\Users\ds>tree ...

  3. python图片生成gif方法汇总

    一.使用 PIL 和 images2gif 库 第一种方法主要是采用PIL和imagegif库,由于版本的原因,可能有的imagegif对图片进行处理的时候,以下语句会报错 writeGif(outf ...

  4. Python自动化生成代码以及验证代码汇总

    python 软件自动化 0 绪 1. 自动打开.编译.下载: 1.1 自动打开工程,打开软件 1.2. 自动点击编译,下载按钮, 2. 自动写代码 2.1 对工程进行更改 2.2 对C文件更改 3 ...

  5. Python OS 文件/目录方法

    目录 一.python os.path模块 二.os.listdir 与os.walk获得文件的路径 情况1:在一个目录下面只有文件,没有文件夹,这个时候可以使用os.listdir 情况2:递归的情 ...

  6. CSDN博客生成目录

    CSDN博客生成目录方法如下: 在博文的首部写@[TOC]目录名,自己定)(注意是英文括号) TOC可以是小写toc,是table of contents的缩写 我的目录 简介 简介A 简介B 发展近 ...

  7. wps怎么添加附录_如何将附录生成目录 - 卡饭网

    word如何自动生成目录并动态更新 word如何自动生成目录并动态更新 在编辑文档的时候,目录有时是必不可少的一项,但在实际情况当中,很多使用者不知道怎样使用word中的目录自动生成的功能,而是自己在 ...

  8. Java 使用iText7生成带页码的PDF文件(同时生成目录,但是不会合并两个PDF)

    一.效果图 1.带页码效果 2.目录效果 前言:Java 使用iText7生成带页码的PDF文件,同时生成目录PDF,但限于水平,暂时还在摸索合并两个PDF.不过看了一下,iText好像有生成目录的代 ...

  9. markdown 自动生成目录索引

    使用@[TOC]命令可以自动生成目录 方法: 将要放入目录中的行用"标题"符号注明 然后在文章中单独用一行写@[TOC]即可 举例: 在编辑目录中写: 文章目录 我是谁 我是帅哥 ...

最新文章

  1. 配置MUX VLAN示例(汇聚层设备)
  2. 终于要揭开神秘面纱?Magic Leap将要展示产品
  3. jQuery操作Dom、jQuery事件机制、jQuery补充部分
  4. [Android]反编译apk + eclipse中调试smali
  5. 国内数据中心分布及供电系统概述
  6. 机器学习基础-特征缩放交叉验证法-05
  7. 2048——Java控制台版本
  8. 【转】四阶魔方还原1
  9. Java获取函数参数名称
  10. Jquery 屏蔽右键菜单,识别右键事件
  11. python datetime格式_python time和datetime常用写法格式
  12. 从小米摄像头事件,到物联网安全的“三重门”
  13. 解决安装office2007的各种工具时提示“安装程序找不到office.zh-cn/*”的问题
  14. Inverse design of 3d molecular structures with conditional generative neural networks(2022论文阅读)
  15. 北京大学肖臻老师《区块链技术与应用》公开课笔记8——BTC挖矿篇
  16. [blog] Speech Recognition Is Not Solved 语音识别领域尚待解决的子问题
  17. FL Studio教程之在线面板详解
  18. Android学习笔记——APP页面转换与Intent学习
  19. e3服务器性能,揭Xeon E3-1200 V3变化:性能测试见分晓
  20. 视频处理-按帧或者按秒截取图像

热门文章

  1. nginx多个server的配置,同一端口
  2. 区块链十年丨公链的成长与演变——BB财经
  3. vue 获取当前div离边框距离_vue获取dom元素的宽高
  4. Nexus Repository Manager OSS 3.x 安装配置
  5. 中职计算机专业课教师难考吗,中专教师入职一个多月,谈谈自己的感受
  6. pytorch读取VOC数据集
  7. Teamleader眼中的CRM格局
  8. Java基础:Java中四种访问修饰符
  9. 计算机国一常用函数,excel常用函数及国二课程分享
  10. snapshot画法小结