1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonでWordファイルを生成す!(Jinja2)

Posted at

はじめに

みなさまはPythonでWordを生成したいと思ったこととはあるでしょうか?
勿論ないですよね。需要はわずかかもしれませんが解説していきたいと思います。

ライブラリ

今回はdocxtplというライブラリを使います。

こちらは、wordファイル(.docx)のテンプレートを準備しておき、Jinja2の構文{{ tag_name }}で指定するプレースホルダーにデータ・文字・数値を置換していくという仕組みです。
この仕組みによりpython上では難しい装飾や段落をword上で編集しつつ、自動生成することができます。

インストール方法

pip install docxtpl

事前準備

docxtplはwordを直接生成する仕組みではないので、事前に.docxファイルを準備する必要があります。

とりあえず今回は納品書をテーマに、以下のようなdocxファイルを準備しました。

template.docx
納品書
発行日: {{ issue_date }}
 宛名: {{ customer_name }} 様
 発行元: uniuni
{{ customer_name }} 様  下記の通り、ご請求申し上げます。
No.	品名	単価	数量	金額
{%tr for item in items %}				
{{ loop.index }}	{{ item.name }}	{{ item.price }}	{{ item.quantity }}	{{ item.total }}
{%tr endfor %}				
合計				{{ grand_total }}

{% if remarks %}
備考
{{ remarks }}
{% endif %}

image.png

docxtplの使い方

一通りの使い方を解説していきます。

値の置換

from docxtpl import DocxTemplate
from datetime import datetime

# テンプレートファイルを読み込む
doc = DocxTemplate("template.docx")

# テンプレート全体に埋め込むデータ(dict)を準備
context = {
    "issue_date": datetime.now().strftime('%Y年%m月%d日'),
    "customer_name": "株式会社△△△△△",
}

# データをレンダリング(埋め込み)
doc.render(context)

# 新しい名前でファイルを保存
output_filename = f"invoice_{context['customer_name']}.docx"
doc.save(output_filename)

これを実行すると以下のdocxファイルが生成されます。

image.png

見事、値が置換されましたね。
{{ customer_name }}を2回使っていますが、これで複数回出現させても置換してくれることがわかりました。

if文

続いてif文です。

.docx
{% if remarks %}
備考
{{ remarks }}
{% endif %}

ポイントとしては、{{ }}ではなく、{% %}となっている点です。
以下のコードを実行すると、

from docxtpl import DocxTemplate
from datetime import datetime

# テンプレートファイルを読み込む
doc = DocxTemplate("template.docx")

# テンプレート全体に埋め込むデータ(dict)を準備
context = {
    "issue_date": datetime.now().strftime('%Y年%m月%d日'),
    "customer_name": "株式会社△△△△△",
+    "remarks":"7月25日までにお振込みください。",
}

# データをレンダリング(埋め込み)
doc.render(context)

# 新しい名前でファイルを保存
output_filename = f"invoice_{context['customer_name']}.docx"
doc.save(output_filename)

備考欄が表示されました!
image.png

Noneを指定することで非表示にできます。

context = {
    "issue_date": datetime.now().strftime('%Y年%m月%d日'),
    "customer_name": "株式会社△△△△△",
-    "remarks":"7月25日までにお振込みください。",
+    "remarks":None,
}

使用可能な条件分岐構文は以下になります。
{% if 条件 %}
{% elif 条件 %}
{% else %}
{% endif %}

表の作成

ワードの資料で避けて通れないのが表の作成です。
しかしこちらも強力にサポートされています。

少し複雑なので.docxで定義する表を再掲します。
image.png

{%tr for item in items %}から{%tr endfor %}までが表のループ部分になります。

表データitemsを追加した以下のコードで実行します。

from docxtpl import DocxTemplate
from datetime import datetime

# テンプレートファイルを読み込む
doc = DocxTemplate("template.docx")

# 表に流し込むデータ(辞書のリスト)を準備
invoice_items = [
    {"name": "静音マウス", "price": 5000, "quantity": 2, "total": 10000},
    {"name": "静音キーボード", "price": 8000, "quantity": 1, "total": 8000},
    {"name": "USB-Cハブ", "price": 4500, "quantity": 1, "total": 4500},
]

# テンプレート全体に埋め込むデータ(dict)を準備
context = {
    "issue_date": datetime.now().strftime('%Y年%m月%d日'),
    "customer_name": "株式会社△△△△△",
    "items": invoice_items,  # ここに表のデータを渡す
    "remarks":"7月25日までにお振込みください。",
}

