NeeNetです。
今回はPySimpleGUIを利用して「Parquet Viewer」というGUIツールを作成してみたいと思います。

はじめに
Apache Parquet は、効率的なデータの保存と検索のために設計された、オープンソースの列指向データファイル形式です。
データ分析基盤におけるファイルの保存形式として、Parquetファイルを選択するケースは多いかと思います。
Windows・Mac共にParquetファイルを表示するためのフリーソフトは種々存在しますが、所属される組織によってはフリーソフトのダウンロードが禁止されていたり、CUIツールやVSCodeの拡張機能で表示できる形式ではデータが見辛い等々使い勝手に問題のあるケースもあるので、今回はPySimpleGUIを利用して自作してみたいと思います。

環境
今回利用するPythonの標準モジュール以外のモジュールは以下の2つです。
- pandas
- PySimpleGUI
何れもpipによりインストールすることができます。
また、Pythonでの実行になるのでWindows・Mac・Linuxに対応していますが、今回はMacで実行します。
スクリプト内容
早速ですが、全体のスクリプトは以下の通りです。
import os
import queue
import threading
import pandas as pd
import PySimpleGUI as sg
df_queue = queue.Queue()
def set_dpi_awareness():
    try:
        from ctypes import windll
        windll.shcore.SetProcessDpiAwareness(1)
    except:
        pass
def load_dataframe(filepath, window):
    try:
        df = pd.read_parquet(filepath)
        df_queue.put(df)
        window.write_event_value("-THREAD-", "LOAD_SUCCESS")
    except:
        window.write_event_value("-THREAD-", "LOAD_ERROR")
def create_window(layout, size=None):
    window_arguments = {
        "title": "Parquet Viewer",
        "layout": layout,
        "finalize": True,
        "font": ("Meiryo UI", 9),
        "margins": (20, 20),
    }
    if size:
        window_arguments["size"] = size
    return sg.Window(**window_arguments)
def create_layout(header_list=None, data=None):
    layout = [
        [
            sg.InputText(key="-INPUT_FILE-", visible=False, enable_events=True),
            sg.FileBrowse(
                "ファイル選択",
                key="-FILE_BROWSE-",
                initial_folder="./",
                file_types=(("Parquet Files", "*.parquet"),),
                size=(20, 0),
            ),
        ],
    ]
    if header_list and data:
        layout[0].append(sg.Button("csvで保存", key="-SAVE_CSV-", size=(20, 0)))
        layout.append(
            [
                sg.Table(
                    values=data,
                    headings=header_list,
                    key="-TABLE-",
                    display_row_numbers=True,
                    auto_size_columns=True,
                    num_rows=min(25, len(data)),
                    vertical_scroll_only=False,
                    def_col_width=5,
                )
            ]
        )
    return layout
def save_csv(filepath, df):
    try:
        dirname, _ = os.path.split(filepath)
        basename_without_ext = os.path.splitext(os.path.basename(filepath))[0]
        save_path = os.path.join(dirname, f"{basename_without_ext}.csv")
        df.to_csv(save_path, index=False)
        window.write_event_value("-THREAD-", "SAVE_SUCCESS")
    except:
        window.write_event_value("-THREAD-", "SAVE_ERROR")
def show_window(title, message):
    sg.Window(
        title,
        [[sg.T(message)], [sg.Button("OK")]],
        disable_close=False,
    ).read(close=True)
if __name__ == "__main__":
    # 高DPIに対応(Windows用)
    set_dpi_awareness()
    sg.theme("SystemDefault1")
    window = create_window(create_layout())
    df = None
    filepath = None
    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break
        elif event == "-INPUT_FILE-":
            filepath = values["-INPUT_FILE-"]
            threading.Thread(
                target=load_dataframe,
                args=(filepath, window),
                daemon=True,
            ).start()
        elif event == "-THREAD-":
            if values["-THREAD-"] == "LOAD_SUCCESS":
                window.close()
                df = df_queue.get()
                window = create_window(
                    create_layout(df.columns.values.tolist(), df.values.tolist()),
                    size=(600, 300),
                )
            elif values["-THREAD-"] == "LOAD_ERROR":
                title = "Error"
                message = "ロードでエラーが発生しました"
                show_window(title, message)
            elif values["-THREAD-"] == "SAVE_SUCCESS":
                title = "Success"
                message = f"csvファイルへの変換が完了しました"
                show_window(title, message)
            elif values["-THREAD-"] == "SAVE_ERROR":
                title = "Error"
                message = f"csvファイルへの変換でエラーが発生しました"
                show_window(title, message)
        elif event == "-SAVE_CSV-":
            threading.Thread(
                target=save_csv,
                args=(filepath, df),
                daemon=True,
            ).start()
本GUIツールには以下の機能が含まれています。
- Parquetファイルの読み込み、表示する機能
- csvファイルに変換して保存する機能
スレッド処理について
メインスレッドで画面の描画を行い、Parquetファイルのロード処理やcsvファイルに変換して保存する処理や別スレッドで行っています。
これは、比較的大きなParquetファイルを読み込む際に、画面の描画を担うメインスレッドが止まってしまわないようにするためです。
Parquetファイルの読み込みや保存についてはpandasに備わっている関数を利用しています。
なお、保存場所については元のParquetファイルがあるフォルダに <元のファイル名>.csv で保存するような仕様にしています。
ロードやcsvファイルの変換の過程でエラーが生じた場合はその旨をメッセージで表示する仕様としています。
Windowsの対応について
本スクリプトではset_dpi_awareness関数にてWindowsの高DPI設定にも対応をしております。
これにより、解像度の高い端末でGUIツールの解像度が低下してしまうことを防いでいます。
MacやLinuxでは影響を受けないようにしています。
実行画面
まずプログラムを実行すると、以下のようなファイル選択ボタンのみがある画面が表示されます。

Parquetファイルを選択すると、冒頭で示したようにデータが表形式で表示されます。

再度ファイル選択ボタンを押すことで、別のParquetファイルを読み込むことも可能です。(前のデータは表から削除されます)
Parquetファイル以外のファイルを選択するなど、ロードでエラーが発生した場合はその旨が表示されます。

csvで保存ボタンを押すと、csvファイルでデータが保存されます。

最後に
今回はPySimpleGUIを利用して「Parquet Viewer」というGUIツールを作成してみました。
個人的に最低限必要だと思った機能だけ盛り込みましたが、他にも「検索機能でデータを絞る」といった機能も作り込めば可能かと思いますので、PySimpleGUIを用いたGUIアプリケーション開発に挑戦してみてはいかがでしょうか。
本記事が参考になりましたら幸いです。
ご依頼について

NeeNetではPythonを用いたGUIアプリケーション開発のご依頼・ご相談をお引き受けしております。
個人・法人問わず、何かご相談事項がございましたら、一度ご連絡いただければと思います。

