【PowerPoint】PythonでPowerPointファイルから図番号を自動取得する

## はじめに
<!--
何についての記事なのか、誰に向けた記事なのかを簡単に説明しましょう。
-->
PowerPointで資料を作成した後に、図番号の整合が取れているかチェックしたい時がありますよね。今回はその第一歩として、Pythonを用いてPowerPointファイルのスライドの図番号を自動取得する方法についてご紹介します。

### この記事でわかる・できること
<!--
読んだら何がわかるようになるのか、何ができるようになるのかを書きましょう。
-->

- PowerPointファイルをXMLファイルに変換する方法がわかります
- PowerPointファイルから図番号を自動で取得する方法がわかります


### この記事の対象者
<!--
誰に向けた記事なのかを明確にすることで読者に選んでもらいましょう。
-->

- PythonでPowerPointファイルを操作したい人
- 資料点検業務を効率化したいと思っている人


### 動作環境・使用するツールや言語

- OS バージョン
    - Windows11 23H2
- ツール
    - Spyder 5.5.1
- 言語
    - Python 3.12

## PowerPointファイルをXMLファイルに変換する
<!--
章全体の要約や、何について書くのかをここに書きます。
記事が長い時や資料として作るときは番号を振ることをおすすめします。
章を増やすときは章から項までをコピーしてください。
-->
PowerPointファイル(.pptx)はXMLファイル(.xml)の集合です。.pptxファイルを圧縮、解凍することにより各スライドを.xmlファイルに変換することができます。
手順は以下のとおりです。

1. PowerPointファイルの拡張子を「.zip」形式に変更します
2. zipファイルに変更したファイルを解凍(展開)します

展開したフォルダを開くと以下のようなフォルダ構成になっています。
_rels
docProps
ppt
[Context_Types].xml

pptフォルダの中に「slides」というフォルダがあり、そこにXMLファイルになった各スライドのデータが保存されています。
slide1.xml
slide2.xml
...

## Pythonで図番号を自動取得する
### 図番号の自動取得

では実際に、XMLファイルにしたPowerPointファイルをPythonで読み込み、図番号を自動で取得するプログラムを作ってみましょう。
まずは図番号を自動的に取得するだけのコードです。

#### コード例

```python:diag_analy.py
import xml.etree.ElementTree as ET
import re
import os
import pandas as pd

def extract_figure_numbers_with_slide(folder_path):
    prs_path = os.path.join(folder_path, 'ppt', 'slides')
    
    # フォルダが存在するかをチェック
    if not os.path.exists(prs_path):
        print(f"指定されたパスが見つかりません: {prs_path}")
        return []
    
    figure_slide_map = []
    pattern = r'図\d+'

    for slide_index, slide_file in enumerate(os.listdir(prs_path), start=1):
        if slide_file.startswith('slide'):
            slide_path = os.path.join(prs_path, slide_file)
            if os.path.isfile(slide_path):
                tree = ET.parse(slide_path)
                root = tree.getroot()
                slide_text = ''.join(root.itertext())

                matches = re.findall(pattern, slide_text)
                for match in set(matches):
                    figure_slide_map.append((match, slide_index))

    return figure_slide_map

# main
extracted_folder_path = 'sample'  # 解凍先のパスを指定
figure_slide_map = extract_figure_numbers_with_slide(extracted_folder_path)

if figure_slide_map:
    # データフレームとして表示
    df = pd.DataFrame(figure_slide_map, columns=["Figure", "Slide"])
    print(df)
else:
    print("図番号が見つかりませんでした。")
```

　なお、コードを実行する前に前章の処理(対象となるPowerPointファイルをzip形式に変更，その後解凍)しておいてください。
　コードを実行する際、「sample」をご自身でPowerPointファイルを解凍したフォルダに変えてください。

　コードを実行した結果がこちらです。sampleではスライド1に図1～3が記載されており、スライド2に図2が、スライド3に図3が記載されていました。

#### コード実行結果
```
  Figure  Slide
0     図3      1
1     図1      1
2     図2      1
3     図2      2
4     図3      3
```

### 図番号と図タイトルの自動取得

次に図番号と一緒に図タイトルも自動で取得してみます。

#### コード例