# データをレンダリング(埋め込み)
doc.render(context)

# 新しい名前でファイルを保存
output_filename = f"invoice_{context['customer_name']}.docx"
doc.save(output_filename)

ということで実行するとそれらしくなってきました。

image.png

ワンポイント
jinja構文にはenumerateやzipなどの便利な関数はありません。
代わりに{{ loop.index }} でindex値が取得可能です。
loop.index = 1から始まる現在のループ回数
loop.index0 = 0から始まる現在のループ回数

{{ items[loop.index0].name }}といった記載も可能です。

forのネスト
ネストすることも可能です。

{% for x in range(3) %}
{% set loop1 = loop.index %}
{% for y in range(2) %}
{{ loop1 }} : {{ loop.index }}
{% endfor %}
{% endfor %}

画像の置き換え

続いて画像を扱っていきます。
画像が扱えるとグッと表現の幅が広がりますね。
今回はダミーの画像をdocx内に貼り付けておき、それを入れ替えてみます。

from docxtpl import DocxTemplate
from datetime import datetime

# テンプレートファイルを読み込む
doc = DocxTemplate("template.docx")

# 表に流し込むデータ(辞書のリスト)を準備
invoice_items = [
    {"name": "静音マウス", "price": 5000, "quantity": 2, "total": 10000},
    {"name": "静音キーボード", "price": 8000, "quantity": 1, "total": 8000},
    {"name": "USB-Cハブ", "price": 4500, "quantity": 1, "total": 4500},
]

# テンプレート全体に埋め込むデータ(dict)を準備
context = {
    "issue_date": datetime.now().strftime('%Y年%m月%d日'),
    "customer_name": "株式会社△△△△△",
    "items": invoice_items,  # ここに表のデータを渡す
    "remarks":"7月25日までにお振込みください。",
    "grand_total": sum(item['total'] for item in invoice_items) # 合計金額を計算
}

# データをレンダリング(埋め込み)
doc.render(context)

# 画像の置き換え
+doc.replace_pic('dummy_image', 'test.jpg')

# 新しい名前でファイルを保存
output_filename = f"invoice_{context['customer_name']}.docx"
doc.save(output_filename)

docx内のdummy_imageという画像をtest.jpgに置き換えます。
しかし、これがひと癖あって、LibreOfficeだと埋め込んだ画像を右クリック→プロパティ→アクセシビリティの名前となります。

image.png

実行すると、画像も入れ替えができました!

image.png

表示書式の変更

金額は,区切りを要求されがちですよね。
Jinja2のカスタムフィルタで簡単に実装可能です。
今回はyenメゾットを追加してみました。

例.docx
{{ item.price | yen }}
from docxtpl import DocxTemplate
from datetime import datetime
import jinja2

# テンプレートファイルを読み込む
doc = DocxTemplate("template.docx")

# 表に流し込むデータ(辞書のリスト)を準備
invoice_items = [
    {"name": "静音マウス", "price": 5000, "quantity": 2, "total": 10000},
    {"name": "静音キーボード", "price": 8000, "quantity": 1, "total": 8000},
    {"name": "USB-Cハブ", "price": 4500, "quantity": 1, "total": 4500},
]

# テンプレート全体に埋め込むデータ(dict)を準備
context = {
    "issue_date": datetime.now().strftime('%Y年%m月%d日'),
    "customer_name": "株式会社△△△△△",
    "items": invoice_items,  # ここに表のデータを渡す
    "remarks":"7月25日までにお振込みください。",
    "grand_total": sum(item['total'] for item in invoice_items) # 合計金額を計算
}


def yen(value: float | int) -> str:
    """数値を「¥」+3 桁カンマ区切りに整形"""
    return f"¥{value:,.0f}"

jinja_env = jinja2.Environment()
jinja_env.filters["yen"] = yen

# データをレンダリング(埋め込み)
doc.render(context,jinja_env=jinja_env)

doc.replace_pic('dummy_image', 'test.jpg')

# 新しい名前でファイルを保存
output_filename = f"invoice_{context['customer_name']}.docx"
doc.save(output_filename)

image.png

ページの複製

納品書、見積書、報告書、検査記録など、同じテンプレートで沢山のファイルを生成することあります。
10、20個も生成すると、開いて印刷するのも大変なので、1枚のワードファイルにしたいこと、ありますよね?
subdoc機能を使えば複数ページの生成も(ちょっと癖はありますが)簡単です。

