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をインストールしていない場合の手順(インストール済みの場合は実行不要)。

  1. 管理者権限でコマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)。
  2. 以下のコマンドをそれぞれ実行する(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: 処理エラー)が表示される。

処理対象と機能

制限事項