PythonによるWordレポートの自動生成:データ抽出からテンプレートレンダリングまで
一、はじめに
運用業務において、定期的にフォーマットが固定された月次レポート(業務量統計、パフォーマンス分析など)を生成する必要があることがよくあります。データソースはDB2、Oracle、SQL Server、MySQL、AS400(DB2 for i)など多岐にわたります。従来の手動作成方式は時間がかかり、エラーも発生しやすいため、Pythonに基づいた自動レポート生成システムを導入しました。本システムは複数データソースの統一抽出 → 中間テーブル保存 → データ変換 → テンプレートレンダリングという完全な連携を実現し、月次レポート作成時間を数時間から数分に短縮しました。
システムの主な特徴:
- 多様なデータベース対応:統一ツール層でDB2、Oracle、MySQL、SQL Server、AS400などのドライバをラップし、上層のビジネスコードはデータベースの差異を意識する必要がありません。
- 三層デカップリングアーキテクチャ:データ抽出、変換、レンダリングの各層が独立しており、保守とテストが容易です。
- テンプレートとデータの分離:
docxtplを使用してWordテンプレートにプレースホルダを埋め込み、スタイル調整にはコード変更が不要です。 - 複雑な表のサポート:動的な行数、多列統計、パーセント計算、千位区切りフォーマットなどに対応。
- 多様な実行モード:全プロセス、変換のみ、レンダリングのみなど、様々なシナリオに柔軟に対応できます。
二、システムアーキテクチャ
2.1 全体のフロー
本番環境(DB2/Oracle/SQL Server/AS400) → 読み取り専用クエリ → 運用環境(MySQL) → 中間テーブル → データ変換 → JSON/Context → Wordテンプレート → レポート
↑ ↑
抽出スクリプト(統一ツール層) 変換モジュール
システムは3つの独立した段階に分かれています:
- データ抽出(ETL)
各種本番読み取り専用データベースから開発者提供のSQL(データベースによって構文が異なる)を実行し、結果を運用専用データベースの
t_report_dms6テーブルに書き込みます。各レコードにはcategory(年月識別子)、value(JSON形式の結果セット)、data_source(ソース識別子)が含まれます。 - データ変換
中間テーブルから指定された
categoryのすべてのレコードを読み取り、JSONを解析し、表の構築、変数計算、統計集約を経て、完全なコンテキストcontext辞書を生成します。 - レポートレンダリング
docxtplを使用してWordテンプレートをロードし、contextを渡して最終的な.docxレポートを生成します。
2.2 モジュール構成
report_builder/ # 核心変換パッケージ
├── data_fetcher.py # 中間テーブルからデータを読み取り、件数を検証
├── table_builders.py # 3つの主要な表(t1/t2/t3)を構築
├── basic_vars_builder.py # 単一値変数(var1~var19)を処理
├── table_stats_calculator.py # 表に基づく統計計算(var14/15/16/17/20)
├── report_context_builder.py # 全体を調整し、最終的なcontextを生成
data_extractor.py # データ抽出スクリプト(各get_*関数を呼び出し、内部で統一ツール層を使用)
utils/ # データベースツール、設定読み込み、ログなど
├── db2_util.py # DB2接続とクエリ
├── oracle_util.py # Oracle接続とクエリ
├── sqlserver2005_util.py # SQL Server接続とクエリ
├── mysql_util.py # MySQL接続とクエリ
├── as400_util.py # AS400 (DB2 for i) 接続とクエリ
├── load_config.py # 統一設定読み込み
├── log_util.py # 非同期ログ
└── mail_util.py # メール通知
main.py # メインエントリーポイント、3つの実行モードをサポート
三、主要技術の実装
3.1 多様なデータベースの統一ツール層
各データベースの接続パラメータとSQL実行方法は異なりますが、外部には一貫したインターフェースを提供します。例えば:
# utils/db2_util.py
def execute_db2_sql(server, db, port, user, pwd, query):
connection = ibm_db.connect(f"DATABASE={db};HOSTNAME={server};PORT={port};...")
statement = ibm_db.exec_immediate(connection, query)
return fetch_all_records(statement)
# utils/oracle_util.py
def execute_oracle_sql(host, service, port, user, pwd, query):
dsn = cx_Oracle.makedsn(host, port, service_name=service)
connection = cx_Oracle.connect(user, pwd, dsn)
return pd.read_sql(query, connection).values.tolist()
上層のretrieve_*関数は設定ファイルのtypeフィールドに基づいて対応するツール関数を自動的に選択します:
# config/config.yaml
databases:
dms6:
type: db2
host: 10.1.1.100
database: DMSDB
port: 50000
user: readonly
password: xxx
operatdb:
type: mysql
host: 10.1.1.200
port: 3306
user: ops
password: xxx
database: report_db
呼び出し時にはタイプに基づいて動的にインポートします:
def execute_query(db_name, query):
db_config = find_configuration_by_name(settings, db_name)
tool = importlib.import_module(f"tools.{db_config['type']}_tool")
return tool.execute_query(**db_config, query=query)
3.2 中間テーブル設計
履歴スナップショットを保持し、本番データベースからデカップリングするため、MySQLのt_report_dms6テーブルを設計しました:
CREATE TABLE t_report_dms6 (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
category VARCHAR(50) NOT NULL COMMENT '年月、例:202603',
value JSON NOT NULL COMMENT 'クエリ結果のJSONデータ',
data_source VARCHAR(255) COMMENT 'データソース識別子',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_category_source (category, data_source)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
categoryは月ごとにパーティション分割し、履歴遡りをサポート。valueはネイティブJSON型を使用し、配列やオブジェクトを保存可能。- 一意制約により、同一月のデータを再実行しても上書き更新されます。
3.3 データ抽出と保存
各統計指標は独立したクエリ関数に対応します。例えば:
def retrieve_product_count(year, month):
query = "SELECT COUNT(*) FROM dmsusr.AGR_AgreementSpec WHERE CURRENTVERSION = 'Y'"
records = execute_db2_sql(config['dms6'], query) # 実際には統一ツール層を呼び出す
count = records[0][0]
data = {"product_count": count}
store_in_t_report_dms6(
category=f"{year}{month:02d}",
data=data,
source="dmsusr.AGR_AgreementSpec.新規製品数"
)
すべてのretrieve_*関数はextract_all_data(year, month)によって一括で呼び出され、データの完全性が保証されます。
3.4 データ変換の核心
3.4.1 一括読み取りと検証
fetch_all_data(year, month, expected_count)は中間テーブルからすべてのレコードを読み取り、件数を検証し、欠落がないようにします:
records = execute_mysql_query("SELECT data_source, value FROM t_report_dms6 WHERE category = %s", (category,))
if len(records) != expected_count:
raise ValueError(f"期待されるレコード数は{expected_count}件ですが、{len(records)}件取得しました")
return {source: json.loads(value) for source, value in records}
3.4.2 表の構築(t1を例に)
t1は「銀行別・月別取引件数」であり、元のJSONリストから2次元配列を生成し、「半年合計」と「半年業務量に占める割合」の列を追加する必要があります。
主要なロジック:
- すべての月フィールド(例:
202509、202510など)を動的に抽出し、数値順にソート。 - 各銀行について6ヶ月の合計を計算すると同時に、各月の合計も累積。
- 各行の割合を計算し、最後に合計行を追加。
- ソートルール:まず割合の降順、次に半年合計の降順。
def construct_table_t1(data_dict):
data = data_dict["dmsusr.ITX_Transaction.銀行別月別取引件数"]
month_keys = sorted(set(...), key=int)
# 各銀行のデータを計算
for bank in data:
row_sum = sum(bank.get(m,0) for m in month_keys)
# 行を保存...
# 割合を計算
for row in table:
ratio = row_sum / total_all * 100
row.append(f"{ratio:.2f}%")
# 多段階ソート
table.sort(key=lambda r: (float(r[-1].rstrip('%')), int(r[-2].replace(',',''))), reverse=True)
# 合計行を追加
table.append(total_row)
3.4.3 特殊な列:当月新規取引の割合(t3)
t3表には「当月新規取引に占める割合」の列が必要です。これは各銀行の取引量がすべての銀行の総取引量に占める割合を意味します。実装時にはまず総取引量を計算します:
total_transactions = sum(item["transaction_cases"] for item in data)
for bank in data:
percentage = bank["transaction_cases"] / total_transactions * 100
row.append(f"{percentage:.1f}%")
3.5 Wordテンプレートとdocxtpl
テンプレートではJinja2構文を使用します:
- 通常の変数:
{{ var1 }} - 表の行ループ:
{%tr for row in t1 %}と{%tr endfor %}を表の最初のセルと最後のセル内に配置し、WPSによる削除を避けます。同時にラベル文字を白色に設定して非表示にします。
重要な注意点:WPSでドキュメントを保存すると、未知のカスタムラベルが削除され、{% for %}が失われることがあります。解決策:
- Microsoft Wordでテンプレートを編集する(互換性が最も良い)。
- または
{%tr %}ラベルを使用し、セル内に配置して非表示文字のテクニックを組み合わせる。 - 最も徹底的な方法:コードで動的に表の行を生成する(テンプレートループを諦める)が、コードの複雑さが増します。
3.6 千位区切りとパーセントフォーマット
すべての数値は表示前に統一してフォーマットします:
def format_number(value: int) -> str:
return f"{value:,}" # 12345 -> "12,345"
def format_percentage(value: float, decimals: int = 2) -> str:
return f"{value:.{decimals}f}%"
Decimal型を扱う場合は、最初にfloatまたはintに変換する必要があります。さもなければ、JSONシリアライズでエラーが発生します。
四、実行とデプロイ
4.1 メインエントリースクリプトmain.py
3つのモードをサポートします:
# 全プロセス:データ取得 + 変換 + レンダリング
py3 main.py --mode full --year 2026 --month 3 --template template.docx
# 中間テーブルデータのみを使用(データ取得済みと仮定)
py3 main.py --mode mid --year 2026 --month 3 --template template.docx
# 既存のJSONコンテキストファイルを使用
py3 main.py --mode json --context context_202603.json --template template.docx
4.2 定時タスク
cronを使用して毎月1日に全プロセスを自動実行:
0 2 1 * * cd /path/to/project && py3 main.py --mode full --year $(date +\%Y) --month $(date +\%m -d 'last month') --template template.docx
4.3 依存環境
- Python 3.8+
- ライブラリ:
docxtpl,python-docx,pymysql,ibm_db,cx_Oracle,pymssql,pyodbc(AS400用),pyyaml,python-dateutil - データベース:MySQL 8+(運用用)、DB2/Oracle/SQL Server/AS400(読み取り専用本番環境)
五、よくある問題と解決策
| 問題 | 原因 | 解決策 |
|---|---|---|
ModuleNotFoundError: No module named 'tools' |
パスが正しくない | スクリプトの先頭にsys.path.insert(0, os.path.dirname(\_\_file\_\_))を追加 |
ibm\_dbが指定されたモジュールを見つけられない |
DB2ドライバまたはVC++ランタイムライブラリが不足 | IBM Data Server DriverとVC++ Redistributableをインストール |
| Oracle接続失敗 | TNSまたはservice_name設定エラー | cx\_Oracle.makedsnでDSNを生成し、TNSファイル依存を回避 |
| AS400で中国語文字化け | エンコーディング設定が正しくない | 接続文字列にCCSID=1208を追加、またはpyodbcのCONVERTオプションを使用 |
| SQL Server 2005接続失敗 | ドライババージョンが古い | pymssqlを使用し、TDS 7.0バージョンを指定 |
JSONシリアライズ失敗 Decimalは非サポート |
DB2がDecimal型を返す | 代入前にfloat(value)またはカスタムJSONエンコーダを使用 |
WPS保存後{% for %}が失われる |
WPSは未知のラベルをクリーンアップする | Microsoft Wordでテンプレートを編集するか、ラベルをセル内に配置して非表示に |
表ループレンダリングエラー Encountered unknown tag 'endfor' |
{%tr %}ラベルを使用していない |
表の行ループには{%tr for %}と{%tr endfor %}を使用する必要がある |
| 千位区切りフォーマット後のソートが失敗 | 文字列比較による順序エラー | ソート時に元の数値を使用し、表示時にフォーマットする |
六、まとめと将来展望
本システムにより、月次レポート作成は手動操作から完全自動化へと移行し、効率と正確性が大幅に向上しました。統一データベースツール層を通じて、DB2、Oracle、SQL Server、AS400などの多様な本番データベースを容易に接続でき、上層のビジネスロジックは完全に再利用可能です。階層化設計により、各モジュールは独立してテストや交換が可能であり、将来の要件変更に適応できます。
将来の改善点:
- 可視化設定の追加:Webインターフェースでレポートテンプレートやデータソースを選択し、使用の敷居を下げる。
- より多くの出力形式のサポート:PDF、HTMLなど。
- リアルタイム監視とアラート:データ抽出の失敗またはタイムアウト時に自動通知。
- 増分データ抽出:大量データのシナリオでは、変更されたデータのみを抽出。
全体のコードは半年間にわたり本番環境で安定稼働し、50以上のレポートを生成し、1つもエラーが発生していません。同様に多様なデータベースと反復的なレポート作業に直面している運用開発者の方々への参考になれば幸いです。
付録:コードリポジトリには企業内部のデータベース接続設定が含まれているため、公開できません。しかし、上記の核心的なロジックと設計パターンは、どのような類似シナリオにも再利用可能です。詳細なコードスニペットやデータベース適応層の実装詳細が必要な場合は、お気軽にお問い合わせください。