template_toppage.docx
納品書
発行日: {{ report_date }}
発行元: uniuni
(ここにword側で改ページを入れる)
{% for invoice_doc in invoices %} 
{{p invoice_doc }} 
{% if not loop.last %} 
(ここにword側で改ページを入れる)
{% endif %} 
{% endfor %} 
from docxtpl import DocxTemplate, Subdoc,InlineImage
from docx.shared import Mm
from datetime import datetime

# テンプレートファイル名
TOPPAGE_TPL = "template_toppage.docx"
INVOICE_TPL = "template.docx"

# 出力するファイル名
FINAL_OUTPUT_FILENAME = f"report_{datetime.now().strftime('%Y%m%d')}.docx"

# データ
all_invoices_data = [
    {
        "issue_date": datetime(2025, 7, 15).strftime('%Y年%m月%d日'),
        "customer_name": "株式会社△△△△△",
        "items": [
            {"name": "静音マウス", "price": 5000, "quantity": 2, "total": 10000},
            {"name": "静音キーボード", "price": 8000, "quantity": 1, "total": 8000},
        ],
        "remarks": "7月25日までにお振込みください。",
        "grand_total": 18000,
    },
    {
        "issue_date": datetime(2025, 7, 16).strftime('%Y年%m月%d日'),
        "customer_name": "株式会社△△△△△",
        "items": [
            {"name": "USB-Cハブ", "price": 4500, "quantity": 3, "total": 13500},
            {"name": "27インチモニター", "price": 32000, "quantity": 1, "total": 32000},
        ],
        "remarks": "請求書到着後、1ヶ月以内にお支払いください。",
        "grand_total": 45500
    },
    {
        "issue_date": datetime(2025, 7, 17).strftime('%Y年%m月%d日'),
        "customer_name": "株式会社△△△△△",
        "items": [
            {"name": "4Kモニター", "price": 45000, "quantity": 1, "total": 45000},
            {"name": "モニターアーム", "price": 7000, "quantity": 1, "total": 7000},
        ],
        "remarks": None,
        "grand_total": 52000
    }
]
# --- メイン処理 ---

#マスターの読み込み
tpl_top = DocxTemplate(TOPPAGE_TPL)

# Subdocオブジェクトのリストを作成する
subdocs = []
for index,invoice_data in enumerate(all_invoices_data):
    tpl_invoice = DocxTemplate(INVOICE_TPL)
    tpl_invoice.render(invoice_data) # subdoc_containerにrenderがないのでここでrender
    subdoc_container = tpl_top.new_subdoc(INVOICE_TPL) # マスターを元に、空のSubdocコンテナを作成する
    subdoc_container.subdocx = tpl_invoice.docx # renderしたデータを渡す。
    subdocs.append(subdoc_container)

# 最終的なコンテキストを作成
# マスターテンプレートに、表紙データと「Subdocのリスト」を渡す
context = {
    "report_date": datetime.now().strftime('%Y年%m月%d日'),# 表紙用のデータ
    "invoices": subdocs  # テンプレート内の {% for invoice_doc in invoices %}  に対応
}

# マスターテンプレートをレンダリング
# この時点で、すべてのサブドキュメントが内部で結合される
tpl_top.render(context)

# 最終ファイルを保存
tpl_top.save(FINAL_OUTPUT_FILENAME)

これで、表紙・納品書1・納品書2・納品書3の計4ページのdocxファイルが完成します。

画像について
subdocでは画像データが上手く取り扱えず、subdocで開くと画像が壊れるようです。(検索すると複数のIssueがヒットします)
画像を埋め込んでいるときはsubdocは使わず、1ファイルずつrender→saveして、後でdocxcomposeなど他のライブラリでファイル結合するのが現実的な解決策かもしれません。

それ以外の使用方法

docxtpl 機能別サンプルコード一覧

その他の機能については、以下リンクからtestsフォルダを参照してください。

それぞれポイントとなる.pyファイルの解説です。


1. 基本的なテキストとデータの差し込み

ファイル名 目的・概要 使用している主な機能・構文
order.py 注文書を作成する実践的なサンプル。ループ、条件分岐、単純な変数置換を組み合わせている。 {% for item in items %}, {% if ... %}, {{ customer_name }}
header_footer.py ヘッダーとフッターに変数(会社名など)を差し込む。 {{ company_name }}
doc_properties.py 文書のプロパティ(作成者など)に関連する変数を扱う。 {{ test }}

