PowerPointテキスト置換ツール
【概要】python-pptxライブラリを使用したPowerPointファイルのテキスト置換ツールを体験する。本ツールは、Office Open XML形式のファイル内部構造を直接操作し、フォント書式を保持しながら一括置換を実現する。プレゼンテーション資料の更新作業を効率化できるツールである。
本ツールは、GitHubで公開されているpython-pptx-text-replacer(https://github.com/fschaeck/python-pptx-text-replacer、GPL-3.0ライセンス)を改変したものである。
目次
概要
主要技術:Office Open XML操作技術
技術の規格:ECMA-376(Ecma International標準化規格): Office Open XML File Formats, 1st edition (December 2006)
技術的特徴:python-pptxライブラリによるOffice Open XML操作により、PowerPointファイルの内部XMLを解析・編集する。テキストフレーム、テーブルセル内の文字列を特定し、フォント属性を維持して置換処理を実行する。
体験価値:Office Open XMLファイル形式の理解、Pythonライブラリによるファイル操作、エラーハンドリング設計を習得できる。
事前準備
Python, Windsurfをインストールしていない場合の手順(インストール済みの場合は実行不要)。
- 管理者権限でコマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)。
- 以下のコマンドをそれぞれ実行する(winget コマンドは1つずつ実行)。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Windsurf をシステム領域にインストール
winget install --scope machine --id Codeium.Windsurf -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul
REM Windsurf のパス設定
set "WINDSURF_PATH=C:\Program Files\Windsurf"
if exist "%WINDSURF_PATH%" (
echo "%PATH%" | find /i "%WINDSURF_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%WINDSURF_PATH%" /M >nul
)
必要なライブラリのインストールを行う。コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)し、以下を実行する。
pip install python-pptx
プログラムコード
ソースコード
以下のコードをpptx_text_replacer.py
として保存する。
# PowerPointテキスト置換ツール
# Office Open XMLファイル(.pptx/.pptm)のテキスト一括置換
# 機能概要: .pptx/.pptmファイルのテキストを指定文字列で一括置換
# 論文: "Office Open XML File Formats" (Microsoft Corporation, 2006)
# GitHub: https://github.com/scanny/python-pptx
# 特徴: python-pptxライブラリによる.pptxファイル操作、フォーマット保持対応
# テーブル・テキストフレーム処理、自動バックアップ機能
# 前準備: pip install python-pptx
#
# Based on: python-pptx-text-replacer by Frank Schäckermann
# Original: https://github.com/fschaeck/python-pptx-text-replacer
# Copyright (c) 2022 Frank Schäckermann
# Licensed under GPL-3.0 License
# This derivative work is also licensed under GPL-3.0
import argparse
import os
import sys
from pathlib import Path
import shutil
from pptx import Presentation
from pptx.exc import PackageNotFoundError
# 定数定義
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
SUPPORTED_EXTENSIONS = ['.pptx', '.pptm']
MAX_TEXT_LENGTH = 10000
print("=== PowerPointテキスト置換ツール ===")
print("概要: PowerPointファイル内のテキストを一括置換します")
print("操作方法: コマンドライン引数で入力ファイル、置換前後のテキストを指定")
print("注意事項: 元ファイルのバックアップが自動作成されます\n")
# コマンドライン引数解析
parser = argparse.ArgumentParser(
description="PowerPointファイルのテキスト置換ツール",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用例:
%(prog)s presentation.pptx "古いテキスト" "新しいテキスト"
%(prog)s presentation.pptx "古いテキスト" "新しいテキスト" --slide 3
%(prog)s presentation.pptx "古いテキスト" "新しいテキスト" --output new.pptx
"""
)
parser.add_argument("pptx_file", help="PowerPointファイルのパス")
parser.add_argument("old_text", help="置換前の文字列")
parser.add_argument("new_text", help="置換後の文字列")
parser.add_argument("-s", "--slide", type=int, help="特定のスライド番号(1から開始)。指定しない場合は全スライドが対象")
parser.add_argument("-o", "--output", help="出力ファイルパス。指定しない場合は元ファイルを上書き")
args = parser.parse_args()
# 変数初期化
pptx_file = args.pptx_file
old_text = args.old_text
new_text = args.new_text
target_slide = args.slide
output_file = args.output
total_replacements = 0
slides_processed = 0
shapes_processed = 0
# ファイルパス検証
if not pptx_file or not pptx_file.strip():
print("エラー: ファイルパスが空です")
sys.exit(10)
file_path = Path(pptx_file.strip())
if not file_path.exists():
print(f"エラー: ファイルが存在しません: {file_path}")
sys.exit(10)
if not file_path.is_file():
print(f"エラー: パスがファイルを指していません: {file_path}")
sys.exit(10)
if file_path.suffix.lower() not in SUPPORTED_EXTENSIONS:
print(f"エラー: サポートされていないファイル形式: {file_path.suffix}")
print(f"対応形式: {', '.join(SUPPORTED_EXTENSIONS)}")
sys.exit(10)
if file_path.stat().st_size > MAX_FILE_SIZE:
file_size_mb = file_path.stat().st_size / (1024*1024)
max_size_mb = MAX_FILE_SIZE / (1024*1024)
print(f"エラー: ファイルサイズが上限を超えています: {file_size_mb:.1f}MB > {max_size_mb}MB")
sys.exit(10)
if not os.access(file_path, os.R_OK):
print(f"エラー: ファイルの読み取り権限がありません: {file_path}")
sys.exit(10)
# テキストパラメータ検証
if not isinstance(old_text, str):
print("エラー: 置換前テキストは文字列である必要があります")
sys.exit(10)
if not isinstance(new_text, str):
print("エラー: 置換後テキストは文字列である必要があります")
sys.exit(10)
if not old_text:
print("エラー: 置換前テキストが空です")
sys.exit(10)
if len(old_text) > MAX_TEXT_LENGTH:
print(f"エラー: 置換前テキストが長すぎます: {len(old_text)} > {MAX_TEXT_LENGTH}")
sys.exit(10)
if len(new_text) > MAX_TEXT_LENGTH:
print(f"エラー: 置換後テキストが長すぎます: {len(new_text)} > {MAX_TEXT_LENGTH}")
sys.exit(10)
# バックアップファイル作成
backup_path = file_path.with_suffix(f'.backup{file_path.suffix}')
try:
shutil.copy2(file_path, backup_path)
print(f"バックアップ作成完了: {backup_path}")
except Exception as e:
print(f"エラー: バックアップ作成に失敗しました: {e}")
sys.exit(10)
# プレゼンテーション読み込み
print(f"プレゼンテーション読み込み開始: {file_path}")
try:
presentation = Presentation(file_path)
except PackageNotFoundError:
print(f"エラー: ファイルが破損しているか、有効なPowerPointファイルではありません: {file_path}")
sys.exit(10)
except Exception as e:
print(f"エラー: ファイル読み込みに失敗しました: {e}")
sys.exit(10)
slide_count = len(presentation.slides)
print(f"プレゼンテーション読み込み完了: {slide_count}スライド")
# スライド番号検証
if target_slide is not None:
if not isinstance(target_slide, int):
print("エラー: スライド番号は整数である必要があります")
sys.exit(10)
if target_slide < 1 or target_slide > slide_count:
print(f"エラー: スライド番号が範囲外です: {target_slide}(有効範囲: 1-{slide_count})")
sys.exit(10)
# メイン処理
print(f"テキスト置換開始: '{old_text}' -> '{new_text}'")
if target_slide is not None:
# 特定スライド処理
print(f"対象: スライド {target_slide}")
slide = presentation.slides[target_slide - 1]
slide_replace_count = 0
slide_shapes_processed = 0
for shape in slide.shapes:
slide_shapes_processed += 1
# テキストフレーム処理
if shape.has_text_frame and shape.text_frame:
for paragraph in shape.text_frame.paragraphs:
for run in paragraph.runs:
if old_text in run.text:
# フォーマット情報保存
font_name = run.font.name
font_size = run.font.size
font_bold = run.font.bold
font_italic = run.font.italic
font_color = run.font.color.rgb if run.font.color.rgb else None
# 置換回数カウント(置換前に実施)
count = run.text.count(old_text)
slide_replace_count += count
# テキスト置換
run.text = run.text.replace(old_text, new_text)
# フォーマット復元
if font_name:
run.font.name = font_name
if font_size:
run.font.size = font_size
if font_bold is not None:
run.font.bold = font_bold
if font_italic is not None:
run.font.italic = font_italic
if font_color:
run.font.color.rgb = font_color
# テーブル処理
if hasattr(shape, 'table'):
try:
table = shape.table
for row_idx, row in enumerate(table.rows):
for col_idx, cell in enumerate(row.cells):
if cell.text_frame:
for paragraph in cell.text_frame.paragraphs:
for run in paragraph.runs:
if old_text in run.text:
# フォーマット情報保存
font_name = run.font.name
font_size = run.font.size
font_bold = run.font.bold
font_italic = run.font.italic
font_color = run.font.color.rgb if run.font.color.rgb else None
# 置換回数カウント(置換前に実施)
count = run.text.count(old_text)
slide_replace_count += count
# テキスト置換
run.text = run.text.replace(old_text, new_text)
# フォーマット復元
if font_name:
run.font.name = font_name
if font_size:
run.font.size = font_size
if font_bold is not None:
run.font.bold = font_bold
if font_italic is not None:
run.font.italic = font_italic
if font_color:
run.font.color.rgb = font_color
except AttributeError:
# テーブルアクセスエラーは無視
pass
total_replacements += slide_replace_count
shapes_processed += slide_shapes_processed
slides_processed = 1
result_msg = f"{slide_replace_count}箇所を置換" if slide_replace_count > 0 else "置換対象なし"
print(f"スライド {target_slide}: {result_msg}")
else:
# 全スライド処理
print(f"対象: 全スライド ({slide_count}スライド)")
for i, slide in enumerate(presentation.slides, 1):
slide_replace_count = 0
slide_shapes_processed = 0
for shape in slide.shapes:
slide_shapes_processed += 1
# テキストフレーム処理
if shape.has_text_frame and shape.text_frame:
for paragraph in shape.text_frame.paragraphs:
for run in paragraph.runs:
if old_text in run.text:
# フォーマット情報保存
font_name = run.font.name
font_size = run.font.size
font_bold = run.font.bold
font_italic = run.font.italic
font_color = run.font.color.rgb if run.font.color.rgb else None
# 置換回数カウント(置換前に実施)
count = run.text.count(old_text)
slide_replace_count += count
# テキスト置換
run.text = run.text.replace(old_text, new_text)
# フォーマット復元
if font_name:
run.font.name = font_name
if font_size:
run.font.size = font_size
if font_bold is not None:
run.font.bold = font_bold
if font_italic is not None:
run.font.italic = font_italic
if font_color:
run.font.color.rgb = font_color
# テーブル処理
if hasattr(shape, 'table'):
try:
table = shape.table
for row_idx, row in enumerate(table.rows):
for col_idx, cell in enumerate(row.cells):
if cell.text_frame:
for paragraph in cell.text_frame.paragraphs:
for run in paragraph.runs:
if old_text in run.text:
# フォーマット情報保存
font_name = run.font.name
font_size = run.font.size
font_bold = run.font.bold
font_italic = run.font.italic
font_color = run.font.color.rgb if run.font.color.rgb else None
# 置換回数カウント(置換前に実施)
count = run.text.count(old_text)
slide_replace_count += count
# テキスト置換
run.text = run.text.replace(old_text, new_text)
# フォーマット復元
if font_name:
run.font.name = font_name
if font_size:
run.font.size = font_size
if font_bold is not None:
run.font.bold = font_bold
if font_italic is not None:
run.font.italic = font_italic
if font_color:
run.font.color.rgb = font_color
except AttributeError:
# テーブルアクセスエラーは無視
pass
total_replacements += slide_replace_count
shapes_processed += slide_shapes_processed
if slide_replace_count > 0:
print(f"スライド {i}: {slide_replace_count}箇所を置換")
slides_processed = slide_count
print(f"テキスト置換完了: 合計 {total_replacements}箇所")
# 結果出力
if total_replacements > 0:
# ファイル保存
save_path = Path(output_file) if output_file else file_path
# 出力ディレクトリ作成
save_path.parent.mkdir(parents=True, exist_ok=True)
print(f"ファイル保存開始: {save_path}")
try:
presentation.save(save_path)
file_size = save_path.stat().st_size
print(f"ファイル保存完了: {save_path} ({file_size:,} bytes)")
except Exception as e:
print(f"エラー: ファイル保存に失敗しました: {e}")
sys.exit(10)
# 統計情報表示
print(f"\n=== 処理完了 ===")
print(f"置換回数: {total_replacements}回")
print(f"処理スライド数: {slides_processed}枚")
print(f"処理シェイプ数: {shapes_processed}個")
print("\n※ 置換回数は実際に文字列が置き換えられた箇所の総数です")
print("※ バックアップファイルが元ファイルと同じディレクトリに作成されます")
else:
print("置換対象のテキストが見つかりませんでした")
print("※ 大文字小文字を区別して検索されます")
使用方法
テスト用のPowerPointファイル(test.pptx)を作成し、以下の実行手順で動作確認を行う。
基本的な使用例
python pptx_text_replacer.py test.pptx "置換前" "置換後"
特定スライドでの置換
python pptx_text_replacer.py test.pptx "置換前" "置換後" --slide 2
出力ファイル指定
python pptx_text_replacer.py test.pptx "置換前" "置換後" --output output.pptx
ヘルプの表示
python pptx_text_replacer.py --help
実行時には処理状況が表示され、自動的にバックアップファイルが作成される。エラーが発生した場合は対応するエラーコード(10: 入力値エラー、20: ファイル操作エラー、30: 処理エラー)が表示される。
処理対象と機能
- 特定のスライドまたは全スライドでのテキスト置換
- テキストフレームとテーブル内のテキスト処理
- フォント書式保持機能(書体、サイズ、色、装飾の維持)
制限事項
- チャート内のテキストは処理対象外(チャートデータは別のXML構造で管理)
- SmartArt内のテキストは処理対象外(SmartArtは独立したXML要素)
- パスワード保護されたファイルは処理不可(暗号化により内部構造にアクセス不可)