Pythonで学ぶテキスト匿名化ツールの作り方 - GUIアプリケーション開発入門

## はじめに

こんにちは！今回は、Pythonを使って日本語テキストの匿名化ツールを作る方法を紹介します。このツールは、テキスト内のメールアドレスと固有名詞を検出し、匿名化する機能を持つGUIアプリケーションです。

## 注意事項

**このコードは学習目的で作成されたものです。実際の業務や重要なデータの処理には使用しないでください。コードの品質や匿名化の完全性は保証されていません。**

## 要件

このプロジェクトでは、以下の要件を満たすアプリケーションを作成します：

1. 日本語テキストからメールアドレスと固有名詞を抽出する
2. 抽出された項目を一覧表示し、ユーザーが選択できるようにする
3. 選択された項目を匿名化（置換）する
4. 匿名化されたテキストを元に戻す機能を提供する
5. GUIで操作可能にする

## 仕様

- メールアドレスの抽出には正規表現を使用
- 固有名詞の抽出にはJanomeライブラリを使用
- GUIの作成にはPyQt5を使用
- 抽出された項目はデフォルトで全て選択状態とし、一括選択/解除も可能に
- 匿名化と元に戻す操作を別々に行えるようにする

## 画面構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/6b0868c4-f6c1-3688-05d9-c209f8eac879.png)

このアプリケーションの画面は、以下の要素で構成されています：

1. **入力テキストエリア**
   - ラベル: "テキストを入力してください:"
   - 機能: ユーザーが匿名化したいテキストを入力する場所

2. **「メールアドレスと固有名詞を抽出」ボタン**
   - 機能: クリックすると入力テキストからメールアドレスと固有名詞を抽出し、下のテーブルに表示

3. **「全選択/解除」ボタン**
   - 機能: テーブル内の全てのチェックボックスを一括でON/OFFする

4. **抽出項目テーブル**
   - 列: ["選択", "項目", "置換後"]
   - 機能: 
     - 「選択」列: チェックボックスで項目の選択/解除を行う
     - 「項目」列: 抽出されたメールアドレスや固有名詞を表示
     - 「置換後」列: 匿名化後の文字列を表示（編集可能）

5. **「選択した項目を匿名化」ボタン**
   - 機能: チェックされた項目を匿名化し、結果を出力テキストエリアに表示

6. **出力テキストエリア**
   - ラベル: "匿名化されたテキスト:"
   - 機能: 匿名化されたテキストを表示（読み取り専用）

7. **元に戻すテキストエリア**
   - ラベル: "元に戻すテキスト:"
   - 機能: 匿名化されたテキストを表示し、ここで元に戻す操作を行う

8. **「選択した項目を元に戻す」ボタン**
   - 機能: チェックされた項目を元の表現に戻し、結果を元に戻すテキストエリアに表示

### 画面の使い方

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/c16ee681-43fd-db7a-a51a-25f01a8a4d1d.png)

1. 入力テキストエリアにテキストを入力します。
2. 「メールアドレスと固有名詞を抽出」ボタンをクリックして項目を抽出します。
3. 抽出項目テーブルで、匿名化したい項目を選択します（デフォルトは全選択）。
   - 「全選択/解除」ボタンで一括操作も可能です。
   - 必要に応じて「置換後」列の内容を編集できます。
4. 「選択した項目を匿名化」ボタンをクリックして匿名化を実行します。
5. 匿名化されたテキストが出力テキストエリアと元に戻すテキストエリアに表示されます。
6. 元に戻す場合は、抽出項目テーブルで戻したい項目を選択し、「選択した項目を元に戻す」ボタンをクリックします。
7. 元に戻されたテキストが元に戻すテキストエリアに表示されます。

この画面構成により、ユーザーは直感的に操作を行うことができ、匿名化と元に戻す操作を柔軟に行うことができます。

## 処理の流れ

```mermaid
sequenceDiagram
    participant User
    participant GUI
    participant Extractor
    participant Anonymizer
    participant Restorer

    User->>GUI: テキストを入力
    User->>GUI: 「メールアドレスと固有名詞を抽出」クリック
    GUI->>Extractor: テキストを送信
    Extractor->>Extractor: メールアドレスを抽出
    Extractor->>Extractor: 固有名詞を抽出
    Extractor-->>GUI: 抽出結果を返す
    GUI->>GUI: 抽出項目をテーブルに表示

    User->>GUI: 項目を選択/編集
    User->>GUI: 「選択した項目を匿名化」クリック
    GUI->>Anonymizer: 選択項目と置換文字列を送信
    Anonymizer->>Anonymizer: テキストを匿名化
    Anonymizer-->>GUI: 匿名化されたテキストを返す
    GUI->>GUI: 匿名化テキストを表示

    User->>GUI: 「選択した項目を元に戻す」クリック
    GUI->>Restorer: 選択項目と元の文字列を送信
    Restorer->>Restorer: テキストを復元
    Restorer-->>GUI: 復元されたテキストを返す
    GUI->>GUI: 復元テキストを表示
```

