概要

このプログラムは、PowerPointファイル(.pptx)からテキストを抽出し、各テキスト要素が箇条書きかどうかを判定します。コマンドライン引数でファイルを指定すると、スライドごとにテキスト、箇条書き状態、シェイプタイプ、プレースホルダータイプを解析し、コンソールに出力します。テキストボックス、プレースホルダー、表など様々な要素に対応し、エラー処理も行います。PowerPointの内容分析や文書変換の前処理に役立ちます。

仕様書

## 1. 概要
このプログラムは、Microsoft PowerPointのプレゼンテーションファイル(.pptx)からテキスト情報を抽出し、各テキスト要素が箇条書きであるかどうかを判定するツールです。抽出したテキストとその属性(箇条書きかどうか、シェイプタイプ、プレースホルダータイプ)をコンソールに出力します。

## 2. 主な機能
### 1. PowerPointファイルの読み込み
   * コマンドライン引数で指定したPowerPointファイル(.pptx)を読み込みます。
   * 読み込み対象は.pptxファイルのみです。

### 2. スライドの処理
   * PowerPointファイル内の各スライドを個別に処理します。
   * 各スライドの開始と終了をコンソールに表示します。

### 3. テキスト要素の抽出
   * スライド内の各シェイプからテキストを抽出します。
   * テキストボックス、プレースホルダー、通常の図形など、様々なタイプのシェイプに対応します。

### 4. 箇条書きの判定
   * 各テキスト要素が箇条書きであるかどうかを判定します。
   * 判定基準には、段落レベル、プレースホルダータイプ、箇条書き記号の有無を使用します。

### 5. シェイプ情報の取得
   * 各テキスト要素のシェイプタイプを取得します。
   * プレースホルダーの場合は、そのタイプも取得します。

### 6. テーブルの処理
   * スライド内のテーブルを検出し、各セル内のテキストを抽出します。

### 7. 出力
   * 抽出したテキスト、箇条書きかどうかの判定結果、シェイプタイプ、プレースホルダータイプ(該当する場合)をコンソールに出力します。

## 3. 使用方法
1. コマンドラインからプログラムを実行します。
2. 引数として処理したいPowerPointファイル(.pptx)のパスを指定します。
   例:`python script.py path/to/your/powerpoint.pptx`
3. プログラムがファイルを処理し、結果をコンソールに表示します。

## 4. 出力形式
* 処理開始時に、PowerPointファイル名と総スライド数を表示します。
* 各スライドの開始と終了を示すメッセージを表示します。
* 各テキスト要素について、以下の情報を表示します。
  - テキスト内容
  - 箇条書きかどうか(「はい」または「いいえ」)
  - シェイプタイプ
  - プレースホルダータイプ(プレースホルダーの場合)
* 処理完了時に、処理したスライド数を表示します。

## 5. エラー処理
* コマンドライン引数が正しく指定されていない場合、使用方法を表示して終了します。
* PowerPointファイルの読み込みに失敗した場合、エラーメッセージを表示します。
* 個々のシェイプの処理中にエラーが発生した場合、警告メッセージを表示し、次のシェイプの処理を続行します。
* 全体の処理中に予期しないエラーが発生した場合、エラーメッセージを表示して終了します。

## 6. 注意事項
* このプログラムは、テキスト情報と箇条書きの判定に特化しています。画像、グラフ、アニメーションなどの非テキスト要素は処理しません。
* 複雑なレイアウトや特殊な書式設定を使用したスライドでは、箇条書き判定の精度が下がる場合があります。

このプログラムを使用することで、PowerPointプレゼンテーションのテキスト内容を抽出し、各テキスト要素の属性(箇条書きかどうか、シェイプタイプなど)を確認できます。プレゼンテーションの内容分析や、テキストベースの文書への変換の前処理として役立ちます。

プログラム

from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.enum.shapes import PP_PLACEHOLDER
import sys


def is_bullet_point(paragraph, shape):
    # 段落レベルが0より大きい場合は箇条書き
    if paragraph.level > 0:
        return True

    # プレースホルダーの種類に基づいて箇条書きを判定
    if shape.is_placeholder:
        placeholder_type = shape.placeholder_format.type
        if placeholder_type in (PP_PLACEHOLDER.BODY, PP_PLACEHOLDER.OBJECT):
            return True

    # 箇条書きの記号(bullet)が設定されているか確認
    pPr = paragraph._p.find(
        '{http://schemas.openxmlformats.org/drawingml/2006/main}pPr'
    )
    if pPr is not None:
        for tag in ('buChar', 'buAutoNum'):
            if pPr.find(
                '{http://schemas.openxmlformats.org/drawingml/2006/main}' + tag
            ) is not None:
                return True

    return False


def extract_text_from_shape(shape):
    text_data = []
    if shape.has_text_frame:
        text_frame = shape.text_frame
        for paragraph in text_frame.paragraphs:
            text = paragraph.text.strip()
            is_bullet = is_bullet_point(paragraph, shape)
            if text:
                text_data.append((text, is_bullet))
    elif hasattr(shape, 'text') and shape.text.strip():
        # テキストボックス以外のシェイプのテキストを処理
        text = shape.text.strip()
        text_data.append((text, False))  # 箇条書きでないと仮定
    return text_data


def process_shape(shape):
    if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
        for subshape in shape.shapes:
            process_shape(subshape)
    else:
        text_data = extract_text_from_shape(shape)
        for text, is_bullet in text_data:
            print(f"テキスト: {text}")
            print(f"箇条書き: {'はい' if is_bullet else 'いいえ'}")
            print(f"シェイプタイプ: {shape.shape_type}")
            if shape.is_placeholder:
                print(f"プレースホルダータイプ: {shape.placeholder_format.type}")
            print("-" * 30)


def process_table(table):
    for row in table.rows:
        for cell in row.cells:
            text_frame = cell.text_frame
            for paragraph in text_frame.paragraphs:
                text = paragraph.text.strip()
                if text:
                    print(f"テキスト (表のセル): {text}")
                    print("箇条書き: いいえ")
                    print("-" * 30)


def process_slide(slide, slide_number):
    print(f"=== スライド {slide_number} ===")
    for shape in slide.shapes:
        try:
            if shape.shape_type == MSO_SHAPE_TYPE.TABLE:
                process_table(shape.table)
            else:
                process_shape(shape)
        except Exception as e:
            print(f"警告: スライド {slide_number} のシェイプ処理中にエラーが発生しました: {str(e)}")
    print(f"=== スライド {slide_number} 終了 ===\n")


def main(pptx_file):
    try:
        prs = Presentation(pptx_file)
        total_slides = len(prs.slides)
        print(f"PowerPointファイル '{pptx_file}' を処理します。全 {total_slides} スライド。\n")

        for i, slide in enumerate(prs.slides, start=1):
            process_slide(slide, i)

        print(f"処理完了。{total_slides} スライドを処理しました。")
    except Exception as e:
        print(f"エラー: PowerPointファイルの処理中に問題が発生しました: {str(e)}")
        sys.exit(1)


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("使用方法: python script.py ")
        sys.exit(1)

    pptx_file = sys.argv[1]
    main(pptx_file)