[備忘録] GradioでExcel風インターフェースを実装してみる

GradioでExcel風インターフェースの画面を実装してみたの備忘録です。

細かい機能や操作性はまだ改善の余地がありますが、データテーブルで情報を確認・編集したり、エクスポート・インポートしたりするユースケースに活用できそうです。完全にExcelと互換というわけではありませんが、シンプルなデータ操作ツールとして機能します。このような操作したいケースもあるのでは・・・？

## 画面例

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/ef22006d-3d91-4eaa-862a-2eb03d6cfbc9.png)

テーブルのセルの値を編集することができる
行、列の追加、削除もできる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/8637bf3a-dceb-48e6-a0df-1059269c87a6.png)

ファイルを読み込んだときの例、ファイル出力もある


## 環境準備

```bash
pip install "gradio>=3.15,<4" pandas openpyxl
```

## 実装

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/098aa171-a636-45d0-a660-81fd545c1272.png)

```python
import gradio as gr
import pandas as pd
import os
import tempfile
import uuid

# サンプルデータ作成
def create_sample_data():
    data = {
        '商品名': ['製品A', '製品B', '製品C', '製品D', '製品E'],
        '価格': [1000, 2000, 1500, 3000, 2500],
        '在庫数': [10, 5, 15, 8, 12],
        '入荷日': ['2025-01-01', '2025-01-15', '2025-02-01', '2025-02-15', '2025-03-01']
    }
    df = pd.DataFrame(data)
    # 日付列を適切なデータ型に変換
    df["入荷日"] = pd.to_datetime(df["入荷日"]).dt.date
    return df

def excel_like_app():
    with gr.Blocks() as app:
        gr.Markdown("# GradioによるExcel風インターフェース")
        
        # 状態管理（元データとフィルタリング結果を保持）
        state = gr.State(value={
            "data": create_sample_data(),
            "filtered_data": None
        })
        
        # ファイル操作UI
        with gr.Row():
            file_input = gr.File(label="インポート", type="filepath")
            import_btn = gr.Button("読み込む")
            export_btn = gr.Button("エクスポート")
            file_output = gr.File(label="エクスポート結果")
        
        # フィルタリング・ソートUI
        with gr.Row():
            with gr.Column(scale=1):
                filter_col = gr.Dropdown(choices=create_sample_data().columns.tolist(), label="フィルター列")
                filter_val = gr.Textbox(label="フィルター値")
                apply_filter = gr.Button("フィルター適用")
                reset_filter = gr.Button("リセット")
            
            with gr.Column(scale=1):
                sort_col = gr.Dropdown(choices=create_sample_data().columns.tolist(), label="並べ替え列")
                sort_asc = gr.Checkbox(label="昇順", value=True)
                apply_sort = gr.Button("並べ替え適用")
        
        # データフレーム
        df = gr.Dataframe(
            value=create_sample_data(),
            datatype=["str", "number", "number", "date"],
            interactive=True
        )
        
        # 機能説明
        with gr.Accordion("使い方", open=False):
            gr.Markdown("""
            - **編集**: セルをクリックして直接編集（右クリックで行の追加・削除なども可能）
            - **フィルタリング**: 列と値を指定して「フィルター適用」
            - **並べ替え**: 列と順序を指定して「並べ替え適用」
            - **インポート/エクスポート**: Excelファイルの読み込み・保存
            """)
        
        # フィルタリング関数
        def filter_data(state, column, value):
            if not column or not value:
                return state["data"], state
            
            df = state["data"]
            filtered_df = df[df[column].astype(str).str.contains(value, case=False)]
            state["filtered_data"] = filtered_df
            return filtered_df, state
        
        # フィルターリセット関数
        def reset_filter_data(state):
            state["filtered_data"] = None
            return state["data"], state
        
        # 並べ替え関数
        def sort_data(state, column, ascending):
            if not column:
                return state["data"], state
                
            df = state["filtered_data"] if state["filtered_data"] is not None else state["data"].copy()
            sorted_df = df.sort_values(by=column, ascending=ascending)
            
            if state["filtered_data"] is not None:
                state["filtered_data"] = sorted_df
            
            return sorted_df, state
        
        # エクスポート関数
        def export_to_excel(df):
            if df is None or len(df) == 0:
                return None
                
            # 一時ファイル作成（ユニークなファイル名）
            temp_dir = tempfile.gettempdir()
            unique_id = uuid.uuid4().hex[:8]
            output_path = os.path.join(temp_dir, f"excel_export_{unique_id}.xlsx")
            
            # Excelファイルに保存
            df.to_excel(output_path, index=False)
            
            return output_path
        
        # インポート関数
        def import_from_excel(file):
            if file is None:
                return create_sample_data(), {"data": create_sample_data(), "filtered_data": None}
                
            try:
                df = pd.read_excel(file)
                return df, {"data": df, "filtered_data": None}
            except Exception as e:
                print(f"インポートエラー: {e}")
                return create_sample_data(), {"data": create_sample_data(), "filtered_data": None}
        
        # イベントハンドラーの設定
        apply_filter.click(
            fn=filter_data,
            inputs=[state, filter_col, filter_val],
            outputs=[df, state]
        )
        
        reset_filter.click(
            fn=reset_filter_data,
            inputs=[state],
            outputs=[df, state]
        )
        
        apply_sort.click(
            fn=sort_data,
            inputs=[state, sort_col, sort_asc],
            outputs=[df, state]
        )
        
        export_btn.click(
            fn=export_to_excel,
            inputs=[df],
            outputs=[file_output]
        )
        
        import_btn.click(
            fn=import_from_excel,
            inputs=[file_input],
            outputs=[df, state]
        )
    
    return app

# アプリケーション起動
if __name__ == "__main__":
    app = excel_like_app()
    app.launch()
```

