僕のpsycopg2とopenpyxlによるデータ一斉取得ツール　～VBAマクロ撲滅計画Phase#3 ～

# 導入
[前回](https://qiita.com/SSKNOK/items/b1d0ad2309c6ed34a596)に引き続き、VBAマクロを撲滅するために何か使えるものはないかな～とか探っております。
今回は参画中のPJでもまさにVBAマクロを使って解決しているテスト前後のエビデンス（データ）の一斉取得をするツールを作成したいと思います。
DBにアクセスしたりするので今回はPythonの方が使い勝手が良さそうな雰囲気がします。
ということで今回はPythonを使ってツールを作成していきたいと思います。
というかJupyterとPythonさえあれば、だいたいなんでもできるような、、、前職では画像認識とか使ってなんちゃってRPAなんかも作ってました。


# ツール作成の流れ

1. Jupyterを使って動作を確認しつつコード作成。
2. Pyファイルにまとめる。
3. Pythonが入っていない環境でも使えるように環境ごとexe化。

こんな流れで開発しました。
結果的に2人/日くらいかかっちゃいました。
前職で涙を流しながら携帯電話の位置情報データ分析をしてた時もJupyterでわちゃわちゃやってたな。。。
（ちょっとローカルのPostgresqlがね、、、リモート接続受け付けない設定になっててパニックになってたら4時間くらい使ってました。）

# ツールの動作の流れ
なんとなく頭の中にイメージしているツールの処理フローは↓の感じ。

1. ツールの設定ファイル読み込み。
2. クエリ条件ファイル読み込み。
3. テンプレートSQLファイル読み込み。
4. エクセルのオブジェクト生成。
5. クエリ実行。
6. エクセルへの出力。
7. エクセル保存。

DBはPostgresql前提なのでpsycopg2でDBアクセス、エクセルファイルはopenpyxlで操作。

# 前提
DB：Postgresql
Python：3.9
anacondaに入ってたPythonが3.9だったので使ってます。特にこだわりはないです。

# 各ソース
一応作ったソースは↓にあります。
https://github.com/SSKNOK/get_datas_for_evidence_tool

ソースはとりあえず、Jupyter版を載せます。
とはいえ、PyファイルにしてもメインのPyファイルにimport文書くだけだからほとんど変わらないです。
詳しくはgitのソースを見てください。

## ツール設定ファイル読み込み処理

Pythonのファイル読み込みが本当に楽に書けるのでうれしい。
反面、改行コードまで読み取っちゃうので、そこはお世話が必要ですが。

処理内容は簡単で
`./setting/tool_setting.txt`にあるテキストファイルを読んで、`＝`で書かれた設定項目と設定値をスプリットして辞書（JavaでいうMap）にしてるだけです。

```python
########################################################################################
# ツール設定ファイル読み込み処理
# [概要]ツール設定ファイルを読み込んで辞書として返却します。
# [引数1]ツールルートディレクトリパス
# [戻り値]./setting/tool_setting.txtの内容を「=」でスプリットした辞書オブジェクト
########################################################################################

# モジュール読み込み
import os

def read_tool_settings(root_dir_path):

    # ツール設定ファイル絶対パス
    root_file_path = os.path.join(root_dir_path, "setting", "tool_setting.txt")

    # ツール設定ルール辞書
    setting_dict = {}

    # ツール設定ファイル読み込んで辞書型のオブジェクトに変換
    with open(root_file_path, 'r', encoding='utf-8') as setting_file:
        for setting_rule in setting_file:

            #始まりが「#」もしくは空の行はスキップする。
            if setting_rule.startswith("#"):
                continue
            if setting_rule == "\n":
                continue

            setting_item = setting_rule.replace("\n","").split("=")[0].strip()
            setting_value = setting_rule.replace("\n","").split("=")[1].strip()

            setting_dict[setting_item] = setting_value

    return setting_dict
```


## クエリ条件ファイル読み込み処理。

`ツール設定ファイル読み込み処理`とほぼ同じです。

```python
########################################################################################
# クエリ条件ファイル読み込み処理
#[概要]クエリ条件ファイルを読みこみクエリ条件を辞書のリストとして返却します。
# [引数1]ツールルートディレクトリパス
# [戻り値]./setting/query_condition.txtの内容を「,」でスプリットした辞書オブジェクトのリスト
########################################################################################

# モジュール読み込み
import os

def read_query_conditions(root_dir_path):
        
        # クエリ条件ファイル絶対パス
        root_file_path = os.path.join(root_dir_path, "setting", "query_condition.txt")
        
        # クエリ条件リスト（{スキーマ, テーブル物理名, データ取得条件}を各行に格納）
        query_condition_list = []
        
       # クエリ条件ファイル読み込んで辞書型のオブジェクトに変換
        with open(root_file_path, 'r', encoding='utf-8') as query_file:
            for query_condition in query_file:
                
                #始まりが「#」もしくは空の行はスキップする。
                if query_condition.startswith("#"):
                    continue
                if query_condition == "\n":
                    continue
                    
                splitted_query_condition = query_condition .replace("\n","").split(",", 2)
                
                query_condition_list.append({"schema":splitted_query_condition[0].strip(), "table":splitted_query_condition[1].strip(), "where":splitted_query_condition[2].strip()})
                
        return query_condition_list
```

## テンプレートSQLファイル読み込み処理

こちらも前2つとほぼ一緒でファイルを読み込んで辞書にしているだけですが
今度は`./sql_templates`配下のファイルごとに内容を全量読み込んで辞書に格納していってます。

```py
########################################################################################
# テンプレートSQLファイル読み込み処理
#[概要]テーブル論物名取得SQL、カラム論物名取得SQL、データ取得SQLを読み込みます。
# [引数1]ツールルートディレクトリパス
# [戻り値]./sql_templates配下のSQLファイルを読みこんだ辞書オブジェクト
########################################################################################

# モジュール読み込み
import os

def read_template_sqls(root_dir_path):
    
    # テンプレートSQLディレクトリ絶対パス
    template_sql_dir_path = os.path.join(root_dir_path, "sql_templates")
    
    # テンプレートSQL辞書
    template_sql_dict = {}
    
    # テンプレートSQLディレクトリ配下のファイル一覧を取得（再帰的には取得しない）
    files = os.listdir(template_sql_dir_path)
    template_sql_files = [f for f in files if os.path.isfile(os.path.join(template_sql_dir_path, f))]
    
    # テンプレートSQLディレクトリ配下のファイル内容をそれぞれ読み取り
    for template_sql_file in template_sql_files:
        with open(os.path.join(template_sql_dir_path, template_sql_file), 'r', encoding='utf-8') as query_file:
            template_sql_dict[template_sql_file.replace(".sql","")] = query_file.read()
    
    return template_sql_dict
            
```

## エクセルファイル操縦クラス
エクセルの操縦だけはメソッドではなくクラスで管理します。
理由はインスタンンス変数として次に書き込む行だったりエクセルのオブジェクトを持ち続けたりさせたいためです。
ソースがちょっと長いので全量は折りたたんで格納しておきます。

<details><summary>エクセルファイル操縦クラス</summary>

```py
########################################################################################
# エクセルファイル操縦クラス
#[概要]ワークブックの操縦を行います
########################################################################################

import openpyxl
import os
import PySimpleGUI
from openpyxl.styles.borders import Border, Side

class operate_excel:
    
    # テンプレートエクセルファイル利用
    use_template_flag = None
    # 出力フォルダ名（フルパス）
    output_absolute_dir_name = None
    # 出力ファイル名（フルパス）
    output_absolute_file_name = None
    # データ登録シート名
    data_sheet_name = None
    # ワークブックオブジェクト
    workbook = None
    # ワークシートオブジェクト
    worksheet = None
    # データ書き込み行番号
    target_row = 1
    # データ書き込み列番号
    target_col = 0
    
    # 罫線スタイル
    side = Side(style='thin', color='000000')
    # 罫線設定
    border = Border(top=side, bottom=side, left=side, right=side)
    # 背景色
    fill = openpyxl.styles.PatternFill(patternType='solid', fgColor='BDD7EE', bgColor='BDD7EE')
    
    
    ########################################################################################
    # コンストラクタ
    # [概要]出力するワークブックオブジェクトを生成し、ワークブックオブジェクトに対してデータを登録するシートを追加します。
    # [引数1]ツールルートディレクトリパス
    # [引数2]ツール設定辞書オブジェクト
    ########################################################################################
    def __init__(self, root_dir_path, setting_dict):
        
        self.output_absolute_dir_name = os.path.join(root_dir_path, setting_dict['output_dir'])
        self.output_absolute_file_name = os.path.join(root_dir_path, setting_dict['output_dir'], setting_dict['output_file_name'])
        
        # テンプレートファイルを使用する場合はテンプレートファイルを読み込んでオブジェクトを生成する
        if setting_dict['use_template'] == "yes":
            self.workbook = openpyxl.load_workbook(os.path.join(root_dir_path,setting_dict['template_dir'],setting_dict['template_file_name']))
            self.use_template_flag = True
        else:
            self.workbook = openpyxl.Workbook()
            self.use_template_flag = False
        
        # PySimpleGUIでシート名を受け取る
        # PySimpleGUIウィンドウ設定
        PySimpleGUI.theme('Dark Blue 3')
        # 表示内容設定
        layout = [
            [PySimpleGUI.Text('データを登録するシート名を入力してください。')],
            [PySimpleGUI.Text('既存のシート名を入力するとシートを上書きします。')],
            [PySimpleGUI.Text('シート名', size=(31, 1)), PySimpleGUI.InputText('')],
            [PySimpleGUI.Submit(button_text='設定')]
        ]

        # PySimpleGUIウィンドウ表示
        window = PySimpleGUI.Window('', layout)
        
        while True:
            event, values = window.read()

            if event is None:
                break
            if event == '設定':
                self.data_sheet_name = values[0]
                break
                
        # PySimpleGUIウィンドウの破棄と終了
        window.close()
        
        # 入力されたシートをワークブックに加えるが、既存のシートの場合は事前に削除する。
        if (self.data_sheet_name in self.workbook.sheetnames):
            self.workbook.remove_sheet(self.data_sheet_name)
        self.workbook.create_sheet(self.data_sheet_name, len(self.workbook.sheetnames))
        
        # 書き込むワークシートを設定
        self.worksheet = self.workbook[self.data_sheet_name]    
        

    ########################################################################################
    # テーブル物理名/論理名書き込み処理
    # [概要]引数のテーブル物理名/論理名を格納したデータフレームの内容をエクセルに書き込みます。
    # [引数1]テーブル物理名/論理名データフレーム
    ########################################################################################
    def write_table_names(self, table_names_df):
        # セルのデコレーション
        self.decorate_cell(self.get_target_row_and_increment(True), 1, True)
        self.decorate_cell(self.get_target_row_and_increment(), 2)
        # 値設定
        self.worksheet.cell(row = self.get_target_row_and_increment(), column = 1).value = "テーブル物理名"
        self.worksheet.cell(row = self.get_target_row_and_increment(), column = 2).value = table_names_df.iloc[0, 0]
        # セルのデコレーション
        self.decorate_cell(self.get_target_row_and_increment(True), 1, True)
        self.decorate_cell(self.get_target_row_and_increment(), 2)
        # 値設定
        self.worksheet.cell(row = self.get_target_row_and_increment(), column = 1).value = "テーブル論理名"
        self.worksheet.cell(row = self.get_target_row_and_increment(), column = 2).value = table_names_df.iloc[0, 1]
        
        
    ########################################################################################
    # カラム物理名/論理名書き込み処理
    # [概要]引数のカラム物理名/論理名を格納したデータフレームの内容をエクセルに書き込みます。（カラム名の前に検索条件を書き出します）
    # [引数1]カラム物理名/論理名データフレーム
    # [引数2]クエリ条件辞書
    ########################################################################################
    def write_column_names(self ,column_names_df ,query_condition_dict):
        # セルのデコレーション
        self.decorate_cell(self.get_target_row_and_increment(True), 1, True)
        self.decorate_cell(self.get_target_row_and_increment(), 2)
        
        # カラム名の書き出し前に検索条件を書き出す
        self.worksheet.cell(row = self.get_target_row_and_increment(), column = 1).value = "検索条件"
        self.worksheet.cell(row = self.get_target_row_and_increment(), column = 2).value = query_condition_dict["where"]
        
        # カラム名の書き出し前に書き込む行数をインクリメントしておく
        self.get_target_row_and_increment(True)
        
        # カラム名をループで書き出す
        for index, row in column_names_df.iterrows():
            # セルのデコレーション
            self.decorate_cell(self.get_target_row_and_increment(), index + 1, True)
            self.decorate_cell(self.get_target_row_and_increment() + 1, index + 1, True)
            # カラムの論理名/物理名設定
            self.worksheet.cell(row = self.get_target_row_and_increment(), column = index + 1).value = row[2]
            self.worksheet.cell(row = self.get_target_row_and_increment() + 1, column = index + 1).value = row[1]
    
        # カラム名の書き出し後に書き込む行数をインクリメントしておく
        #（カラム名は論物名を書き込むために2行使うためインクリメントしないと値を書き込む時に整合性が取れなくなる）
        self.get_target_row_and_increment(True)
        
        
    ########################################################################################
    # 取得データ書き込み処理
    # [概要]引数の取得データを格納したデータフレームの内容をエクセルに書き込みます。
    # [引数1]取得データデータフレーム
    ########################################################################################
    def write_datas(self, datas_df):
        # データフレームを1行ずつ抜き出す
        for row_index, row in datas_df.iterrows():
            
            # 書き込み先の行番号をインクリメント
            self.get_target_row_and_increment(True)
            
            # 各列の値を書き込み
            col_index = 0
            for value in row:
                # セルのデコレーション
                self.decorate_cell(self.get_target_row_and_increment(), col_index + 1)
                # 値書き込み
                self.worksheet.cell(row = self.get_target_row_and_increment(), column = col_index + 1).value = value
                col_index = col_index + 1
                
        # 書き込み先の行番号をインクリメント
        self.get_target_row_and_increment(True)
            
        
    ########################################################################################
    # 書き込み行番号取得およびインクリメント処理
    # [概要]書き込み先の行番号を取得します。（引数がTrueであればクラス変数の「データ書き込み行番号」をインクリメントします。）
    # [引数1]インクリメントフラグ（default=False）
    ########################################################################################
    def get_target_row_and_increment(self, increment_flag = False):
        if increment_flag:
            self.target_row = self.target_row + 1
        return self.target_row
    
    ########################################################################################
    # セル装飾処理
    # [概要]引数の行番、列番のセルを枠付き、文字列に装飾します。タイトルカラムであれば背景も設定します。
    # [引数1]行番号
    # [引数2]列番号
    # [引数3]タイトルフラグ
    ########################################################################################
    def decorate_cell(self, row_number, col_number, title_flag = False):
        target_cell = self.worksheet.cell(row = row_number, column = col_number)
        # 罫線設定
        target_cell.border = self.border
        # 表示形式
        target_cell.number_format  = openpyxl.styles.numbers.FORMAT_TEXT
        # タイトルカラム用設定
        if title_flag:
            target_cell.fill = self.fill
        
    ########################################################################################
    # エクセルファイル保存処理
    # [概要]エクセルファイルを保存します。
    # [引数1]ツールルートパス
    # [引数2]ツール設定辞書
    ########################################################################################
    def save_excel_file(self):
        # フォルダを作成する
        os.makedirs(self.output_absolute_dir_name, exist_ok=True)
        
        # 新規ワークブックを作成した場合、不要なシートができてしまうため削除
        if self.use_template_flag == False:
            self.workbook.remove(self.workbook['Sheet'])
            
        self.workbook.save(self.output_absolute_file_name)
```
</details>

クラスの各メソッドのうち気になるメソッドだけ見ていくと。。。

### コンストラクタ

コンストラクタではエクセルのオブジェクトを生成したり、設定を読み込んだりとコンストラクタらしいことをしていますが、
データを書き込むシート名をGUIでユーザからの入力として受け取れるようにしてます。
PythonでGUIを作るにはTkInterしかないかなと思ってたんですが、調べたらPySimpleGUIっていうかなりシンプルなGUIを作れるモジュールがあったのでそちらを使ってます。
PySimpleGUIで作った入力ウィンドウはこんな感じです↓
![スクリーンショット 2023-02-14 082009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/d375dd30-2484-1cc5-6554-eea8ae156d1d.png)


```python
########################################################################################
    # コンストラクタ
    # [概要]出力するワークブックオブジェクトを生成し、ワークブックオブジェクトに対してデータを登録するシートを追加します。
    # [引数1]ツールルートディレクトリパス
    # [引数2]ツール設定辞書オブジェクト
    ########################################################################################
    def __init__(self, root_dir_path, setting_dict):
        
        self.output_absolute_dir_name = os.path.join(root_dir_path, setting_dict['output_dir'])
        self.output_absolute_file_name = os.path.join(root_dir_path, setting_dict['output_dir'], setting_dict['output_file_name'])
        
        # テンプレートファイルを使用する場合はテンプレートファイルを読み込んでオブジェクトを生成する
        if setting_dict['use_template'] == "yes":
            self.workbook = openpyxl.load_workbook(os.path.join(root_dir_path,setting_dict['template_dir'],setting_dict['template_file_name']))
            self.use_template_flag = True
        else:
            self.workbook = openpyxl.Workbook()
            self.use_template_flag = False
        
        # PySimpleGUIでシート名を受け取る
        # PySimpleGUIウィンドウ設定
        PySimpleGUI.theme('Dark Blue 3')
        # 表示内容設定
        layout = [
            [PySimpleGUI.Text('データを登録するシート名を入力してください。')],
            [PySimpleGUI.Text('既存のシート名を入力するとシートを上書きします。')],
            [PySimpleGUI.Text('シート名', size=(31, 1)), PySimpleGUI.InputText('')],
            [PySimpleGUI.Submit(button_text='設定')]
        ]

        # PySimpleGUIウィンドウ表示
        window = PySimpleGUI.Window('', layout)
        
        while True:
            event, values = window.read()

            if event is None:
                break
            if event == '設定':
                self.data_sheet_name = values[0]
                break
                
        # PySimpleGUIウィンドウの破棄と終了
        window.close()
        
        # 入力されたシートをワークブックに加えるが、既存のシートの場合は事前に削除する。
        if (self.data_sheet_name in self.workbook.sheetnames):
            self.workbook.remove_sheet(self.data_sheet_name)
        self.workbook.create_sheet(self.data_sheet_name, len(self.workbook.sheetnames))
        
        # 書き込むワークシートを設定
        self.worksheet = self.workbook[self.data_sheet_name]    
```

### 取得データ書き込み処理

この処理に限らずなんですが、
エクセルの操縦に使ってるopenpyxlっていうモジュールなんですが、1セルごとにしか操作ができないっぽい（なので行にまるまる背景色をつけるとかはできないよう）です。

DataFrameならそのままエクセルに貼り付けられないかなと期待を込めてDataFrameを引数にして後からDataFrameをそのままエクセルに貼り付けるメソッドがないか調べたんですが、それらしいものがなかったのであきらめてDataFrameの良さを全部殺してループを回してます。

一応openpyxlにappend?とかいうDataFrameやらをそのままエクセルに貼り付けるメソッドはあったんですが、リファレンスを見た感じ、貼り付けの開始位置を指定できなさそう（いやでもA1セルから貼り付けてしまいそう）だったので止めました。

```Python
  ########################################################################################
    # 取得データ書き込み処理
    # [概要]引数の取得データを格納したデータフレームの内容をエクセルに書き込みます。
    # [引数1]取得データデータフレーム
    ########################################################################################
    def write_datas(self, datas_df):
        # データフレームを1行ずつ抜き出す
        for row_index, row in datas_df.iterrows():
            
            # 書き込み先の行番号をインクリメント
            self.get_target_row_and_increment(True)
            
            # 各列の値を書き込み
            col_index = 0
            for value in row:
                # セルのデコレーション
                self.decorate_cell(self.get_target_row_and_increment(), col_index + 1)
                # 値書き込み
                self.worksheet.cell(row = self.get_target_row_and_increment(), column = col_index + 1).value = value
                col_index = col_index + 1
                
        # 書き込み先の行番号をインクリメント
        self.get_target_row_and_increment(True)
```


## DB検索およびワークブックアウトプット処理

ツールの要にもなるDB検索処理です。
一応トンネリングもできるようにしますが、トンネリングの動作は未確認です。

こちらの処理では
クエリ条件ファイルごとに

1. テーブル論理名検索&エクセルへの出力
2. カラム論理名検索＆エクセルへの出力
3. テーブルでデータ検索＆エクセルへの出力

をぶん回しているだけです。

```python
########################################################################################
# DB検索およびワークブックアウトプット処理
#[概要]DBに接続しクエリ条件ファイルに設定された条件をもとにテーブル論理名、カラム論理名、データの取得を行います。
# [引数1]ツール設定辞書
# [引数2]テンプレートSQL辞書
# [引数3]クエリ条件リスト
# [引数4]エクセル操縦クラス
# [戻り値]./sql_templates配下のSQLファイルを読みこんだ辞書オブジェクト        
########################################################################################

# モジュール読み込み
import psycopg2
import pandas
from sshtunnel import SSHTunnelForwarder

def query_and_output_excel(setting_dict, template_sql_dict, query_condition_list, operate_excel):
    
    # SSHトンネルオブジェクト
    ssh_tonnel = None
    # DB接続オブジェクト
    connection = None
    # カーソル
    cursor = None
    
    try:
        # SSHトンネリング
        if setting_dict["use_ssh_tonnel"] == "yes":
            ssh_tonnel = SSHTunnelForwarder((setting_dict['ssh_server_address'], int(setting_dict['ssh_server_port'])),
                                                ssh_host_key = setting_dict['ssh_host_key'] if setting_dict['ssh_host_key'] else None,
                                                ssh_username = setting_dict['ssh_user'],
                                                ssh_password = setting_dict['ssh_password'],
                                                ssh_pkey = setting_dict['ssh_pkey_file_path'] if setting_dict['ssh_pkey_file_path'] else None,
                                                remote_bind_address=(setting_dict['ssh_remote_bind_address'], int(setting_dict['ssh_remote_bind_port']))
                                            )
            # トンネリングスタート
            ssh_tonnel.start()
        
        # DB接続オブジェクト生成
        connection = psycopg2.connect(host = setting_dict['db_host'],
                                      port = setting_dict['db_port'],
                                      database = setting_dict['db_name'],
                                      user = setting_dict['db_user'],
                                      password = setting_dict['db_password'])
        cursor = connection.cursor()
        
        # クエリ条件リスト1要素ごとにクエリとエクセルファイルへの出力を実行
        for query_condition_dict in query_condition_list:
            # 1.テーブル論理名取得およびエクセルファイル出力
            cursor.execute(template_sql_dict['get_table_logical_name'], [query_condition_dict['schema'], query_condition_dict['table']])
            table_names_df = pandas.DataFrame(cursor.fetchall())
            operate_excel.write_table_names(table_names_df)
            
            # 2.テーブルカラム名取得およびエクセルファイル出力
            cursor.execute(template_sql_dict['get_column_logical_names'], [query_condition_dict['schema'], query_condition_dict['table']])
            column_names_df = pandas.DataFrame(cursor.fetchall())
            operate_excel.write_column_names(column_names_df, query_condition_dict)
            
            # 3.データ取得およびエクセルファイル出力
                # カラム名をカンマ区切りの文字列に変形してデータ取得用SQLのカラム名を置換
            get_datas_sql = template_sql_dict['get_datas']
            get_datas_sql = get_datas_sql.replace(":column_names", ", ".join(column_names_df.iloc[:,1].tolist()))
            
                # データ取得用SQLのテーブル名を検索対象のテーブル名に置換
            get_datas_sql = get_datas_sql.replace(":table_name", query_condition_dict['table'])
            
                # データ取得用SQLのwhere句を指定された検索条件に置換
            if query_condition_dict['where']:
                get_datas_sql = get_datas_sql.replace(":condition", "and " + query_condition_dict['where'])
            else:
                get_datas_sql = get_datas_sql.replace(":condition", "")
                
                #データ取得
            cursor.execute(get_datas_sql)
            datas_df = pandas.DataFrame(cursor.fetchall())
                # エクセルファイル出力
            operate_excel.write_datas(datas_df)
            
            
    finally:
        # DB切断
        cursor.close()
        connection.close()
        
        # トンネルクローズ
        if setting_dict["use_ssh_tonnel"] == "yes":
            ssh_tonnel.stop()

```


## main

これまで紹介した各処理を呼び出してます。
エラーが発生した場合はエラー内容をPySimpleGUIで表示します。

所詮は効率化のツールなのでエラーハンドリングにはあまりこだわってないです。

実際にエラーを起こすとこんな感じ↓です。
![err.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/8244fd09-9aee-3239-a993-ad20c3449757.png)


```py
########################################################################################
# main
########################################################################################

# モジュール
import PySimpleGUI
import traceback

# Jupyter用の設定
root_dir_path = "C:\\ssknok_work\\05_development\\get_datas_for_evidence_tool"

try:
    # ツール設定ファイル読み込み
    setting_dict = read_tool_settings(root_dir_path)

    # クエリ条件ファイル読み込み
    query_condition_list = read_query_conditions(root_dir_path)

    # エクセルオブジェクト生成
    operate_excel = operate_excel(root_dir_path, setting_dict)

    # テンプレートSQLファイル読み込み
    template_sql_dict = read_template_sqls(root_dir_path)

    # クエリ実行
    query_and_output_excel(setting_dict, template_sql_dict, query_condition_list ,operate_excel)

    # エクセル保存
    operate_excel.save_excel_file()

    
# エラッた場合はGUIで表示
except Exception as e:
    
    # PySimpleGUIウィンドウ設定
    PySimpleGUI.theme('Dark Blue 3')
    
    # 表示内容設定
    layout = [
        [PySimpleGUI.Text('type:' + str(type(e)))],
        [PySimpleGUI.Text('args:' + str(e.args))],
        [PySimpleGUI.Text('trace:' + traceback.format_exc())],
        [PySimpleGUI.Submit(button_text='OK')]
    ]

    # PySimpleGUIウィンドウ表示
    window = PySimpleGUI.Window('ERROR', layout)

    while True:
        event, values = window.read()

        if event is None:
            break
        if event == 'OK':
            break

    # PySimpleGUIウィンドウの破棄と終了
    window.close()
``` 

# exe化
ここまでで紹介したソースを実行環境ごとexeとして固めて、python環境を持たないユーザでもツールを使えるようにします。

`pyinstaller`を利用して簡単にpyファイルをexe化します。
exe化するデメリットとして、処理が若干遅くなったり、exeファイルは結構デカめのサイズになったりってなことはありますが、とはいえ、python環境をわざわざ作らせるのも手間なので。

pyinstallerを使えば、pyファイルを簡単にexe化できますが、何も考えずにpyinstallerを実行するとローカルのリポジトリ（って言い方でいいのか？）にあるモジュールがすべてexeに固められてしまうそうなので

先にpipenvで独立した環境を作っておいてから、その独立した環境でpyinstallerを実行してexeを作ります。

pipenvの使い方は[こちら](https://qiita.com/y-tsutsu/items/54c10e0b2c6b565c887a)の記事を参考にしました。
pyinstallerの使い方は[こちら](https://camp.trainocate.co.jp/magazine/pyinstaller-python-exe/)の記事を参考に。

# 挙動の確認
最後に挙動の確認をします。
今回はexeバージョンのツールを使います。

## ツール設定ファイル
テンプレートファイルは使わずに新規ファイルを作成する設定にします。

```
#######################################################
# ツール設定ファイル
#######################################################
#各設定は「[設定項目名]=[設定値]」で設定すること

# テンプレートファイル利用(yes, no)
use_template=no
# テンプレートディレクトリ（exeファイルからの相対パスで指定）
template_dir=template
# テンプレートファイル（拡張子までつけること）
template_file_name=template.xlsx

# 出力ディレクトリ（exeファイルからの相対パスで指定）
output_dir=output
# 出力ファイル名（拡張子までつけること）
output_file_name=test.xlsx

# DBホスト名
db_host=localhost
# DBポート番号
db_port=5432
# データベース名
db_name=postgres
# DBユーザ名
db_user=postgres
# DBパスワード
db_password=postgres

# DB接続の際のSSHトンネル利用(yes, no)
use_ssh_tonnel=no
# SSHサーバーアドレス
ssh_server_address=
# SSHサーバーポート
ssh_server_port=
# SSHホストキー
ssh_host_key=
# SSHユーザ名
ssh_user=
# SSHパスワード
ssh_password=
# SSH鍵ファイルパス
ssh_pkey_file_path=
# SSHサーバから見た接続先サーバのアドレス
ssh_remote_bind_address=
# SSHサーバから見た接続先サーバのポート
ssh_remote_bind_port=
```

## クエリ設定ファイル
事前に用意しておいた`user_master`、`pet_master`というてきとうに作っておいたテーブルからデータを取得します。

```
#######################################################
# クエリ設定ファイル
#######################################################
#各設定は「スキーマ,テーブル名,検索条件およびソート」を全て物理名で設定すること

public,user_master,user_id = '1'
public,pet_master,
```

## 実行

main.exeというpyinstallerで固めたツール本体を実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/6d54b91f-23f7-256d-7cba-3f9b1ad2170e.png)

シート名を入力するように言われるので適当に入力。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/d81a0f86-6ef7-6650-7659-88b41d69e720.png)

するとexcelファイルが生成されます。
やろうやろうと思って何もしてないんですが、このツール正常終了時にwindowが出たりとか何も合図がないのでツールが正常終了したのかさっぱりわからないという。。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/8663af30-8e3a-f0fb-957d-3b827fb908a6.png)

中身を確認するとこんな感じでデータが取れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/a31b4d7e-07c7-50f7-8c4f-5724552355d9.png)