美文网首页
python-docx生成目录方法探索及汇总整理

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

作者: jackmanzhang | 来源:发表于2024-01-16 12:47 被阅读0次

    前言
    工具: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

    相关文章

      网友评论

          本文标题:python-docx生成目录方法探索及汇总整理

          本文链接:https://www.haomeiwen.com/subject/acykodtx.html