## 解説

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/5485130c-bf17-4229-8c16-bca9a144d574.png)

### 1. 基本構造
このアプリはGradioの`Blocks`インターフェースを使用して構築されています。主要コンポーネントは以下の通りです：

- **インタラクティブなDataframe**: `interactive=True`を設定することでセル編集が可能になります
- **状態管理**: `gr.State`を使って元データとフィルタリング結果を保持します
- **UI要素**: ファイル操作、フィルタリング、ソート用のコントロールを配置しています

### 2. 主要機能の実装ポイント

#### フィルタリング機能
フィルタリング機能は、特定の列の値が指定された文字列を含むかどうかを確認します。`filtered_data`という状態変数に結果を保存することで、複数の操作（例：フィルタリング後のソート）を可能にしています。

#### ソート機能
ソート機能は、現在表示されているデータ（フィルタリング済みまたは元データ）を指定された列でソートします。

#### ファイル入出力
- **エクスポート**: 現在のデータフレームをExcelファイルとして保存します
- **インポート**: Excelファイルからデータを読み込み、データフレームと状態を更新します

#### データ編集
セルを直接クリックして編集できます。また、Dataframeコンポーネントの右クリックメニューから行の追加、削除、コピーなどの操作が可能です。

### 3. 実装上の注意点

- **Gradioのバージョン**: 3.15以降で`interactive=True`が安定して動作します
- **データ型の設定**: `datatype=["str", "number", "number", "date"]`でUIの挙動を最適化
- **状態管理の設計**: `gr.State`を使った状態管理が、複合操作のキーポイントです
- **エラー処理**: 特にファイル操作時には適切なエラー処理が重要です
- **UI設計**: `gr.Row`や`gr.Column`でレイアウトを調整できます

このシンプルな実装をベースに、独自の機能を追加したり、より複雑なデータ処理を組み込んだりすることが可能です。

## 参考情報

https://huggingface.co/blog/gradio-dataframe-upgrade

https://www.gradio.app/docs/gradio/dataframe

https://github.com/gradio-app/gradio/blob/main/gradio/components/dataframe.py

## 免責事項
本記事の作成にあたり、文章・図解の生成に生成AIを活用しました。最終的な編集と確認は筆者が行っています。