意識低い系Powerpoint資料検索システムをつくろう

## 背景
製造業あるある風景
・まとめ資料、提出資料は共有サーバーやNASに置くことになっている
・共有ドライブ上に資料が置かれていくが、台帳で管理されているわけではない
・そのうちどこに何があるかわからなくなり、定期的に過去資料発掘キャンペーンが開催

どこでもこんなものかと思いますが、めんどくさがりの人にはつらいと思います。
これに対処するために昔てきとうに作った意識低い系データベースの記録を残しておきます。
意識低いというのは「やる気がなくてもおいしくできるお手軽料理本」的な意味です。
つまり全くおしゃれではないですが「こういうのでいいんだよ」的な簡易文書検索を目指しました。

## 方針
共有サーバー上にあるPowerpointファイルのデータベースを作ります。
AccessでデータベースとGUI部分を作って、Pythonでそれを更新していく感じにします。

### Access部分に欲しい機能
・スライド中のテキストを検索し、そのファイルの場所とページ番号を知ることができる
・データベースUIからクリックで当該ファイルを開ける

### Python部分に欲しい機能
・テキスト抽出
・データベースをレコードを追加できる(INSERT) 
・内容が更新されている場合はレコードを更新する(UPDATE)

## Access部分の実装(データベース編)
テーブルの構造はこんな感じにしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3471244/3d43dd04-c356-e639-8bbe-5956ed7785c8.png)

これをもとにフォームウィザードで帳票形式のフォームも作っておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3471244/750dc197-705f-285f-05df-2367cfce2362.png)

ファイルのパスを開くボタンも追加しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3471244/f1b15a87-cca6-dd1f-1c12-c7d1ce770253.png)
``` vb:ボタンに割り振る動作のコード
Private Sub コマンド9_Click()
Dim filepath As String
If IsNull(Me.Path) = True Then
    MsgBox "No path in record"
    Exit Sub
End If

filepath = Me.Path
Dim pp_app As Object

If Dir(filepath, vbNormal) <> "" Then
    Dim rc As Integer
    rc = MsgBox(filepath & "が見つかりました。開きますか?", vbYesNo + vbQuestion, "ファイルを開く")
    If rc = vbYes Then
        Set pp_app = CreateObject("PowerPoint.Application")
        pp_app.Presentations.Open filepath
        Set pp_app = Nothing
    End If
Else
MsgBox "ファイルが見つかりません。"

End If

End Sub

```

ボタンを押すとファイルを開くかどうかのダイアログが出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3471244/4200f5ec-a157-246e-ecc3-041dea69bbc3.png)



## Access部分の実装(検索編)
任意のフレーズで検索できるような仕組みを追加しておきます。
検索文字列を入力するテキストボックスと、フィルター処理を実行するボタンをフォームの上に作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3471244/90a2a98a-7508-aaef-e0e3-17a973b3c773.png)
```vb:検索ボタンに割り振る動作のコード
Private Sub コマンド13_Click()
Me.Refresh
Dim filters, sqltmp As String
filters = ""
sqltmp = ""

'フィルター文の準備
If IsNull(searchpath) = False Then
    filters = "(Path like '*" & Me.searchpath & "*')"
End If

If IsNull(search1) = False Then
    sqltmp = "(Texts like '*" & Me.search1 & "*')"
    If filters = "" Then
        filters = sqltmp
    Else
    filters = filters & "AND " & sqltmp
    End If
sqltmp = ""
End If

If IsNull(search2) = False Then
    sqltmp = "(Texts like '*" & Me.search2 & "*')"
    If filters = "" Then
        filters = sqltmp
    Else
    filters = filters & "AND " & sqltmp
    End If
sqltmp = ""
End If

Me.Filter = filters
Me.FilterOn = True
If filters = "" Then
    Me.FilterOn = False
End If

End Sub
```

動作確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3471244/a238c399-4015-aa2f-1698-596a04672d4f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3471244/0d86cadc-127b-7bd9-7d32-bd78fb7398ba.png)

