01スライドの題と箇条書きを段の深さつきで Markdown の骨組みにする確認済
各スライドの題(shapes.title)と、題以外のプレースホルダーの段落を paragraph.level の深さで字下げし、デッキごとの Markdown にする。題のプレースホルダーが無いか空のスライドは (no title) として一覧に出す。構成の見直しや、Copilot に渡す前の下ごしらえに使える。
- Presentation(path).slides を順に読む。
- slide.shapes.title で題のプレースホルダーを取る。スライドに題のプレースホルダーが無ければ None が返る。
- slide.placeholders のうち、placeholder_format.type が TITLE・CENTER_TITLE 以外で has_text_frame が True のものから段落を取る。
- 段落は runs の文字をつなぎ、paragraph.level の数だけ字下げして「- 」を付ける。
- 題の無いスライドの番号とレイアウト名(slide.slide_layout.name)を画面に出し、out_outline_<名前>.md を保存する。
"""Turn each deck into a Markdown outline (title + bullets by level) and flag untitled slides."""
from pathlib import Path
from pptx import Presentation
from pptx.enum.shapes import PP_PLACEHOLDER
DECKS = sorted(Path("samples").glob("*.pptx"))
TITLE_TYPES = {PP_PLACEHOLDER.TITLE, PP_PLACEHOLDER.CENTER_TITLE}
def slide_outline(slide):
title = slide.shapes.title # None when the slide has no title placeholder
text = title.text_frame.text.strip() if title is not None else ""
lines = [text or "(no title)"]
for shape in slide.placeholders:
if shape.placeholder_format.type in TITLE_TYPES or not shape.has_text_frame:
continue
for p in shape.text_frame.paragraphs:
body = "".join(r.text for r in p.runs).strip()
if body:
lines.append(" " * p.level + "- " + body)
return lines
def main():
for path in DECKS:
prs = Presentation(path)
md, untitled = ["# " + path.stem], []
for i, slide in enumerate(prs.slides, 1):
lines = slide_outline(slide)
if lines[0] == "(no title)":
untitled.append("%d (%s)" % (i, slide.slide_layout.name))
md += ["", "## %d. %s" % (i, lines[0])] + lines[1:]
out = Path("out_outline_%s.md" % path.stem)
out.write_text("\n".join(md) + "\n", encoding="utf-8")
print("%s: slides %d, untitled: %s -> %s" % (path.name, len(prs.slides), ", ".join(untitled) or "none", out.name))
if __name__ == "__main__":
main()実行結果(2026-09-12)
deck_a.pptx: slides 3, untitled: none -> out_outline_deck_a.md
deck_b.pptx: slides 2, untitled: none -> out_outline_deck_b.md
docs_deck_c.pptx: slides 4, untitled: 3 (Blank) -> out_outline_docs_deck_c.mdpython-pptx で、フォルダ内の .pptx ごとに、スライドの題(shapes.title)と題以外のプレースホルダーの段落を paragraph.level で字下げした Markdown を書き出すコードを書いて。題が無いか空のスライドは、番号とレイアウト名を一覧で表示すること。
プレースホルダーの文字だけを拾うので、手で置いたテキストボックスや図形、表、グループの中の文字は骨組みに入らない(それらは一括点検のノウハウで拾う)。題をテキストボックスで作ったスライドは (no title) になる。構成を Copilot に渡すときも、元のスライドと突き合わせて抜けを確かめる。
出典の該当箇所
The title placeholder shape on the slide. None if the slide has no title placeholder.
0 represents a top-level paragraph and is the default value.
The title placeholder will always have idx 0 if present and any other placeholders will follow in sequence, top to bottom and left to right.
02スライドの表を CSV に書き出し、結合セルと空のセルに印を付ける確認済
has_table が True の図形から shape.table を取り、行と列の座標ごとに 1 行の CSV にする。結合セルの左上(is_merge_origin)には縦横の大きさを、その下に隠れるセル(is_spanned)には spanned と書き、空のセルに印を付ける。料金表などの記入漏れの点検に使える。
- slide.shapes を回し、shape.has_table が True の図形から shape.table を取る。
- table.rows と row.cells で、セルを格子の座標の順に読む。
- cell.is_merge_origin なら span_height と span_width を、cell.is_spanned なら spanned を記録する。
- 結合で隠れていないのに文字が無いセルを empty とする。
- out_tables.csv の empty の行を、資料の作成者に確かめてもらう。
"""Export every table in each deck to CSV, marking merged and empty cells."""
import csv
from pathlib import Path
from pptx import Presentation
DECKS = sorted(Path("samples").glob("*.pptx"))
def iter_tables(prs):
for s, slide in enumerate(prs.slides, 1):
for shape in slide.shapes:
if shape.has_table:
yield s, shape.name, shape.table
def cell_rows(table):
for r, row in enumerate(table.rows):
for c, cell in enumerate(row.cells):
if cell.is_spanned:
state = "spanned" # hidden under a merged cell
elif cell.is_merge_origin:
state = "merged %dx%d" % (cell.span_height, cell.span_width)
else:
state = ""
empty = "empty" if not cell.is_spanned and not cell.text.strip() else ""
yield r, c, state, empty, cell.text.strip()
def main():
rows = []
for path in DECKS:
for s, name, table in iter_tables(Presentation(path)):
rows += [(path.name, s, name) + rec for rec in cell_rows(table)]
with open("out_tables.csv", "w", encoding="utf-8-sig", newline="") as f:
w = csv.writer(f)
w.writerow(["deck", "slide", "shape", "row", "col", "state", "flag", "text"])
w.writerows(rows)
tables = {(r[0], r[1], r[2]) for r in rows}
print("tables:", len(tables), "cells:", len(rows), "empty cells:", sum(r[6] == "empty" for r in rows))
for r in rows:
if r[5] or r[6]:
print(" | ".join(str(x) for x in r))
if __name__ == "__main__":
main()実行結果(2026-09-12)
tables: 2 cells: 13 empty cells: 1
docs_deck_c.pptx | 4 | Table 2 | 0 | 0 | merged 1x3 | | プラン別の月額(税別、架空)
docs_deck_c.pptx | 4 | Table 2 | 0 | 1 | spanned | |
docs_deck_c.pptx | 4 | Table 2 | 0 | 2 | spanned | |
docs_deck_c.pptx | 4 | Table 2 | 2 | 2 | | empty | python-pptx で、フォルダ内の .pptx のすべての表を、デッキ・スライド番号・図形名・行・列・結合の状態・空欄の印・文字の列を持つ CSV に書き出すコードを書いて。is_merge_origin と is_spanned を使って結合セルを区別すること。
python-pptx のセルは格子の座標で読むので、画面上の位置と一致しないことがある、と公式に書かれている。結合で隠れたセルに文字が残っていても画面には出ない。この例は slide.shapes の最上位だけを見るので、グループの中の表は拾わない(グループのノウハウの関数を使う)。表の値を集計に使う前に、見出しの行と単位を人が確かめる。
出典の該当箇所
Access to a table cell in python-pptx is always via that cell’s coordinates in the cell grid, which may not conform to its visual location (or lack thereof) in the table.
In python-pptx a merge-origin cell can be identified with the _Cell.is_merge_origin property.
True if this shape is a graphic frame containing a table object.
03発表者ノートを has_notes_slide で確かめてから読み、未記入と要注意語を一覧にする確認済
slide.notes_slide は、ノートが無いスライドでは呼んだだけで新しいノートを作る。先に has_notes_slide を見て、ノートがあるスライドだけ notes_text_frame の文字を読む。ノートの無いスライド、空のノート、表記ルールに当たる語を CSV にまとめる。
- スライドごとに slide.has_notes_slide を見る。False ならノート無しとして記録し、notes_slide には触れない。
- True なら slide.notes_slide.notes_text_frame の text を読む。notes_text_frame が None のときは空として扱う。
- ノートの文字を style_terms.csv の語と照らし、当たった語を記録する。
- デッキ・スライド番号・題・状態(none / empty / text)・ノート・語を out_notes.csv に書く。
"""List speaker notes per slide without creating empty notes slides, and flag style-list terms."""
import csv
from pathlib import Path
from pptx import Presentation
DECKS = sorted(Path("samples").glob("*.pptx"))
TERMS = Path("samples/style_terms.csv")
def load_terms(path):
with open(path, encoding="utf-8-sig", newline="") as f:
return [row["term"] for row in csv.DictReader(f)]
def notes_text(slide):
"""None when the slide has no notes slide; never creates one."""
if not slide.has_notes_slide: # slide.notes_slide would create a notes slide
return None
frame = slide.notes_slide.notes_text_frame
return frame.text.strip() if frame is not None else ""
def main():
terms = load_terms(TERMS)
rows = []
for path in DECKS:
missing = []
for i, slide in enumerate(Presentation(path).slides, 1):
title = slide.shapes.title.text_frame.text.strip() if slide.shapes.title is not None else ""
notes = notes_text(slide)
state = "none" if notes is None else ("empty" if not notes else "text")
if state != "text":
missing.append(i)
found = [t for t in terms if notes and t in notes]
rows.append((path.name, i, title, state, notes or "", " ".join(found)))
print("%s: slides without notes: %s" % (path.name, missing or "none"))
with open("out_notes.csv", "w", encoding="utf-8-sig", newline="") as f:
w = csv.writer(f)
w.writerow(["deck", "slide", "title", "state", "notes", "terms"])
w.writerows(rows)
for r in rows:
if r[5]:
print("term in notes:", r[0], "slide", r[1], "|", r[5], "|", r[4])
if __name__ == "__main__":
main()実行結果(2026-09-12)
deck_a.pptx: slides without notes: [3]
deck_b.pptx: slides without notes: [1]
docs_deck_c.pptx: slides without notes: [2, 3]
term in notes: deck_a.pptx slide 2 | 必ず | 必ず短縮できるとは言わないこと。python-pptx で、.pptx のスライドごとに has_notes_slide を先に確かめてから発表者ノートを読み、ノート無し・空・記入ありの状態と、style_terms.csv の語に当たったかを CSV に出すコードを書いて。notes_slide を無条件に呼ばないこと。ファイルは保存しないこと。
点検のコードで notes_slide を先に呼ぶと、ノートの無いスライドにノートが作られ、保存すればファイルが変わる。ノートは発表者だけが見る前提で書かれることが多いので、配布の前に中身を確かめる。語の包含だけで判定するため、「必ず…とは言わない」のような注意書きも当たる。
出典の該当箇所
A notes slide is created by notes_slide when one doesn’t exist; use this property to test for a notes slide without the possible side effect of creating one.
Each slide can have zero or one notes slide.
None if there is no notes placeholder.
04グループ図形の中を再帰でたどり、文字の拾い漏れを防ぐ確認済
グループ図形そのものは文字枠を持たず、has_text_frame は常に False になる。slide.shapes の最上位だけを見る点検は、グループの中のテキストボックスを読み落とす。shape_type が GROUP の図形は shape.shapes を再帰でたどり、中の図形をグループ名の経路つきで一覧にする。
- slide.shapes を回し、shape.shape_type が MSO_SHAPE_TYPE.GROUP なら shape.shapes を同じ関数に渡す。
- グループでない図形は、たどってきたグループ名の経路と一緒に返す。
- 最上位で文字のある図形の数と、グループの中で文字のある図形の数を並べて出す。
- グループの中にあった文字を、スライド番号・経路・図形名・shape_id つきで表示する。
"""Find text hidden inside group shapes by walking the shape tree recursively."""
from pathlib import Path
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
DECKS = sorted(Path("samples").glob("*.pptx"))
def iter_shapes(shapes, path=()):
"""Yield (group path, shape) for every shape, descending into groups."""
for shape in shapes:
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
yield from iter_shapes(shape.shapes, path + (shape.name,))
else:
yield path, shape
def has_text(shape):
return shape.has_text_frame and shape.text_frame.text.strip()
def main():
for deck in DECKS:
top = nested = 0
for i, slide in enumerate(Presentation(deck).slides, 1):
top += sum(1 for s in slide.shapes if has_text(s))
for path, shape in iter_shapes(slide.shapes):
if path and has_text(shape):
nested += 1
print("slide %d | %s > %s (id %d) | %s" % (
i, " > ".join(path), shape.name, shape.shape_id, shape.text_frame.text.strip()))
print("%s: text shapes at top level %d, in groups: %d" % (deck.name, top, nested))
if __name__ == "__main__":
main()実行結果(2026-09-12)
deck_a.pptx: text shapes at top level 6, in groups: 0
slide 2 | Group 5 > TextBox 2 (id 3) | 申込
slide 2 | Group 5 > TextBox 3 (id 4) | 設定
slide 2 | Group 5 > TextBox 4 (id 5) | 最短 3 日で運用開始出来る
docs_deck_c.pptx: text shapes at top level 4, in groups: 3python-pptx で、スライドの図形をグループ(MSO_SHAPE_TYPE.GROUP)の中まで再帰でたどるジェネレーターを書いて。グループの経路と図形を返し、グループの中にだけある文字をスライド番号・図形名・shape_id つきで表示すること。
グループの中にグループがあっても、この関数で奥までたどれる。表や画像もグループに入れられるので、表や画像を探すコードにも同じ関数を使う。図形を特定して記録するときは、スライド内で一意の shape_id を使う。グラフ(chart)の中の文字はこの方法では読まない。
出典の該当箇所
A group shape does not have a textframe and cannot itself contain text. This does not impact the ability of shapes contained by the group to each have their own text.
Unconditionally MSO_SHAPE_TYPE.GROUP in this case
The id of a shape is unique among all shapes on a slide.
05スライドの画像を取り出し、SHA1 で重複を除いて一覧にする確認済
Picture 図形の image から、画像のバイト列(blob)・拡張子(ext)・MIME 型・画素の大きさ・dpi・SHA1 を読める。同じ画像が何か所で使われているかを SHA1 でまとめ、重複の無い画像だけを out_images に保存し、使われた場所を CSV にする。
- slide.shapes を回し、グループなら中へ再帰して、Picture の図形を集める。
- picture.image の sha1 をキーにし、初めて見た画像だけ blob を out_images/<デッキ>_s<番号>_<sha1 の先頭>.<ext> に書く。
- content_type・size(画素)・dpi も記録する。
- 同じ sha1 の 2 枚目以降は duplicate として CSV に残す。
- out_pictures.csv で、使い回された画像や解像度の低い画像を確かめる。
"""Extract pictures from decks, skip duplicates by SHA1, and list every placement in a CSV."""
import csv
from pathlib import Path
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.shapes.picture import Picture
DECKS = sorted(Path("samples").glob("*.pptx"))
OUT_DIR = Path("out_images")
def iter_pictures(shapes):
for shape in shapes:
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
yield from iter_pictures(shape.shapes)
elif isinstance(shape, Picture):
yield shape
def main():
OUT_DIR.mkdir(exist_ok=True)
rows, saved = [], {}
for deck in DECKS:
for i, slide in enumerate(Presentation(deck).slides, 1):
for pic in iter_pictures(slide.shapes):
img = pic.image
dup = "duplicate" if img.sha1 in saved else ""
if not dup:
saved[img.sha1] = "%s_s%d_%s.%s" % (deck.stem, i, img.sha1[:8], img.ext)
(OUT_DIR / saved[img.sha1]).write_bytes(img.blob)
w, h = img.size
rows.append((deck.name, i, pic.name, img.content_type, "%dx%d" % (w, h),
"%dx%d" % img.dpi, saved[img.sha1], dup))
with open("out_pictures.csv", "w", encoding="utf-8-sig", newline="") as f:
wr = csv.writer(f)
wr.writerow(["deck", "slide", "shape", "content_type", "pixels", "dpi", "file", "flag"])
wr.writerows(rows)
print("pictures:", len(rows), "unique files:", len(saved))
for r in rows:
print(" | ".join(str(x) for x in r))
if __name__ == "__main__":
main()実行結果(2026-09-12)
pictures: 3 unique files: 2
docs_deck_c.pptx | 3 | Picture 1 | image/png | 64x40 | 72x72 | docs_deck_c_s3_dc9edf4b.png |
docs_deck_c.pptx | 3 | Picture 2 | image/png | 64x40 | 72x72 | docs_deck_c_s3_dc9edf4b.png | duplicate
docs_deck_c.pptx | 3 | Picture 3 | image/png | 64x40 | 72x72 | docs_deck_c_s3_ece03d63.png | python-pptx で、フォルダ内の .pptx の画像(Picture 図形。グループの中も含む)を取り出し、image.sha1 で重複を除いて out_images フォルダに保存し、デッキ・スライド・図形名・MIME 型・画素の大きさ・dpi・保存名・重複の印を CSV に出すコードを書いて。
dpi が画像のファイルに書かれていないときは (72, 72) が返る、と公式に書かれている。size は画像の画素数で、スライド上の表示の大きさは shape.width と shape.height(EMU)で別に読む。図形の塗りつぶしに使った画像は Picture ではないので拾わない。取り出した画像の利用範囲(社外の素材など)は、元の資料の扱いに従う。
出典の該当箇所
The Image object provides access to detailed properties of the image itself, including the bytes of the image file itself.
A default value of (72, 72) is used if the dpi is not specified in the image file.
Note that an auto shape can have a picture fill
06フォルダ内の全デッキを表記ルールで一括点検し、CSV の報告にする確認済
フォルダの .pptx をすべて開き、テキストボックス・プレースホルダー・グループの中・表のセル・発表者ノートの文字を集め、表記ルールの語に当たった箇所を 1 つの CSV にまとめる。開けないファイルがあっても止めず、その旨を行として残す。
- FOLDER.glob("*.pptx") でファイルを集め、style_terms.csv を読む。
- スライドの図形を順に取り出し、グループなら中の図形を待ち行列の先頭に戻して続けて読む。
- 文字枠(has_text_frame)は text_frame.text を、表(has_table)は結合で隠れていないセルの文字を読む。
- has_notes_slide が True のときだけ発表者ノートの文字を読む。
- デッキ・スライド番号・場所・語・対応・文を out_deck_check.csv に書き、デッキごとの件数を表示する。
"""Check every .pptx in a folder against a style list: text, groups, tables and notes."""
import csv
from collections import Counter
from pathlib import Path
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
FOLDER = Path("samples")
TERMS = FOLDER / "style_terms.csv"
def load_terms(path):
with open(path, encoding="utf-8-sig", newline="") as f:
return list(csv.DictReader(f))
def texts(slide):
"""Yield (where, text) for text frames, table cells (also inside groups) and notes."""
queue = [(shape, shape.name) for shape in slide.shapes]
while queue:
shape, where = queue.pop(0)
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
queue[:0] = [(s, where + ">" + s.name) for s in shape.shapes]
elif shape.has_text_frame:
yield where, shape.text_frame.text
elif shape.has_table:
for r, row in enumerate(shape.table.rows):
for c, cell in enumerate(row.cells):
if not cell.is_spanned:
yield "%s[r%dc%d]" % (where, r, c), cell.text
if slide.has_notes_slide and slide.notes_slide.notes_text_frame is not None:
yield "notes", slide.notes_slide.notes_text_frame.text
def check_deck(path, terms):
try:
prs = Presentation(path)
except Exception as e: # keep going when one file cannot be opened
return [(path.name, "", "", "", "", "open failed: %s" % type(e).__name__)]
rows = []
for i, slide in enumerate(prs.slides, 1):
for where, text in texts(slide):
for t in terms:
if t["term"] in text:
rows.append((path.name, i, where, t["term"], t["action"], text.strip().replace("\n", " / ")))
return rows
def main():
terms = load_terms(TERMS)
decks = sorted(FOLDER.glob("*.pptx"))
rows = [r for path in decks for r in check_deck(path, terms)]
with open("out_deck_check.csv", "w", encoding="utf-8-sig", newline="") as f:
w = csv.writer(f)
w.writerow(["deck", "slide", "where", "term", "action", "text"])
w.writerows(rows)
print("decks:", len(decks), "hits:", len(rows))
for deck, n in Counter(r[0] for r in rows).most_common():
print(" %s %d" % (deck, n))
for r in rows[:8]:
print(" | ".join(str(x) for x in r[:5]))
if __name__ == "__main__":
main()実行結果(2026-09-12)
decks: 3 hits: 5
deck_a.pptx 4
docs_deck_c.pptx 1
deck_a.pptx | 1 | Content Placeholder 2 | 業界初 | 根拠を確認する
deck_a.pptx | 2 | notes | 必ず | 言い換える
deck_a.pptx | 3 | Content Placeholder 2 | 唯一 | 根拠を確認する
deck_a.pptx | 3 | Content Placeholder 2 | 出来る | 「できる」に統一
docs_deck_c.pptx | 2 | Group 5>TextBox 4 | 出来る | 「できる」に統一python-pptx で、フォルダ内のすべての .pptx について、テキストボックス・プレースホルダー・グループの中の図形・表のセル・発表者ノート(has_notes_slide を先に確認)の文字を style_terms.csv の語と照らし、当たった箇所を 1 つの CSV にまとめるコードを書いて。開けないファイルはエラー名を行に残して続けること。
語の包含だけで判定するので、否定の文脈や引用でも当たる。直すかどうかは人が決める。グラフ(chart)の中の文字、画像の中の文字、スライドマスターとレイアウトの文字は読まない。ファイルを開いて読むだけで保存しないので、元のデッキは変わらない。社外秘の資料を点検したときは、出力の CSV も同じ扱いにする。
出典の該当箇所
Auto shapes and table cells can contain text. Other shapes can’t. Text is always manipulated the same way, regardless of its container.
use this property to test for a notes slide without the possible side effect of creating one.
A spanned cell can be identified with its _Cell.is_spanned property.
07複数のデッキの題と件数から、新しい要約デッキを組み立てる確認済
各デッキのスライドの題・表の数・ノートのあるスライドの数を読み取り、Presentation() で作った新しいデッキに、表紙・デッキごとの題の一覧・合計の表を書く。レイアウトは番号でなく get_by_name() で名前から選ぶので、テンプレートを替えても取り違えにくい。
- 元のデッキを開き、slide.shapes.title の文字、has_table の図形の数、ノートのあるスライドの数を数える。
- Presentation() で新しいデッキを作り、slide_layouts.get_by_name("Title Slide") などでレイアウトを選ぶ。見つからなければ止める。
- デッキごとに Title and Content のスライドを足し、本文のプレースホルダーに題を level 1 の段落で並べる。
- 最後に Title Only のスライドに shapes.add_table で合計の表を置く。
- out_summary.pptx として保存し、開き直してスライドの数を確かめる。
"""Build a new summary deck: one slide per source deck listing its slide titles, plus a totals table."""
from pathlib import Path
from pptx import Presentation
from pptx.util import Inches, Pt
DECKS = sorted(Path("samples").glob("deck_*.pptx"))
OUT = Path("out_summary.pptx")
def describe(path):
titles, tables, notes = [], 0, 0
for slide in Presentation(path).slides:
t = slide.shapes.title
titles.append(t.text_frame.text.strip() if t is not None else "(no title)")
tables += sum(1 for s in slide.shapes if s.has_table)
if slide.has_notes_slide and slide.notes_slide.notes_text_frame.text.strip():
notes += 1
return {"name": path.name, "titles": titles, "tables": tables, "notes": notes}
def layout(prs, name):
found = prs.slide_layouts.get_by_name(name)
if found is None:
raise SystemExit("layout not found in template: %s" % name)
return found
def build(infos, out):
prs = Presentation() # built-in default template; open your own .pptx to reuse its layouts
cover = prs.slides.add_slide(layout(prs, "Title Slide"))
cover.shapes.title.text = "Deck summary"
cover.placeholders[1].text = "%d decks" % len(infos)
for info in infos:
slide = prs.slides.add_slide(layout(prs, "Title and Content"))
slide.shapes.title.text = info["name"]
tf = slide.placeholders[1].text_frame
tf.text = "%d slides" % len(info["titles"])
for i, title in enumerate(info["titles"], 1):
p = tf.add_paragraph()
p.text = "%d. %s" % (i, title)
p.level = 1
last = prs.slides.add_slide(layout(prs, "Title Only"))
last.shapes.title.text = "Totals"
n = len(infos) + 1
table = last.shapes.add_table(n, 4, Inches(0.5), Inches(1.8), Inches(9), Inches(0.4) * n).table
for c, head in enumerate(["deck", "slides", "tables", "slides with notes"]):
table.cell(0, c).text = head
for r, info in enumerate(infos, 1):
for c, value in enumerate([info["name"], len(info["titles"]), info["tables"], info["notes"]]):
table.cell(r, c).text = str(value)
table.cell(r, c).text_frame.paragraphs[0].runs[0].font.size = Pt(14)
prs.save(out)
def main():
if any(OUT.resolve() == p.resolve() for p in DECKS):
raise SystemExit("output must not overwrite an input")
infos = [describe(p) for p in DECKS]
build(infos, OUT)
print("saved", OUT, "slides:", len(Presentation(OUT).slides))
for info in infos:
print(" %s: %d slides, %d tables, %d with notes" % (
info["name"], len(info["titles"]), info["tables"], info["notes"]))
if __name__ == "__main__":
main()実行結果(2026-09-12)
saved out_summary.pptx slides: 4
deck_a.pptx: 3 slides, 1 tables, 2 with notes
deck_b.pptx: 2 slides, 0 tables, 1 with notespython-pptx で、フォルダ内の deck_*.pptx からスライドの題・表の数・ノートのあるスライドの数を集め、新しいプレゼンテーションに表紙・デッキごとの題の一覧・合計の表を作るコードを書いて。レイアウトは slide_layouts.get_by_name() で名前から選び、見つからなければ止めること。元のファイルは保存し直さないこと。
レイアウトの名前はテンプレートごとに違う。社内のテンプレートを使うときは、そのファイルを Presentation(path) で開き、prs.slide_layouts の name を先に表示して確かめる。このコードは題の文字を写すだけで、内容の要約文は作らない。要約文が要るときは Copilot の要約を使い、結果を人が確かめる。
出典の該当箇所
Return SlideLayout object having name , or default if not found.
If pptx is missing or None, the built-in default presentation “template” is loaded.
Not all shapes can contain text, but those that do always have at least one paragraph, even if that paragraph is empty and no text is visible within the shape.