```python:diag_analy2.py
from pptx import Presentation
import re
import pandas as pd

def extract_figure_and_table_info(pptx_file):
    prs = Presentation(pptx_file)
    info_list = []

    # 図番号と表番号のパターンを定義
    figure_pattern = re.compile(r'(図\d+)\s*(.*)')
    table_pattern = re.compile(r'(表\d+)\s*(.*)')

    for slide_index, slide in enumerate(prs.slides, start=1):
        for shape in slide.shapes:
            if shape.has_text_frame:
                text = shape.text_frame.text

                # 図番号と図タイトルの抽出
                figure_matches = figure_pattern.findall(text)
                for match in figure_matches:
                    figure_number, figure_title = match
                    figure_title = figure_title.strip()
                    if figure_title and figure_title[-1] in '。．，、':
                        figure_title = "呼び出し"
                    info_list.append((figure_number, slide_index, figure_title))

                # 表番号の抽出
                table_matches = table_pattern.findall(text)
                for match in table_matches:
                    table_number, table_title = match
                    table_title = table_title.strip()
                    info_list.append((table_number, slide_index, table_title))

    return info_list

# 使用例
pptx_file = 'sample'
info_list = extract_figure_and_table_info(pptx_file)

if info_list:
    # データフレームとして表示
    df = pd.DataFrame(info_list, columns=["Diag/Table", "Slide", "Title"])
    print(df)
else:
    print("図番号または表番号が見つかりませんでした。")
```

　図タイトルは同じテキストボックス内で図Xに続く文字列としました。
　ただ、図Xの後ろに続く文字列をそのまま取得して図タイトルとしてしまうと、文中で図を呼び出している箇所にも反応してしまいます。
　そこで、一旦図Xに続く文字列を取得した後に句読点で終わる文字列かどうかを判定し、句読点で終わる場合には図を呼び出している箇所と判断して図タイトルを「呼び出し」とするようにしました。


#### コード実行結果
```
  Diag/Table  Slide Title
0         図1      1  はじめに
1         図2      1  呼び出し
2         図3      1  呼び出し
3         図2      2    概要
4         図3      3    結果
5         表1      4   比較表
```

### 図番号と図タイトルの自動取得

せっかく図番号がどのスライドで登場するのかがわかるので、どのスライドで呼ばれているかがわかるように出力フォーマットを変更してみましょう。

フォーマット
図番号　図タイトル　呼び出しスライド

#### コード例

```python:diag_analy3.py
from pptx import Presentation
import re
import pandas as pd
from collections import defaultdict

def extract_figure_and_table_info(pptx_file):
    prs = Presentation(pptx_file)
    info_dict = defaultdict(lambda: {'title': '', 'slides': []})

    # 図番号と表番号のパターンを定義
    figure_pattern = re.compile(r'(図\d+)\s*(.*)')
    table_pattern = re.compile(r'(表\d+)\s*(.*)')

    for slide_index, slide in enumerate(prs.slides, start=1):
        for shape in slide.shapes:
            if shape.has_text_frame:
                text = shape.text_frame.text

                # 図番号と図タイトルの抽出
                figure_matches = figure_pattern.findall(text)
                for match in figure_matches:
                    figure_number, figure_title = match
                    figure_title = figure_title.strip()
                    if figure_title and figure_title[-1] in '。．，、':
                        figure_title = "呼び出し"
                    info_dict[figure_number]['title'] = figure_title
                    info_dict[figure_number]['slides'].append(slide_index)

                # 表番号の抽出
                table_matches = table_pattern.findall(text)
                for match in table_matches:
                    table_number, table_title = match
                    table_title = table_title.strip()
                    info_dict[table_number]['title'] = table_title
                    info_dict[table_number]['slides'].append(slide_index)

    return info_dict

# 使用例
pptx_file = 'sample'
info_dict = extract_figure_and_table_info(pptx_file)

if info_dict:
    # データフレームとして表示
    data = []
    for diag_table, info in info_dict.items():
        slides = ','.join(map(str, sorted(set(info['slides']))))
        data.append([diag_table, info['title'], slides])

    df = pd.DataFrame(data, columns=["Diag/Table", "Title", "Slide"])
    print(df)
else:
    print("図番号または表番号が見つかりませんでした。")

```

#### コード実行結果
```
  Diag/Table Title Slide
0         図1  はじめに     1
1         図2    概要   1,2
2         図3    結果   1,3
3         表1   比較表     4
```

最初のコード実行結果と比べてすっきりまとまりましたね。

## おわりに・まとめ
<!--
どちらかを書きます。あってもなくても良いです。
-->
今回はPythonでPowerPointファイルを操作して図番号を自動取得する方法について試してみました。今後はもう少し改良を重ねて図番号の自動修正にチャレンジしようと思っています。
この記事がどなたかのお役に立てば幸いです。

### 関連記事(続き)
[【PowerPoint】PythonでPowerPointファイルから図番号を自動取得する](https://qiita.com/ebi3kani3/items/afc86ba5f8fcc85b8eac)