Skip to content
Python:生成具有目錄的Word文件
📆2024-02-26 | 📂Python

Word文件內容若涉及大量變數,依靠Excel、VBA搭配合併列印的組合技會很辛苦且依然費力。


關於用來生成Word文件的Python套件,就我所知,常用的有兩個:

docxtpl是基於python-docx加上jinja2的套件,用來修改文件更加方便。

This package uses 2 major packages :

  • python-docx for reading, writing and creating sub documents
  • jinja2 for managing tags inserted into the template docx

python-docx-template has been created because python-docx is powerful for creating documents but not for modifying them.

基本用法

使用方式很簡單,以下是官方文件的範例:

python
from docxtpl import DocxTemplate

# *** .docx ONLY ***
doc = DocxTemplate('my_word_template.docx')
context = { 'company_name': 'World company' }
doc.render(context)
doc.save('generated_doc.docx')

基本上就是讀取文件模板,接著將資料彙整為字典結構,交給DocxTemplate進行render就完成了。

TIP

  1. 字典結構中的keyname對應模板中的標籤語法變數{{ keyname }},變數名稱不允許含有空格
  2. 標籤語法與變數名稱之間必須以空格分隔,例如{{ keyname }},變數名稱前後都有空格

對應範例的文件模板如下:

shell
# docx file
my company name is {{ company_name }}

當然變數內容也可以是列表或字典結構,搭配loop使用:

shell
# docx file
{% for elem in list %}
  {{ elem }}
{% endfor %}

{% for key, value in dict.items() %}
  {{ key }} {{ value }}
{% endfor %}

loop也能用來產生表格:

TIP

  1. 個人經驗,橫向loop結果會有欄寬大小不一致的情況,不如直接寫出所有欄位(參考上方圖片)
  2. 在loop中使用 {% hm %} / {% vm %} 來水平/垂直合併欄位(參考上方圖片)

嵌入外部文件

python
doc_temp = DocxTemplate('tpl_main.docx')
# *** .docx ONLY ***
doc_sub = doc_temp.new_subdoc('tpl_sub.docx')
doc_temp.render({'doc_sub': doc_sub})
doc_temp.save('main.docx')

對應範例的文件模板如下:

shell
# docx file
{{p doc_sub }}

自動生成目錄

起初我以為這是個「簡單」的任務,稍微研究了一下便發現沒有想像中容易,因為分頁是Word軟體的排版引擎所提供的複雜功能,該資訊不存在於檔案結構內容中,這意味著必須透過Word軟體或其他方式render出分頁,因此我想到的方法是轉檔為PDF來確定頁數。關於將Word文件轉檔為PDF,在StackOverflow上有相當多的討論,但無論是利用什麼Python套件,它終究不是一個跨平台的解決方案,基本上都是依賴win32com,也就是Windows環境限定,對於開發和部署為不同OS環境的我來說並非適合的解決方案。最終我選擇使用LibreOffice來協助轉檔,然後讀取PDF取得分頁資訊後再重新render出一份具有目錄頁數的Word文件。

TIP

下方範例中的pdfminer套件名稱為pdfminer.six,安裝時請特別注意

python
import os
import platform
import subprocess
from pdfminer.layout import LTTextContainer
from pdfminer.high_level import extract_pages


def find_toc(name: str):
    # convert docx to pdf with LibreOffice
    env = platform.system()
    if env == 'Windows':
        subprocess.run(f'C:\Program Files\LibreOffice\program\soffice.exe --convert-to pdf:writer_pdf_Export . {name}.docx')
    else:
        subprocess.run(f'libreoffice --convert-to pdf:writer_pdf_Export {name}.docx')

    toc = []
    keys = ['壹、標題文字', '貳、標題文字', '參、標題文字']
    start_page = 5
    bias = start_page - 1

    for idx, page in enumerate(extract_pages(f'{name}.pdf'), start=1):
        if idx < start_page:
            continue
        page_text = ''.join(elem.get_text() for elem in page if isinstance(elem, LTTextContainer))
        for key in keys:
            if key in page_text:
                toc.append(idx - bias)
    
    os.remove(f'{name}.pdf')
    return toc

 # render for toc
doc_temp = DocxTemplate('tpl_main.docx')
data['toc'] = []
doc_temp.render(data)
doc_temp.save('main.docx')
toc = find_toc('main')
# update toc & re-render
doc = DocxTemplate('tpl_main.docx')
data['toc'] = toc
doc.render(data)
doc.save('main.docx')

在上面的範例中,我寫了一個find_toc,根據不同OS環境呼叫LibreOffice執行轉檔任務,接著讀取PDF尋找keys中的標題所在分頁後回傳,再將分頁資訊交給DocxTemplate重新render出目錄的頁數。當然了,你得事先在模板中建立目錄,然後將頁數的部份修改為docxtpl標籤語法。

Last updated: