【Python × tkinter】マッチした文字列だけを抽出！正規表現GUIツールを作ってみた

# 1. はじめに

:::note info
開発〜記事執筆まで，ChatGPT-4oに相談しながら作りました． 
:::

## 背景

正規表現を使った「検出」や 検出したものを「整形」するのは多くのツールでできますが，「**マッチした文字列だけを残し，それ以外を削除する**」という用途に特化したツールは自分が探した限りでは見当たりませんでした．

CTFやデータ前処理で「これだけ抜き出せれば…」という場面が多々あり，GUIで手軽に試せるツールが欲しいと思い作ってみました！

## こんなときに便利！

以下のような場面で活躍します

- CTFやセキュリティ演習で，特定の形式（例：`FLAG{.*}`）だけを抽出したい
- ログファイルから日付やエラーメッセージだけを抜き出したい
- テキストデータからURLやメールアドレスをリスト化したい
- テストデータの整形や事前チェックで，マッチした部分だけを見たい

「マッチしたものだけを残して，他は消す」という処理は，意外と既存のツールではやりにくく，ちょっとした用途に非常に便利です．

# 2. 完成イメージ


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4051745/0672bc56-c8d9-455a-8043-52ecbd8bd0b0.png)


# 3. Info（使用環境・構成など）
- ✅ 実行環境：macOS（※Windowsでも基本的に動作すると思われます）
- 🐍 Pythonバージョン：3.13.2 (64bit)
- 📦 使用ライブラリ：`tkinter`, `re`（すべて標準ライブラリ）
- ⚙️ 実行方法：`$ python UI.py` を実行するだけ！
- 📎 ファイル構成： 
    ```markdown
    ├── UI.py           # GUIメイン部分
    └── callback.py     # コールバック関数
    ```

# 4. ソースコード紹介

<details>
  <summary>
    GUIプログラム
  </summary>
  
```UI.py
import tkinter as tk
from callback import *


#------------------------------------#
#           ウィンド作成               #
#------------------------------------#
root = tk.Tk()
root.geometry("600x500")
root.title("ClipMatch")

#------------------------------------#
#          　 BASEフレーム    　       #
#------------------------------------#

# --- 縦方向の比率を設定（4:1:4） --- #
root.rowconfigure(0, weight=2)  # 入力欄フレーム
root.rowconfigure(1, weight=1)  # 正規表現欄フレーム
root.rowconfigure(2, weight=2)  # 出力欄フレーム
root.columnconfigure(0, weight=1)  # 横方向も伸ばす
input_label = tk.Label(text="入力欄")

# --- フレーム定義 --- #
input_frame = tk.Frame(root, bg='lightblue')
regex_frame = tk.Frame(root, bg='lightgray')
output_frame = tk.Frame(root, bg='lightgreen')

# --- フレーム配置（grid） --- #
input_frame.grid(row=0, column=0, sticky="nsew")
regex_frame.grid(row=1, column=0, sticky="nsew")
output_frame.grid(row=2, column=0, sticky="nsew")




#------------------------------------#
#          　 input_flame   　        #
#------------------------------------#

# --- input_frame の中も比率設定 --- #
input_frame.rowconfigure(0, weight=0)  # 上段（ラベル・ボタン）は高さ固定
input_frame.rowconfigure(1, weight=3)  # 下段（Text）は縦に伸びる
input_frame.columnconfigure(0, weight=1)  # 左ラベル
input_frame.columnconfigure(1, weight=1)  # 右ボタン

# --- 上部：左にラベル、右にクリアボタン --- #
label_input = tk.Label(input_frame, text="入力欄", anchor="w", bg="white")
label_input.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)


clear_button = tk.Button(input_frame, text="クリア", command=lambda: clear(input_text, output_text))
clear_button.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)


# --- 下部：Text（テキスト入力） --- #
input_text = tk.Text(input_frame, height=4)
input_text.grid(row=1, column=0, columnspan=2, sticky="nsew", padx=10, pady=10)





#------------------------------------#
#           output_flame   　        #
#------------------------------------#

# --- input_frame の中も比率設定 --- #
regex_frame.rowconfigure(0, weight=0)  # 上段（ラベル・ボタン）は高さ固定
regex_frame.rowconfigure(1, weight=3)  # 下段（Text）は縦に伸びる
regex_frame.columnconfigure(0, weight=1)  # 左ラベル
regex_frame.columnconfigure(1, weight=1)  # 右ボタン

# --- 上部：左にラベル、右にクリアボタン --- #
label_regex = tk.Label(regex_frame, text="正規表現入力", anchor="w", bg="white")
label_regex.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)


run_regex_button = tk.Button(regex_frame, text="抽出実行", command=lambda: run_regex(input_text, regex_text, output_text))
run_regex_button.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)


# --- 下部：Text（テキスト入力） --- #
regex_text = tk.Text(regex_frame, height=2)
regex_text.grid(row=1, column=0, columnspan=2, sticky="nsew", padx=10, pady=10)





#------------------------------------#
#           output_flame   　        #
#------------------------------------#

# --- input_frame の中も比率設定 --- #
output_frame.rowconfigure(0, weight=0)  # 上段（ラベル・ボタン）は高さ固定
output_frame.rowconfigure(1, weight=3)  # 下段（Text）は縦に伸びる
output_frame.columnconfigure(0, weight=1)  # 左ラベル
output_frame.columnconfigure(1, weight=1)  # 右ボタン

# --- 上部：左にラベル、右にクリアボタン --- #
label_output = tk.Label(output_frame, text="出力欄", anchor="w", bg="white")
label_output.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)


copy_button = tk.Button(output_frame, text="コピー", command=lambda: copy(output_text, root))
copy_button.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)


# --- 下部：Text（テキスト入力） --- #
output_text = tk.Text(output_frame, height=4, state="disabled")
output_text.grid(row=1, column=0, columnspan=2, sticky="nsew", padx=10, pady=10)




root.mainloop()
```


</details>


<details>
  <summary>
    コールバック関数
  </summary>

```callback.py
import re

def clear(input_widget, output_widget):
    input_widget.delete("1.0", "end")
    
    output_widget.config(state="normal")         # 編集可能にして
    output_widget.delete("1.0", "end")
    output_widget.config(state="disabled")       # 編集禁止に戻す


def copy(text_widget, root):
    root.clipboard_clear()                   # クリップボードを一旦クリア
    root.clipboard_append(text_widget.get("1.0", "end"))  # テキストを追加
    root.update()  # クリップボードの内容を即座に反映（これがないと動かない場合あり）



def run_regex(input_widget, regex_widget, output_widget):
    output_widget.config(state="normal")         # 編集可能にして
    
    # 入力文字列と正規表現パターンを取得
    input_str = input_widget.get("1.0", "end").strip()
    pattern = regex_widget.get("1.0", "end").strip()

    # 空チェック（必要なら）
    if not input_str or not pattern:
        output_widget.delete("1.0", "end")
        output_widget.insert("1.0", "入力欄または正規表現が空です。")
        output_widget.config(state="disabled")       # 編集禁止に戻す
        return

    try:
        # 正規表現によるマッチ
        matches = re.findall(pattern, input_str)

        # 出力欄を更新
        output_widget.delete("1.0", "end")
        if matches:
            output_widget.insert("1.0", "\n".join(matches))
        else:
            output_widget.insert("1.0", "一致するものが見つかりませんでした。")

    except re.error as e:
        # 正規表現の構文エラーなどを表示
        output_widget.delete("1.0", "end")
        output_widget.insert("1.0", f"正規表現エラー: {e}")
    finally:
        output_widget.config(state="disabled")       # 編集禁止に戻す
```

</details>

# 5. 使い方

## 使い方
1. 処理対象の文字列を入力
    ![usage1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4051745/253fd232-f3a9-4f6a-935d-a1342659c715.png)

1. 抽出したい文字列にマッチする正規表現を入力（今回はメールアドレス抽出）
    ![usage2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4051745/e771f8af-3684-40ce-96e8-b6cad6645cd2.png)

1. 「抽出実行」ボタンを押す
    ![usage3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4051745/aaa86337-ca0f-4fed-bfb1-fac3105fea12.png)


1. 出力
    ![usage4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4051745/6af08448-314c-42c4-b0f5-c63346e2ff9b.png)



## その他の機能
- 水色の領域にある「クリア」ボタンを押すと，入力欄と出力欄のテキスト部分がクリアされます．
- 緑色の領域にある「コピー」ボタンを押すと，出力結果をクリップボードにコピーすることができます．

## テストデータ
- 入力
    ```markdown
    名前：田中太郎
    電話番号：090-1234-5678
    メール：tanaka.taro@example.com
    
    こんにちは！今日は2025/04/08です。
    この文章にはURLも含まれています：http://example.com/test?key=value
    あと、郵便番号：123-4567 や価格 ￥1,200 も入れておきます。
    
    別の人：
    名前：山田花子
    電話番号：080-9876-5432
    メール：hanako.yamada@example.co.jp
    ```

- 良く使う正規表現パターン
    - 📧メールアドレス
        ```markdown
        [\w\.-]+@[\w\.-]+\.\w+
        ```
    
    - 🌐URL
        ```markdown
        https?://[^\s]+
        ```
    
    - 📅 日付（YYYY/MM/DD）
        ```markdown
        \d{4}/\d{2}/\d{2}
        ```
    
    - 📍郵便番号（日本）
        ```markdown
        \d{3}-\d{4}	
        ```
    
    - 📱 電話番号（日本）
        ```markdown
        0\d{1,4}-\d{1,4}-\d{4}
        ```


# 6. 苦労した点・工夫した点

UIの開発経験がほとんどなく，`tkinter`ライブラリの構文にも不慣れな状態からのスタートでした．

「フレームの上にラベルやボタン配置をする」という基本的な概念すら曖昧で，最初は何をどう書けば良いかもわからない状況でした．

体体系的に知識を入れてから着手するのも一つの方法だと思いますが，今回は「まず動かしてみる」「実現したいものを形にする」ことを重視しました．

実現したいことのサンプルプログラムを調べたり，Chat GPTに生成してもらい，それを動かしながら修正を加えていく中で，少しずつ関数の役割やパラメータ，仕組みを理解できるようになりました．

「やりたいことはあるけれど、やり方がわからない」という場面に出会ったとき，都度調べながら進める経験は，より理解が定着しやすくなると思います．

今回の経験を通して，**とりあえず触ってみることの大切さ**を改めて感じました．


# 7. おわりに・課題

現時点では，ご覧の通り機能が少ないことやデザイン性の低さが目立ちます．

また，今回の実装では**正規表現マッチの結果に重複があってもフィルタリングされない**など，実用面で不十分な点が多く残っています．

「プロトタイプ」と考えれば個人的には許容でありますが，実際に使っていくとなれば不十分な点・課題はたくさんあります．

今後より実用的なツールにしていくためには

- 重複の除去
- マッチ結果の保存機能（CSV出力など）
- 見た目の改善（テーマやフォント調整）

といった機能や改善が必要だと感じています．

今後も改良を重ねながら，使いやすく実用性の高いツールに育てていければと思います．

<br>

最後までお読みいただき，ありがとうございました！