定期的な報告資料作成の便利用に、LaTeX で予め定義した環境の中身を空にする / スライドさせる Python スクリプトを記します (標準ライブラリのみで動きます)。
背景・方針
定期的に報告資料を作成することはあると思います。が、
- 前回資料をコピーして編集する運用にすると、編集漏れが発生しがちです。また、どこが前回からの差分なのかもわかりにくくなりがちです。
- テンプレートに記入する運用にすると、テンプレートが陳腐化してもメンテナンスがなおざりになりがちです。また、引き続き掲載すべき情報はコピーしなければなりません。
そこで、LaTeX で「この環境の中身は毎回空にする (または他の環境の中身をスライドさせる)」というフロー情報用環境を定義し、フロー情報とストック情報を区別して資料を作成し、次回資料はスクリプトで作成するようにすることで、上記の課題を解決します。
具体例
下図の左の資料 (Beamer スライド) から、右の次回資料を自動生成します。
なお、「年間日程表」は、引き続き掲載すべきストック情報の例です (遠い未来の予定とか、これまでの成果のまとめとかが相当すると思います)。
以下を守って運用する必要があります。ただし、2 回目以降の資料作成ではスクリプトの自動記入が機能するので、毎回記入すべきなのは太字の箇所だけです。
-
\newcommand{\zenkai}{2026/04/30}の定義箇所に、前回報告日付を記入します。- 2 回目以降は前回資料の「今回報告日付」がスライドされてきます。
-
\newcommand{\konkai}{2026/05/12}の定義箇所に、今回報告日付を定義します。- 2 回目以降は前回資料の「次回打合せ:」の箇所のパースが自動で記入されます。
-
myEnvZenkaiItemize環境 (itemize環境のラップ) で「前回のトピック」を書きます。- 2 回目以降は前回資料の「今回のトピック」の中身がスライドされてきます。
-
myEnvKonkaiItemize環境 (itemize環境のラップ) で「今回のトピック」を書きます。 なお、この環境は末尾に「次回打合せ:」が自動で挿入されます。 myEnvHoukoku環境で報告内容を書きます。- 引き続き掲載すべきストック情報は、環境の外に書きます。
myEnvKonkaiItemize環境の定義箇所の「次回打合せ:」に次回報告日付を記入します。
上図の左側の資料の LaTeX ファイルは以下です (xelatex slide.tex でビルドできます)。
\documentclass[t,unicode,10pt]{beamer}
\usepackage{zxjatype}
\setsansfont[BoldFont={BIZ UDゴシック Bold}]{BIZ UDゴシック}
\setCJKsansfont[BoldFont={BIZ UDゴシック Bold}]{BIZ UDゴシック}
\usepackage{relsize}
\usepackage{booktabs}
\newcommand{\structfg}{\usebeamercolor[fg]{structure}}
\newcommand{\myheader}[1]{\textbf{\relsize{+0.5}\structfg #1}\par\smallskip}
% =============== 変数・環境定義 ===============
\newcommand{\konkai}{2026/05/12}
\newcommand{\zenkai}{2026/04/30}
\newenvironment{myEnvZenkaiItemize}{%
\myheader{前回報告 (\zenkai) のトピック}
\begin{itemize}
}{\end{itemize}}
\newenvironment{myEnvKonkaiItemize}{%
\myheader{今回報告 (\konkai) のトピック}
\begin{itemize}
}{%
{\structfg \item 次回打合せ: 2026/05/20 (水) 15時}
\end{itemize}
}
\newenvironment{myEnvHoukoku}{%
\myheader{報告内容}
}{}
% ########################################
\begin{document}
\begin{frame}[allowframebreaks]
\begin{myEnvZenkaiItemize}
\item ほげほげについて
\end{myEnvZenkaiItemize}
\bigskip
\begin{myEnvKonkaiItemize}
\item ふがふがについて
\end{myEnvKonkaiItemize}
\bigskip
\begin{myEnvHoukoku}
ふがふがふがふがふがふが\par
ふがふがふがふがふがふが\par
\end{myEnvHoukoku}
\bigskip
\myheader{年間日程表}
\begin{tabular}{ll}
\toprule 2026/07/20 & 海の日 \\
\midrule 2026/08/11 & 山の日 \\
\bottomrule
\end{tabular}
\end{frame}
\end{document}
上記の LaTeX ファイルを元に新しい LaTeX ファイルを生成するスクリプトは以下です。
python new_mtg_deck.py slide.tex slide_new.tex のように実行します。
import re
from pathlib import Path
import argparse
def parse_and_reset_next_meeting(text):
pattern = r'(\\item\s+次回打合せ:\s*)(\d{4}/\d{1,2}/\d{1,2})(\s*)\([^)]*\)(\s*)\d+時'
m = re.search(pattern, text)
if m is None:
raise ValueError('次回打合せが見つかりません')
next_date = m.group(2)
text = text[:m.start(2)] + 'Y/m/d' + m.group(3) + '(DOW)' + m.group(4) + 'H時' + text[m.end():]
return next_date, text
def replace_command(text, name, value):
pat = rf'^(\\newcommand\{{\\{name}\}}\{{)([^}}]*)(\}})'
return re.sub(pat, rf'\g<1>{value}\3', text, count=1, flags=re.MULTILINE)
def find_env_content_range(text, env_name, i_char_from=0):
pattern_begin = rf'^\\begin\{{{re.escape(env_name)}\}}'
pattern_end = rf'^\\end\{{{re.escape(env_name)}\}}'
m_begin = re.search(pattern_begin, text[i_char_from:], re.MULTILINE)
if m_begin is None:
return None, None
i_char_start = i_char_from + m_begin.end()
m_end = re.search(pattern_end, text[i_char_start:], re.MULTILINE)
if m_end is None:
raise ValueError(f'\\end{{{env_name}}} が見つかりません')
i_char_end = i_char_start + m_end.start()
return i_char_start, i_char_end
def get_env_content(text, env_name):
i_char_start, i_char_end = find_env_content_range(text, env_name)
if i_char_start is None:
return None
return text[i_char_start:i_char_end]
def replace_env_content(text, env_name, content):
ranges = []
i_char_from = 0
while True:
i_char_start, i_char_end = find_env_content_range(text, env_name, i_char_from)
if i_char_start is None:
break
ranges.append((i_char_start, i_char_end))
i_char_from = i_char_end
for i_char_start, i_char_end in reversed(ranges):
text = text[:i_char_start] + '\n' + content + '\n' + text[i_char_end:]
return text
def update_tex(current_slide, new_slide):
text = Path(current_slide).read_text(encoding='utf-8')
next_date, text = parse_and_reset_next_meeting(text)
m = re.search(r'^\\newcommand\{\\konkai\}\{([^}]*)\}', text, re.MULTILINE)
if not m:
raise ValueError(r'\konkai が見つかりません')
old_konkai = m.group(1)
text = replace_command(text, 'zenkai', old_konkai)
text = replace_command(text, 'konkai', next_date)
tbu = r'\alert{To be updated}'
konkai_content = get_env_content(text, 'myEnvKonkaiItemize').strip()
if not konkai_content.startswith(r'\item '):
konkai_content = r'\item ' + tbu
text = replace_env_content(text, 'myEnvZenkaiItemize', konkai_content)
text = replace_env_content(text, 'myEnvKonkaiItemize', r'\item ' + tbu)
text = replace_env_content(text, 'myEnvHoukoku', tbu)
Path(new_slide).write_text(text, encoding='utf-8', newline='\n')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('current_slide')
parser.add_argument('new_slide')
args = parser.parse_args()
update_tex(args.current_slide, args.new_slide)
補足
- 上記の
slide.texではフレームにallowframebreaksを指定しているので、どんどん内容を増やしても自動的にフレームが区切られます。 - 上記の Python スクリプトは LaTeX ファイル内に
myEnvZenkaiItemizemyEnvKonkaiItemizemyEnvHoukoku環境が複数あってもすべて中身を刷新します (myEnvZenkaiItemizemyEnvKonkaiItemize環境が複数あることはあまり想定しないが)。- ただし、同名環境の入れ子には対応していません。
- また、対象環境の
\begin{...}と\end{...}はどちらも行頭にあることを想定しています (意図せぬマッチを防ぐためであり、修正したい場合はパターンから行頭記号を除去ください)。