ほかの人も使う場合、Accessファイルを開いたときにナビゲーションウィンドウは表示せず、またフォームが最初から表示されるように設定しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3471244/131611f6-fa2c-5f92-403d-ac97fb942c63.png)


## Python(データベースの更新)部分の実装
ここからはデータベース生成・更新用のPythonコードを書きます。
次の操作を行います。
・指定したディレクトリ内のpptxファイルからテキストを抽出
・上記のAccessデータベースに登録または更新する

また、python-pptxを環境に、「Microsoft Access データベース エンジン 2016 再頒布可能コンポーネント」を事前にお使いのシステムに導入しておく必要があります。(たぶん)

```python:データベース生成保守
from glob import glob
import pptx
from pptx.exc import PackageNotFoundError
import os
import pyodbc

#スライドを探すフォルダを設定
folder_paths=[r'C:\database']

#ファイル一覧作成
print("PowerPointファイルの一覧を作っています...")
file_paths=[]
for folder_path in folder_paths:
    file_paths +=glob(folder_path+r'\**\*.pptx', recursive=True)

#データベース接続
conn_str= (
    r'DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};'
    r'DBQ=C:\Database.accdb;'#Accessファイルのパス
    )
conn=pyodbc.connect(conn_str)
cursor=conn.cursor()

def text_extract(pptx_file):
    try:
        prs=pptx.Presentation(pptx_file)
    except PackageNotFoundError:
        print(pptx_file)
        print("パスワードがかかっているか、空のファイルです")
        print('_'*40)
    record_list=[]
    for i, sld in enumerate(prs.slides, start=1):
        record_dict={'filepath':pptx_file,'page_num':i,'text':''}
        extracted_texts=''
        for shape in sld.shapes:
            if shape.has_text_frame:
                extracted_texts+=r'/'+shape.text.replace('\n','')
            if shape.has_table:
                for cell in shape.table.iter_cells():
                    extracted_texts+=r'/'+cell.text.replace('\n','')
        record_dict['text']=extracted_texts
        record_list.append(record_dict)
    return record_list

#データベースに登録
for pptx_file in file_paths:
    if r'~$' in pptx_file:
        continue
    record_set=text_extract(pptx_file)
    for record in record_set:
        param_insert=[record['filepath'],record['page_num'],record['text']]
        sql_insert="INSERT INTO [テーブル1] ([Path],[Page],[Texts]) VALUES (?,?,?)"
        param_update=[record['text'],record['filepath'],record['page_num']]
        sql_update="UPDATE [テーブル1] SET [Texts]=? WHERE [Path]=? AND [Page]=?"
        cursor.execute(sql_update,param_update)
        if cursor.rowcount>0:
            print("updated")
        else:
            cursor.execute(sql_insert, param_insert)
        cursor.commit()
cursor.close()
conn.close()
print("おわり")
```

動作確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3471244/03f67d8f-2b56-2785-1307-d552bdfaaddc.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3471244/e5a97483-2b4b-1342-98da-00a801164993.png)


## 使い方メモ
・共有サーバーにAccessファイルを置いて、リンクを利用者におしえる
・定期的にPythonのコードを実行して更新しよう
・PowerPointファイルが多い場合は書き込み処理が多くなるので、Accessファイルをローカルに置いて更新し、終わったら共有サーバーに置くのがいいでしょう
## この方法のよいところ
・人を説得して巻き込む必要がない(とても重要)
・保守が楽

## おわりに
まあSharepointに置けば全て解決するんですけどね。
ただ普通は新しいやり方を提案しても面倒だといわれることは多そうなので、
既存のやり方を変える必要がないこういう迂回策のほうが話が早いことはよくある気がします。
今でも誰かの役に立つと信じています。

## 追記
ファイル名変わったらリンク切れが起きますね。
リンク切れをチェックして削除するコードも考えました。

https://qiita.com/walfiesfan/items/c0bcd71bab894407982c