## 実装

まず、必要なライブラリをインストールします：

```bash
pip install janome PyQt5
```

次に、アプリケーションのコードを実装します：
```python
import sys
import re
from janome.tokenizer import Tokenizer
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTextEdit, QLabel, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox
from PyQt5.QtCore import Qt

def extract_emails_and_proper_nouns(text):
    # メールアドレスを先に抽出
    email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
    emails = re.findall(email_pattern, text)
    
    # メールアドレスを仮の文字列で置換
    for i, email in enumerate(emails):
        text = text.replace(email, f"__EMAIL_{i}__")
    
    # 固有名詞の抽出
    tokenizer = Tokenizer()
    tokens = tokenizer.tokenize(text)
    proper_nouns = set()

    for token in tokens:
        if '名詞' in token.part_of_speech and '固有名詞' in token.part_of_speech:
            if not token.surface.startswith("__EMAIL_"):  # メールアドレスの仮置換を除外
                proper_nouns.add(token.surface)

    # メールアドレスを元に戻す
    for i, email in enumerate(emails):
        text = text.replace(f"__EMAIL_{i}__", email)

    return list(emails), list(proper_nouns)

class AnonymizerWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("日本語テキスト匿名化ツール")
        self.setGeometry(100, 100, 1200, 900)

        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        layout = QVBoxLayout()

        input_label = QLabel("テキストを入力してください:")
        layout.addWidget(input_label)

        self.input_text = QTextEdit()
        layout.addWidget(self.input_text)

        extract_button = QPushButton("メールアドレスと固有名詞を抽出")
        extract_button.clicked.connect(self.extract_items)
        layout.addWidget(extract_button)

        self.toggle_all_button = QPushButton("全選択/解除")
        self.toggle_all_button.clicked.connect(self.toggle_all_checkboxes)
        layout.addWidget(self.toggle_all_button)

        self.items_table = QTableWidget(0, 3)
        self.items_table.setHorizontalHeaderLabels(["選択", "項目", "置換後"])
        self.items_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        layout.addWidget(self.items_table)

        anonymize_button = QPushButton("選択した項目を匿名化")
        anonymize_button.clicked.connect(self.anonymize_selected)
        layout.addWidget(anonymize_button)

        output_label = QLabel("匿名化されたテキスト:")
        layout.addWidget(output_label)

        self.output_text = QTextEdit()
        self.output_text.setReadOnly(True)
        layout.addWidget(self.output_text)

        restore_label = QLabel("元に戻すテキスト:")
        layout.addWidget(restore_label)

        self.restore_text = QTextEdit()
        layout.addWidget(self.restore_text)

        restore_button = QPushButton("選択した項目を元に戻す")
        restore_button.clicked.connect(self.restore_selected)
        layout.addWidget(restore_button)

        central_widget.setLayout(layout)

        self.items = []
        self.replacements = {}
        self.all_checked = True

    def extract_items(self):
        text = self.input_text.toPlainText()
        emails, proper_nouns = extract_emails_and_proper_nouns(text)
        self.items = emails + proper_nouns
        self.update_items_table()

    def update_items_table(self):
        self.items_table.setRowCount(len(self.items))
        for i, item in enumerate(self.items):
            checkbox = QCheckBox()
            checkbox.setChecked(True)  # デフォルトで全てチェック
            self.items_table.setCellWidget(i, 0, checkbox)
            self.items_table.setItem(i, 1, QTableWidgetItem(item))
            self.items_table.setItem(i, 2, QTableWidgetItem(f"[項目{i+1}]"))

    def toggle_all_checkboxes(self):
        self.all_checked = not self.all_checked
        for i in range(self.items_table.rowCount()):
            checkbox = self.items_table.cellWidget(i, 0)
            checkbox.setChecked(self.all_checked)

    def anonymize_selected(self):
        text = self.input_text.toPlainText()
        self.replacements = {}

        for i in range(self.items_table.rowCount()):
            checkbox = self.items_table.cellWidget(i, 0)
            if checkbox.isChecked():
                original = self.items_table.item(i, 1).text()
                replacement = self.items_table.item(i, 2).text()
                self.replacements[original] = replacement
                text = text.replace(original, replacement)

        self.output_text.setPlainText(text)
        self.restore_text.setPlainText(text)

    def restore_selected(self):
        text = self.restore_text.toPlainText()
        reverse_replacements = {v: k for k, v in self.replacements.items()}

        for i in range(self.items_table.rowCount()):
            checkbox = self.items_table.cellWidget(i, 0)
            if checkbox.isChecked():
                replacement = self.items_table.item(i, 2).text()
                original = reverse_replacements.get(replacement)
                if original:
                    text = text.replace(replacement, original)

        self.restore_text.setPlainText(text)

def main():
    app = QApplication(sys.argv)
    window = AnonymizerWindow()
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
```

