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.
基本用法
使用方式很簡單,以下是官方文件的範例:
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
- 字典結構中的keyname對應模板中的標籤語法變數{{ keyname }},變數名稱不允許含有空格
- 標籤語法與變數名稱之間必須以空格分隔,例如{{ keyname }},變數名稱前後都有空格
對應範例的文件模板如下:
# docx file
my company name is {{ company_name }}
當然變數內容也可以是列表或字典結構,搭配loop使用:
# docx file
{% for elem in list %}
{{ elem }}
{% endfor %}
{% for key, value in dict.items() %}
{{ key }} {{ value }}
{% endfor %}
loop也能用來產生表格:
TIP
- 個人經驗,橫向loop結果會有欄寬大小不一致的情況,不如直接寫出所有欄位(參考上方圖片)
- 在loop中使用 {% hm %} / {% vm %} 來水平/垂直合併欄位(參考上方圖片)
嵌入外部文件
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')
對應範例的文件模板如下:
# docx file
{{p doc_sub }}
自動生成目錄
起初我以為這是個「簡單」的任務,稍微研究了一下便發現沒有想像中容易,因為分頁是Word軟體的排版引擎所提供的複雜功能,該資訊不存在於檔案結構內容中,這意味著必須透過Word軟體或其他方式render出分頁,因此我想到的方法是轉檔為PDF來確定頁數。關於將Word文件轉檔為PDF,在StackOverflow上有相當多的討論,但無論是利用什麼Python套件,它終究不是一個跨平台的解決方案,基本上都是依賴win32com,也就是Windows環境限定,對於開發和部署為不同OS環境的我來說並非適合的解決方案。最終我選擇使用LibreOffice來協助轉檔,然後讀取PDF取得分頁資訊後再重新render出一份具有目錄頁數的Word文件。
TIP
下方範例中的pdfminer套件名稱為pdfminer.six,安裝時請特別注意
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標籤語法。