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

前言
工具: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判断获取标题及其标签。
包含页码
一些网上查阅到的方案:

  1. 对于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
  1. 从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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容