## 学習ポイント

1. **正規表現によるメールアドレス抽出**: 正規表現を使用してテキストからメールアドレスを抽出する方法を学べます。

2. **Janomeによる形態素解析**: 日本語テキストを解析し、固有名詞を抽出する方法を学べます。

3. **テキスト処理の順序**: メールアドレスを先に抽出し、固有名詞の抽出から除外する方法を学べます。

4. **PyQt5によるGUI作成**: ボタン、テキストエリア、テーブルなどのウィジェットの使い方や、イベント処理の方法を学べます。

5. **オブジェクト指向プログラミング**: `QMainWindow`を継承したクラスを作成し、アプリケーションの構造を整理しています。

6. **データ構造の操作**: リストや辞書を使って、抽出された項目や置換情報を管理しています。

## 発展の方向性

- より高度な自然言語処理技術を導入し、固有名詞の抽出精度を向上させる
- 他の種類の個人情報（電話番号、住所など）も検出・匿名化できるようにする
- 匿名化ルールをカスタマイズできる機能を追加する
- 処理済みのテキストや置換ルールを保存・読み込みできるようにする
- メールアドレスと固有名詞で異なる匿名化処理を行う機能を追加する

## 注意事項（再掲）

このコードは学習目的で作成されたものであり、実際の業務やセンシティブなデータの処理には適していません。以下の点に注意してください：

- メールアドレスの抽出精度は完璧ではありません。複雑なパターンのメールアドレスを見逃す可能性があります。
- 固有名詞の抽出精度も完璧ではありません。重要な情報を見逃したり、不要な情報を抽出したりする可能性があります。
- セキュリティ面での考慮が不十分です。重要なデータを扱う際は、より堅牢なソリューションを使用してください。
- パフォーマンスの最適化がされていないため、大量のデータ処理には適していません。
- エラーハンドリングが不十分です。予期しない入力に対して適切に対応できない可能性があります。
- このツールは完全な匿名化を保証するものではありません。重要なデータを扱う際は、専門家の助言を求めてください。

## まとめ

このプロジェクトでは、Pythonを使用してシンプルな日本語テキスト匿名化ツールを作成しました。主な特徴は以下の通りです：

1. メールアドレスと固有名詞の抽出
2. GUI（グラフィカルユーザーインターフェース）による操作
3. 選択的な匿名化と復元機能

このツールを通じて、テキスト処理、GUI開発、そして基本的な自然言語処理の技術を学ぶことができます。

## 今後の学習に向けて

このプロジェクトを基にして、以下のような方向で学習を深めていくことをおすすめします：

1. **自然言語処理**: より高度なNLPライブラリ（例：spaCy、NLTK）を使用して、テキスト分析の精度を向上させる。

2. **データベース連携**: SQLiteなどのデータベースを使用して、匿名化ルールや処理履歴を保存する機能を追加する。

3. **セキュリティ**: 暗号化やアクセス制御など、セキュリティ関連の機能を実装する。

4. **ユーザビリティ向上**: ドラッグ＆ドロップによるファイル入力、進捗バーの追加など、より使いやすいインターフェースを設計する。

5. **テスト駆動開発**: ユニットテストやインテグレーションテストを書いて、コードの信頼性を高める。

6. **コード最適化**: プロファイリングツールを使用して、パフォーマンスのボトルネックを特定し、最適化を行う。

このプロジェクトを通じて、実践的なPythonプログラミングのスキルを磨くとともに、テキスト処理やGUI開発の基礎を学ぶことができます。さらに学習を進め、より高度で信頼性の高いアプリケーションの開発に挑戦してみてください！

最後に、このツールはあくまで学習目的で作成されたものであり、実際の重要なデータの匿名化には適していないことを重ねて強調します。実際の匿名化作業には、十分な検討と適切なツールの選択が必要です。

ハッピーコーディング！