2. 書式設定(リッチテキスト)

ファイル名 目的・概要 使用している主な機能・構文
richtext.py RichTextの包括的なサンプル。太字、斜体、色、サイズ、ハイパーリンク、フォント、下線など様々な書式を動的に設定する。 RichText, rt.add(), {{r example }}
richtext_and_if.py if文の条件によってRichTextオブジェクトを出力する。 RichText, {% if foobar %}
richtextparagraph.py RichTextParagraphを使い、段落全体のスタイル(中央揃え、箇条書き、行間など)を動的に適用する。 RichTextParagraph, {{p example }}

3. 画像の操作

ファイル名 目的・概要 使用している主な機能・構文
inline_image.py InlineImageを使い、本文中に動的に画像を挿入する。サイズ指定やループ内での使用例も示す。 InlineImage, {{ myimage }}
replace_picture.py replace_picを使い、テンプレート内のダミー画像(python_logo.png)を別の画像に差し替える。 tpl.replace_pic()
header_footer_image.py replace_mediaを使い、ヘッダー/フッター内のダミー画像を別の画像に差し替える。 tpl.replace_media()
header_footer_image_file_obj.py replace_mediaで、ファイルパスの代わりにファイルオブジェクト(io.BytesIO)を使って画像を置換する。 tpl.replace_media() (with file objects)
header_footer_inline_image.py InlineImageを使って画像をコンテキストに渡し、テンプレートに挿入する。 InlineImage

4. 表(テーブル)の操作

ファイル名 目的・概要 使用している主な機能・構文
dynamic_table.py ループを使い、動的な列と行を持つテーブルを作成する。 {% for ... %} (列と行の両方)
cellbg.py cellbgタグを使い、テーブルのセルの背景色を動的に変更する。 {% cellbg bg %}
vertical_merge.py forループ内で、条件に応じて縦方向にセルを結合する。 itemsのループ
horizontal_merge.py 横方向にセルを結合する。 {% hm %}
vertical_merge_nested.py ネストされたループ内で、縦方向にセルを結合する。 ネストされたループ

5. 制御構文と高度な機能

ファイル名 目的・概要 使用している主な機能・構文
nested_for.py forループをネスト(入れ子に)して、複雑な階層構造のデータを展開する(例:著者とその著書リスト)。 {% for author in authors %} ... {% for book in author.books %}
merge_paragraph.py {%- ... -%}構文を使い、テンプレート上で改行されていても、出力結果では一つの段落にテキストを結合する。 {%- if ... -%}
subdoc.py new_subdocでサブドキュメントをプログラムで動的に作成し、メインの文書に挿入する。 tpl.new_subdoc()
merge_docx.py 既存のWordファイル(.docx)をそのままサブドキュメントとして読み込み、メイン文書にマージする。 tpl.new_subdoc('path/to/doc.docx')
embedded.py replace_embeddedreplace_zipnameを使い、文書に埋め込まれた別のdocxやExcel、PowerPointファイルを差し替える。 tpl.replace_embedded(), tpl.replace_zipname()
escape.py RichTextListingオブジェクトを使い、XMLで特殊文字となる< > &を安全にエスケープする方法を示す。 R(), Listing()
escape_auto.py renderメソッドのautoescape=Trueオプションを使い、自動でXML特殊文字をエスケープする。 tpl.render(..., autoescape=True)
custom_jinja_filters.py Jinja2の環境をカスタマイズして、独自のフィルタ(例: ` my_filterA`)を定義して使用する。
template_error.py 意図的にエラーを発生させ、TemplateError例外を捕捉してエラー箇所やコンテキスト情報を表示する。 try...except TemplateError
multi_rendering.py 一つのテンプレートオブジェクトを再利用して、ループ内で複数の異なるデータから複数のWordファイルを生成する。 tpl.render() (in a loop)

おわりに

いかがだったでしょうか?
pythonによるwordファイル生成の自動化でした。

wordファイルはLLMなどのAIとはすこぶる相性が悪いですが、実際の現場ではmarkdownやhtmlは理解され辛く、wordなどのofficeソフトでの出力を希望されることが多々あります。
そのような時にお役に立てば幸いです。

そしてまさかJinja2の経験がword作りに生かされるとは思ってもみませんでした。

記事を良いと思った方、役に立った方はいいね頂けると嬉しいです。

それではまた。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?