要約
- とある運用保守プロジェクトでの月次レポート作成作業を徐々に改善していくお話
- データベースから抽出した情報をもとに行うExcelでの加工・集計をPython + Excel VBAで自動化
- Excelで加工・集計した内容をパワポにまとめる作業をPythonのライブラリ(python-pptx, Openpyxl)を駆使して自動化
環境
- Windows10 64bit
- Python 3.7
- python-pptx
- Openpyxl
pip install python-pptx
# Anacondaをインストールしている環境であれば不要
pip install openpyxl
■改善その1
▼問題提起
話は遡ること約3年前。
とある運用保守のプロジェクトに参入した私は月次レポートの作成を引き継ぐことに。
- 1.データベースから必要な情報を抽出する
- 2.抽出した情報を元にExcelで加工・集計を行う
- 3.加工・集計した内容をパワポにまとめる
大雑把に言うとこのような流れ。
作成するための手順書もあり、それに沿って行えば問題なく行えそう。
そう思って着手したところ一つ大きな問題が...それは
Excelの保存にめちゃくちゃ時間が掛かる!!!
もうね、時間が掛かるってレベルを超越していて、下手したら5時間ぐらいずっと固まったままでして...
その間少なくてもExcelは使えなく、他の作業をしていても突然Excelが落ちることがあり、結局帰宅間際に保存操作をして、電源つけたまま帰宅→翌朝確認という対処法で凌ぐことに。
▼解析
Excelは加工・集計を行うデータ種類ごとにいくつかのファイルに分かれていて、今回問題になったのはその内の1つ。
そのExcelで行っていたのは、当月利用数トップ10の顧客の過去10か月分の利用数推移をグラフにするという内容。
具体的には、
- 過去9か月分のデータを保持しているシート
- 当月分のデータを保持しているシート
- この2つのシートの顧客名を結合して、当月利用数の多い順に並び変える
- トップ10をグラフにする
という流れなんですが、パフォーマンスに大きく影響していたのは
- この2つのシートの顧客名を結合して、当月利用数の多い順に並び変える
という部分。
過去9か月分と当月分とで同じ顧客名を結合するのにExcelのVLOOKUP関数を使っていました。
片方のシートの顧客名と同じ顧客名がもう一方のシートにあるのかを先頭からサーチするというのを片方のシートの最後の行まで繰り返すので、行数が多ければ多いほどサーチ回数も多くなる。
顧客数が10なら最大10×10=100回、100なら最大100×100で10,000回。
ちなみに実データの顧客数は200,000...
そりゃ果てしなく時間かかるよな┐(´д`)┌ヤレヤレ
ということで改善を検討し始めたのでした。
▼どう改善したか?
過去分と当月分の結合、当月利用数の多い順への並び替えをPythonにお願いする(`・ω・´)
具体的には以下の通りです。
- Excelシートで保持していた過去分と当月分のデータをcsvファイルにする
company_name, 201901, 201902, 201903, 201904, 201905, 201906, 201907, 201908, 201909
companyA, 10, 12, 5, 0, 36, 28, 30, 12, 10
companyB, 110, 112, 95, 58, 85, 77, 103, 137, 109
companyC, 24, 27, 32, 37, 39, 40, 38, 25, 19
company_name, 201910
companyA, 17
companyB, 142
companyD, 7
- 2つのcsvファイルをDataFrame型で読み込んで結合してcsvファイルに出力
# coding: utf-8
import pandas as pd
join_column = 'customer_name'
# 入力パス
input_path = 'input_data'
# 出力パス
output_path = 'output_data'
# 先月分までの履歴データ読み込み
df_data_old = pd.read_csv(input_path + '/last_month_data.csv', sep=',', header=0, encoding = 'cp932')
df_data_old = df_data_old.dropna(subset=[join_column]) # 結合カラムが欠損値の行は除く
# 今月データ読み込み
df_data_new = pd.read_csv(input_path + '/this_month_data.csv', sep=',', header=0, encoding = 'cp932')
df_data_new = df_data_new.dropna(subset=[join_column]) # 結合カラムが欠損値の行は除く
# 先月分までの履歴データと今月データを完全外部結合する
df_data_out = pd.merge(df_data_old, df_data_new, how='outer', on=join_column)
# 今月データの降順にソートする
df_data_out = df_data_out.sort_values(by=df_data_new.columns[1], ascending=False)
# ファイルに出力する
df_data_out.to_csv(output_path + '/out_data.csv', sep=',', index=False, encoding = 'cp932')
出力されたファイルは以下の通り。
company_name, 201901, 201902, 201903, 201904, 201905, 201906, 201907, 201908, 201909, 201910
companyB, 110, 112, 95, 58, 85, 77, 103, 137, 109, 142
companyA, 10, 12, 5, 0, 36, 28, 30, 12, 10, 17
conpanyD, , , , , , , , , , 7
companyC, 24, 27, 32, 37, 39, 40, 38, 25, 19,
▼改善結果
10秒も経たずに終わるようになりました(∩´∀`)∩ワーイ
後は、出来上がったcsvファイルを読み込んでグラフを作成する部分をExcel VBAで実装し、Pythonでデータ作成→Excelでグラフ作成という流れをバッチファイルで実装し、ワンクリックでグラフ作成(画像で保存)まで行えるようにしました。
matplotlibを使えばPythonでグラフ作成まで完結できるんですが、そうするとこれまでExcelで作成していたグラフとどうしても見栄えは変わってきてしまうので。
それをレポートの提出先に説明して了承を得るというプロセスが面倒でしたし、業務的にはそこまでする必要は全くなかったので、このままということにしました。
改善その2
▼問題提起
記事のタイトルに見合った内容はここからになりますw
改善その1で行った対応を他のファイルについても行って、レポート作成の中の2.については基本的にワンクリックでできるようになりました。
- 1.データベースから必要な情報を抽出する
- 2.抽出した情報を元にExcelで加工・集計を行う
- 3.加工・集計した内容をパワポにまとめる
データベースからの情報抽出についてはセキュリティ上の制約が厳しく、自動化の仕組みを入れるのは難しそうで、自動化できそうなのはパワポにまとめるところだな...
ということで、まずはパワポを自動で作成できる方法を探し始めたのでした。
▼調査1
まず思い浮かんだのがパワポのVBA。
マクロの記録で作られたコードをベースにしたらすぐできるのでは(* ̄▽ ̄)フフフッ♪
とメニューを一通り探してもそのような機能が見つからず...どうやらあるバージョンからパワポでのマクロの記録機能はなくなったようでして...
VBAをゴリゴリ書いていくしかなさそうで、学習コストをかけてまで進めるモチベーションはなく、他の方法を探すこととしました。
▼調査2
Pythonのライブラリでパワポを操作できるものはないのかな...とグーグルさんにお願いしたら...見つかりました!
Qiita内を検索しても先駆者は結構いらっしゃるようで、これを駆使して改善するぜ(`・ω・´)シャキーン
ということにしました。
合わせて、加工・集計した内容をExcelから取り出す部分は同じくPythonライブラリであるopenpyxlを使うことに。
▼どう改善したか?
テンプレートの作成
まずはアウトプットとなるパワポについて、毎回イチから作るのかテンプレートを用意してそこから作成するのかという方針決めから。
フォーマットは毎回同じで、イチから作り直す必要性もなかったので、テンプレートを用意してそこから作成する方針に。
テンプレートの中で、表オブジェクトなどには適切なオブジェクト名を付け、Pythonで固定のオブジェクト名で参照できるようにしました。
Pythonでパワポを読み込む
まずは、テンプレートとして作成したパワポをPythonで読み込むまでを実装。
起動時引数として、作成対象の年月を指定させるようにしました。
これは出力ファイル名などで使用します。
また、レポートの対象期間を記載する箇所があるので開始日、終了日もそこから設定。
# coding: utf-8
import sys
import numpy as np
import pandas as pd
import openpyxl
import pptx
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.util import Inches, Pt
from dateutil.relativedelta import relativedelta
from datetime import datetime, timedelta
ARGV_SIZE = 2 # コマンドライン引数の数
# 引数の数が異なれば終了
if len(sys.argv) != ARGV_SIZE:
print('Error : Please specify ', str(ARGV_SIZE - 1) , ' arguments.') # プログラム名が引数の1つなので
sys.exit(1)
# 引数から値を取得
target_yyyymm = sys.argv[1]
# yyyy-mm形式として認識できない場合はエラーとする
start_date = target_yyyymm + '-01'
try:
start_date = datetime.strptime(start_date, '%Y-%m-%d')
except ValueError:
print('Error : For the argument, specify the \'yyyy-mm\' format.')
sys.exit(1)
end_date = start_date + relativedelta(months = 1) - timedelta(days = 1)
# 入力ファイル名
input_file_name = 'template/Template.pptx'
# 出力ファイル名
file_name_base = 'Monthly report '
output_file_name = 'output_file/' + file_name_base + str(target_yyyymm) + '.pptx'
# パワポテンプレート読込
prs = pptx.Presentation(input_file_name)
# *************************** #
# スライドごとに必要な処理を行う #
# *************************** #
for page_no, sld in enumerate(prs.slides, start=1):
・
・
・
最後の"スライドごとに必要な処理を行う"というfor文のところで、各スライドのオブジェクトを取得して、スライドごとに処理を行う構造です。
python-pptxでは基本的に以下の3階層を意識して処理を組み立てていけばいいのかなと感じました。
Presentationが1つのファイル全体を表していて、各スライドがSlide。
Slideの集合体がSlides。
各スライドのオブジェクトがShapeで、その集合体がShapes。
グラフをパワポに貼り付ける
まずは簡単なところから。
改善その1でワンクリックで画像保存できるようにしたグラフを貼り付ける部分から着手。
画像の貼り付けには指定した画像ファイルをスライド上の指定した位置に張り付けるadd_picture を使います。
# *************************** #
# スライドごとに必要な処理を行う #
# *************************** #
for page_no, sld in enumerate(prs.slides, start=1):
#########################
# 保存済の画像を貼り付ける #
#########################
if page_no == 11:
# 画像ファイル名, 貼り付ける際の横位置(インチ), 貼り付ける際の縦位置(インチ)
pic = sld.shapes.add_picture('グラフ.png', Inches(0.7), Inches(1.4))
# 画像の幅がスライドの幅より大きい場合のみスライドの幅にそろえる
if pic.width > prs.slide_width:
pic.width = prs.slide_width
画像の貼り付けは複数ページで行っているので少し汎用的に。
# 貼り付ける画像の情報を取得する
# 引数:
# なし
# 戻り値:
# 貼り付ける画像情報一覧(DataFrame型)
def get_pic_list():
pic_list = [
# ページ番号, ファイル名, 貼り付ける際の横位置(インチ), 貼り付ける際の縦位置(インチ)
[11, '../../log_summary/グラフ作成/グラフ/グラフ1.png', 0.8, 2.1]
,[12, '../../access_summary/グラフ作成/グラフ/グラフ2.png', 0.8, 2.1]
(中略)
,[20, '../../月次データ/グラフ作成/グラフ/グラフ_月次データ5.png', 0, 1.4]
]
df_pic = pd.DataFrame(pic_list)
df_pic.columns = ['page_no', 'pic_file_name', 'paste_pos_left', 'paste_pos_top']
return df_pic
# 貼り付ける画像情報一覧
df_pic = get_pic_list()
# *************************** #
# スライドごとに必要な処理を行う #
# *************************** #
for page_no, sld in enumerate(prs.slides, start=1):
###################################
# 指定ページに保存済の画像を貼り付ける #
###################################
df_tmp = df_pic[df_pic['page_no'] == page_no]
if not df_tmp.empty:
pic = sld.shapes.add_picture(df_tmp['pic_file_name'].values[0], Inches(df_tmp['paste_pos_left'].values[0]), Inches(df_tmp['paste_pos_top'].values[0]))
# 画像の幅がスライドの幅より大きい場合のみスライドの幅にそろえる
if pic.width > prs.slide_width:
pic.width = prs.slide_width
画像の貼り付けを行うページの一覧をlistで保持し、必要な情報をDataFrameで返すようにしました。
テキストの編集
方法としてはテキストが記載されているオブジェクトであるtext_frameオブジェクトを更新することとなります。
Working with text — python-pptx 0.6.18 documentation
こちらのチュートリアルを参考にしました。
# *************************** #
# スライドごとに必要な処理を行う #
# *************************** #
for page_no, sld in enumerate(prs.slides, start=1):
if page_no == 1:
# スライド内のshpaeオブジェクトの集合体から編集対象のオブジェクトを特定する
for shape_no, shape in enumerate(sld.shapes, start=1):
# 編集対象のオブジェクト
if shape.name == 'TextBox_Title_YYYYMM':
tf = shape.text_frame
tf.text = shape.text_frame.text.replace('YYYY-MM', target_yyyymm)
tf.paragraphs[0].font.size = Pt(36)
tf.paragraphs[0].font.color.rgb = font_color_rgb
tf.paragraphs[0].font.name = RGBColor(255, 255, 255)
tf.paragraphs[0].alignment = 'Calibri' # 書体
私の実装に問題がある可能性はありますが、テキストのみ設定してフォントサイズなどのプロパティ項目はそのままとしたいのですが、テキストを設定したタイミングでリセット?されてしまうようでして...
という事情で必要なフォントのプロパティを設定し直しています。
このtext_frameオブジェクトの更新も少し汎用的にします。
# text_frameを更新する
# 引数:
# org_tf 更新元となるtext_frame
# text textの更新値
# font_size font.sizeの更新値(pptx.util.Pt型)
# font_color_rgb font.color.rgbの更新値(pptx.dml.color.RGBColor型)
# font_name font.nameの更新値
# alignment alignmentの更新値(省略時はPP_ALIGN.LEFT)
# 戻り値:
# なし
def make_text_frame(org_tf, text, font_size, font_color_rgb, font_name, alignment = PP_ALIGN.LEFT):
tf = org_tf
tf.text = text
tf.paragraphs[0].font.size = font_size
tf.paragraphs[0].font.color.rgb = font_color_rgb
tf.paragraphs[0].font.name = font_name
tf.paragraphs[0].alignment = alignment
# *************************** #
# スライドごとに必要な処理を行う #
# *************************** #
for page_no, sld in enumerate(prs.slides, start=1):
if page_no == 1:
# スライド内のshpaeオブジェクトの集合体から編集対象のオブジェクトを特定する
for shape_no, shape in enumerate(sld.shapes, start=1):
# 編集対象のオブジェクト
if shape.name == 'TextBox_Title_YYYYMM':
make_text_frame(
shape.text_frame
,shape.text_frame.text.replace('YYYY-MM', target_yyyymm)
,Pt(36)
,RGBColor(255, 255, 255)
,'Calibri')
設定するテキストの部分、
"shape.text_frame.text.replace('YYYY-MM', target_yyyymm)"
となっていますが、これは元々設定してあるテキストの'YYYY-MM'の部分を対象年月で置き換えるという実装にしています。
加工・集計した内容をExcelから取り出す
ここからは、Excelを操作するopenpyxlも使っていきます。
今回想定している用途は、Excelで加工・集計済のデータを集めてくることのみですので、それに特化した実装としました。
# Excelからデータを読み込む
# 引数:
# workbook_path 読み込むExcelファイルのパス
# sheet_name 読み込むExcelファイルのシート名
# sheet_range_start 読込範囲の開始点
# sheet_range_end 読込範囲の終了点
# 戻り値:
# Excelから取得したデータリスト
def get_excel_data(workbook_path, sheet_name, sheet_range_start, sheet_range_end):
list_data = []
workbook =openpyxl.load_workbook(workbook_path, read_only=True, data_only=True)
sheet = workbook[sheet_name]
sheet_range = sheet[sheet_range_start : sheet_range_end]
for row in sheet_range:
for cell in row:
# 該当セルの値取得
cell_value = str(cell.value)
# 該当セルに値が存在する場合にリスト追加
if cell_value is not None:
list_data = np.append(list_data, cell_value)
workbook.close()
return list_data
# *************************** #
# スライドごとに必要な処理を行う #
# *************************** #
for page_no, sld in enumerate(prs.slides, start=1):
if page_no == xx:
# 記載する情報の取得
list_customer_user = get_excel_data(
'../../Usage_summary/3.グラフ作成/日ごとアクセス数グラフ.xlsm'
,'集計_ユーザー数'
,'D5'
,'G6')
Excelから取り出した内容をパワポに転記する
ここでは、転記先としてパワポ内の表を想定した実装としています。
こういう表の左上~右下の空白部分に空白部分にExcelから取り出したデータを順に入れていくこととします。
ただし、上記の表だと一番左上のようにデータを入れたくないスペースには何も入れないようスキップできるようにします。
# tableを更新する
# 引数:
# org_table 更新元となるtable
# data_list tableに設定していくデータ
# 戻り値:
# なし
def make_table(org_table, data_list, font_size, font_color_rgb, font_name, alignment = PP_ALIGN.LEFT, skip_rows_list = []):
table = org_table
list_num = 0
# 表内で空白のセルに順次Excelから読み込んだデータをセットしていく
for cell_no, cell in enumerate(table, start=1):
if cell_no in skip_rows_list: # スキップ
continue
if len(cell.text) == 0 and cell.is_spanned == False:
make_text_frame(
cell.text_frame
,str(data_list[list_num])
,font_size
,font_color_rgb
,font_name
,alignment)
list_num += 1
# text_frameを更新する
# (省略)
def make_text_frame(org_tf, text, font_size, font_color_rgb, font_name, alignment = PP_ALIGN.LEFT):
(省略)
# *************************** #
# スライドごとに必要な処理を行う #
# *************************** #
for page_no, sld in enumerate(prs.slides, start=1):
if page_no == xx:
# 記載する情報の取得
list_customer_user = get_excel_data(
'../../Usage_summary/3.グラフ作成/日ごとアクセス数グラフ.xlsm'
,'集計_ユーザー数'
,'D5'
,'G6')
for shape_no, shape in enumerate(sld.shapes, start=1):
if shape.name == '表_Customer_User_Num':
# 表内で空白のセルに順次Excelから読み込んだデータをセットしていく
make_table(
shape.table.iter_cells()
,list_customer_user
,Pt(16)
,RGBColor(0, 0, 0)
,'MS Pゴシック'
,PP_ALIGN.CENTER
,[1])
tableオブジェクトをセルの集合体として扱い、空白のセルに順次Excelから読み込んだデータをセットしていくようになっています。
ただし、make_tableを呼び出す際の最後の引数でスキップするセルを指定できるようにしています。
データをセットする部分はテキストの編集にあたるので、ここまでで実装していたtext_frameを更新する関数を使います。
ソースコードの全体像
# coding: utf-8
import sys
import numpy as np
import pandas as pd
import openpyxl
import pptx
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.util import Inches, Pt
from dateutil.relativedelta import relativedelta
from datetime import datetime, timedelta
# 貼り付ける画像の情報を取得する
# 引数:
# なし
# 戻り値:
# 貼り付ける画像情報一覧(DataFrame型)
def get_pic_list():
pic_list = [
# ページ番号, ファイル名, 貼り付ける際の横位置(インチ), 貼り付ける際の縦位置(インチ)
[11, '../../log_summary/グラフ作成/グラフ/グラフ1.png', 0.8, 2.1]
,[12, '../../access_summary/グラフ作成/グラフ/グラフ2.png', 0.8, 2.1]
(中略)
,[20, '../../月次データ/グラフ作成/グラフ/グラフ_月次データ5.png', 0, 1.4]
]
df_pic = pd.DataFrame(pic_list)
df_pic.columns = ['page_no', 'pic_file_name', 'paste_pos_left', 'paste_pos_top']
return df_pic
# text_frameを更新する
# 引数:
# org_tf 更新元となるtext_frame
# text textの更新値
# font_size font.sizeの更新値(pptx.util.Pt型)
# font_color_rgb font.color.rgbの更新値(pptx.dml.color.RGBColor型)
# font_name font.nameの更新値
# alignment alignmentの更新値(省略時はPP_ALIGN.LEFT)
# 戻り値:
# なし
def make_text_frame(org_tf, text, font_size, font_color_rgb, font_name, alignment = PP_ALIGN.LEFT):
tf = org_tf
tf.text = text
tf.paragraphs[0].font.size = font_size
tf.paragraphs[0].font.color.rgb = font_color_rgb
tf.paragraphs[0].font.name = font_name
tf.paragraphs[0].alignment = alignment
# Excelからデータを読み込む
# 引数:
# workbook_path 読み込むExcelファイルのパス
# sheet_name 読み込むExcelファイルのシート名
# sheet_range_start 読込範囲の開始点
# sheet_range_end 読込範囲の終了点
# 戻り値:
# Excelから取得したデータリスト
def get_excel_data(workbook_path, sheet_name, sheet_range_start, sheet_range_end):
list_data = []
workbook =openpyxl.load_workbook(workbook_path, read_only=True, data_only=True)
sheet = workbook[sheet_name]
sheet_range = sheet[sheet_range_start : sheet_range_end]
for row in sheet_range:
for cell in row:
# 該当セルの値取得
cell_value = str(cell.value)
# 該当セルに値が存在する場合にリスト追加
if cell_value is not None:
list_data = np.append(list_data, cell_value)
workbook.close()
return list_data
# tableを更新する
# 引数:
# org_table 更新元となるtable
# data_list tableに設定していくデータ
# 戻り値:
# なし
def make_table(org_table, data_list, font_size, font_color_rgb, font_name, alignment = PP_ALIGN.LEFT, skip_rows_list = []):
table = org_table
list_num = 0
# 表内で空白のセルに順次Excelから読み込んだデータをセットしていく
for cell_no, cell in enumerate(table, start=1):
if cell_no in skip_rows_list: # スキップ
continue
if len(cell.text) == 0 and cell.is_spanned == False:
make_text_frame(
cell.text_frame
,str(data_list[list_num])
,font_size
,font_color_rgb
,font_name
,alignment)
list_num += 1
###########
# メイン部 #
###########
ARGV_SIZE = 2 # コマンドライン引数の数
# 引数の数が異なれば終了
if len(sys.argv) != ARGV_SIZE:
print('Error : Please specify ', str(ARGV_SIZE - 1) , ' arguments.') # プログラム名が引数の1つなので
sys.exit(1)
# 引数から値を取得
target_yyyymm = sys.argv[1]
# yyyy-mm形式として認識できない場合はエラーとする
start_date = target_yyyymm + '-01'
try:
start_date = datetime.strptime(start_date, '%Y-%m-%d')
except ValueError:
print('Error : For the argument, specify the \'yyyy-mm\' format.')
sys.exit(1)
end_date = start_date + relativedelta(months = 1) - timedelta(days = 1)
# 入力ファイル名
input_file_name = 'template/Template.pptx'
# 出力ファイル名
file_name_base = 'Monthly report '
output_file_name = 'output_file/' + file_name_base + str(target_yyyymm) + '.pptx'
# パワポテンプレート読込
prs = pptx.Presentation(input_file_name)
# 貼り付ける画像情報一覧
df_pic = get_pic_list()
# *************************** #
# スライドごとに必要な処理を行う #
# *************************** #
for page_no, sld in enumerate(prs.slides, start=1):
・
・
・
# 作成したパワポを保存
prs.save(output_file_name)
▼改善結果
- これまで手作業で行っていたパワポの作成を全自動化することができました
- 自動化により作成に要する時間を1時間→数分(作成後の確認作業含めて)に短縮することができました
- 自動化により作成ミス(転記漏れ、誤りなど)をすることがなくなりました
個人的には、品質向上に繋がった3点目が一番大きな成果かなと。
改善その3
仕上げとして、今回自動化に取り組んだ下記2、3とそれに付随して手作業で行っていたファイルのコピー部分を繋げて、ワンクリックで行えるようにしました。
- 1.データベースから必要な情報を抽出する
- 2.抽出した情報を元にExcelで加工・集計を行う
- 3.加工・集計した内容をパワポにまとめる
これはWindowsのバッチファイルを作ることで実現させましたが、今回の内容からは逸脱してしまうので詳細は省かせていただきます。
バッチファイルの流れとしては以下の通りです。
- 1.抽出した情報を含めた、加工・集計用データを集める
- 2.集めたデータのバックアップを作成する(再作成が必要になった時用)
- 3.抽出した情報を元にExcelで加工・集計を行う
- 4.加工・集計した内容をパワポにまとめる
- 5.次月使用するデータを集めておく
感想
この記事を書き終わって、成果物含めて振り返ると、もっと整理して簡略化できるところや、もっとスマートなライブラリの使い方を調べたい欲求にかられるんですが、趣味でやっているわけではなく、業務時間も限られていますのでこの辺が着地点かなと。
この作業を始めたころはPythonも使い始めたばかりで、Excelやパワポの作成をPythonで書けるなんて考えもしなかったんですが、定型作業であれば使わない手はないかなと感じました。
作成に要する時間短縮はもちろんですが、それよりも作成ミス(転記漏れ、誤りなど)を完全になくせることによる品質向上面の方が大きいかなと。