4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日本語テキストのPII匿名化ライブラリ「Besshouka」を作った話

4
Posted at

日本語テキストのPII匿名化をローカルで、コード不要で、5分で動かせるOSSライブラリを作りました。

なぜ作ったのか

別のプロジェクトで外部APIにデータを送る前に、自分のデータを保護したいと思ったのがきっかけです。

既存のOSSで一番有名な匿名化ライブラリといえば Microsoft Presidio ですが、英語専用で日本語には対応していません。日本語NERモデルに差し替えることは技術的には可能ですが、認識ルール(Recognizer)をゼロから書き直す必要があり、それはもはや別プロジェクトの規模です。さらに厄介なことに、Presidioの組み込みRecognizerは無効化できないため、日本語テキストに英語ルールが走ってノイズまみれになります。

クラウドAPIという選択肢もありますが、「機密データを外部サーバーに送ること自体がプライバシーリスク」というジレンマがあります。

そこで作ったのが Besshouka です。

⚠️ Besshoukaは現在アルファ版であり、プロダクション利用は推奨しません。バグ報告・ドキュメント改善・テスト追加など、あらゆるコントリビューションを歓迎しています。


何をできるのか

  • 日本語テキストを受け取り、匿名化されたテキストを返す
  • 検出されたPIIの種類・位置・信頼スコアを含む 監査証跡 を生成できる
  • ステートレス設計 — 状態を持たず、100%ローカルで動作
  • すべてオープンソースかつカスタマイズ可能
    • 新しいRecognizerの追加
    • 既存パターンの変更
    • 匿名化ルールの差し替え
    • NERモデルの交換
    • 自作Python関数をオペレーターとして使用

他ツールとの比較

日本語PII匿名化をエンドツーエンドで処理できる単一のOSSツールは存在しませんでした。NERツール(検出のみ)と匿名化フレームワーク(日本語非対応)はそれぞれ存在しますが、クラウドAPI(プライバシーリスク)はすべてのユースケースに適しているわけではありません。Besshoukaはこのギャップを埋めることを目的としています。

ローカル / OSS との比較

機能 Presidio Fujitsu Enterprise Besshouka
日本語フリーテキストPII ❌ JP Recognizer・JPモデルを自前で実装が必要 ❌ 構造化データのマスキングのみ ✅ マイナンバー・電話番号・郵便番号・クレジットカード(Visa/MC/Amex/JCB)・銀行口座・運転免許証・パスポートなど15種類以上
全角文字の正規化 ❌ NFKC正規化なし N/A ✅ NFKC正規化+日本語ダッシュ処理
Recognizerの選択 ❌ 組み込みRecognizerは削除不可 N/A ✅ YAMLに書いたものだけが動く
エンティティごとの匿名化制御 限定的 カラム単位のみ ✅ メソッド・マスク文字・マスク文字数・カスタム置換テキストをエンティティ単位で設定
信頼スコア ✅ ユーザー側フィルタリング不可 N/A ✅ 0.0〜1.0のスコア+ソース情報。--explainフラグで詳細表示
匿名化メソッド replace / mask / redact / hash / encrypt カラムマスク・k匿名化 replace / mask / redact / hash / encrypt / keep / custom(自作関数) の7種類
監査証跡 実装依存 ✅ 変換ごとに元テキスト・オペレーター・入出力での位置を記録
設定方法 Pythonコード or 限定的YAML コンサルティング・独自ツール ✅ 2つのYAMLファイルのみ(コード不要)
カスタムパターンの追加 ✅ Pythonで追加 コンサルティング必要 ✅ YAMLにエントリを追加するだけ
ベンダーロックイン なし(MIT) 高い(Fujitsuミドルウェア依存) なし(OSS)
コスト 無料 エンタープライズライセンス+コンサル費 無料
デプロイ時間 数時間(pip install+JP対応作業) 数週間〜数ヶ月 数分(pip install、すぐ日本語対応)

クラウドAPIとの比較

機能 Google Cloud DLP Azure AI Language Besshouka
データがローカルに留まる ❌ Googleサーバーへ送信 ❌ Azureサーバーへ送信 ✅ ステートレス・インフラ依存なし
日本語PII対応 マイナンバー・銀行・DL・パスポート・税ID マイナンバー(個人・法人)・在留カード・社会保険・DL・パスポート・銀行 15種類以上
全角文字の正規化 不明(非公開) 不明(非公開)
匿名化メソッド replace / mask / redact replace / mask 7種類(カスタム関数含む)
カスタムパターンの追加
ベンダーロックイン GCP依存 Azure依存 なし(OSS)
コスト APIコール課金 APIコール課金 無料

アーキテクチャ概要

Besshoukaは 2つのコンポーネントオーケストレーター が制御する構成になっています。

テキスト入力 → [Analyzer] → [Anonymizer] → テキスト出力
モジュール 役割
Analyzer 検出パイプライン(正規化・Recognizer・NER・競合解決)
Anonymizer 変換パイプライン(マスク・置換・リダクション等)
Orchestrator 上記2つをつなぐパイプライン管理
Models 共有データ構造(循環インポート回避)
Config YAMLの読み込み・バリデーション
Defaults 組み込みRecognizerパターン・オペレータールール

AnalyzerとAnonymizerは完全に疎結合なので、片方だけ差し替えることも、独自関数で置き換えることも簡単にできます。

Analyzer — 検出

Raw Text → normalize → recognizers → conflict resolution → list[RecognizerResult]
  1. 正規化 (normalize.py) — NFKC正規化(全角→半角)+日本語ダッシュの標準化
  2. Recognizer群 — 正規化済みテキストをそれぞれ独立してスキャンし、RecognizerResultを返す
    • 正規表現Recognizer:スコア1.0(完全一致)
    • マイナンバーRecognizer:チェックディジット検証+文脈スコアリング(0.4〜1.0)
    • GiNZA NER:確率的スコア(DATE検証で非日付スパンは0.2に降格)
  3. 競合解決 (conflict_resolution.py) — 複数Recognizerの重複検出を信頼スコアに基づいてマージ

対応エンティティタイプ:

エンティティタイプ 説明
PERSON 人物名
LOCATION 住所・地名
ORGANIZATION 企業名・組織名
PHONE_NUMBER 電話番号(携帯・固定・フリーダイヤル)
EMAIL メールアドレス
MY_NUMBER マイナンバー(チェックディジット+文脈スコアリング)
POSTAL_CODE 郵便番号(〒XXX-XXXX)
CREDIT_CARD クレジットカード番号(Visa/MC/Amex/JCB)
BANK_ACCOUNT 銀行口座番号
DRIVERS_LICENSE 運転免許証番号
PASSPORT パスポート番号
DATE 日付
TIME 時刻
MONEY 金額
QUANTITY 数量

Anonymizer — 変換

検出されたエンティティに対して、設定されたオペレーターを適用します。重要な実装上のポイントは 右から左へ処理する(逆順ループ) こと。先に左側のスパンを置き換えてしまうと後続スパンのインデックスがずれるため、末尾から処理することでこの問題を回避しています。

Original:  "田中太郎の電話番号は090-1234-5678です"
                                       ← まずここを処理
              ← 次にここを処理

Orchestrator — グルーコード

パイプライン全体のフローを管理し、ProcessingContext(パイプラインの状態を保持するデータ型)を各ステップに渡します。コンポーネントが利用不可・失敗した場合はグレースフルデグラデーションで処理し、未知のエンティティタイプが検出された際は警告をログ出力します。


データコントラクト

AnalyzerとAnonymizerが共通で使うデータ構造はmodelsモジュールに集約されており、循環インポートを回避しています。

データ型 内容
RecognizerResult 文字オフセット・エンティティタイプ・信頼スコア・検出元Recognizer
OperatorResult 変更内容・使用オペレーター・入出力での位置
EngineResult 最終的な匿名化テキスト+全OperatorResultのリスト(完全な監査証跡)

設定システム

すべての動作を2つのYAMLファイルで制御します。コード変更は一切不要です。

ファイル 役割
recognizers.yaml 何を検出するか(Recognizer定義)
operators.yaml 検出したものをどう処理するか(オペレーター定義)

Configローダーは起動時に両ファイルをバリデーションし、フィールド不足や不正なエントリに対して明確なエラーを出します。デフォルト設定が組み込まれているためインストール直後から動作しますが、独自のYAMLファイルを用意してCLIの--recognizers--rulesオプションで指定することもできます。

組み込みRecognizerパターン

デフォルトのrecognizers.yamlには以下の正規表現パターンが含まれています:

  • 携帯電話・固定電話・フリーダイヤル番号
  • メールアドレス
  • マイナンバー(スペースあり・なし両対応)
  • 郵便番号(〒XXX-XXXX)
  • クレジットカード(Visa・Mastercard・Amex・JCB)
  • 銀行口座番号
  • 運転免許証番号
  • パスポート番号

NERベースの検出(GiNZA)は人物名・組織名・地名をカバーします。

デフォルトのオペレーター割り当て

エンティティ デフォルト動作
人物名・地名・組織名 日本語プレースホルダーに置換(<氏名> <住所> <組織名>
電話番号・クレジットカード・銀行口座 末尾を部分マスク(読めないが形式は保持)
マイナンバー・運転免許証・パスポート 完全リダクション

これらはデフォルト設定ですが、YAMLをコピーして好みに合わせて変更できます。


Besshoukaの拡張

新しいエンティティタイプの追加にコード変更は不要です。YAMLにエントリを追加するだけです。

例:社員番号を検出してプレースホルダーに置換する場合

# recognizers.yaml
recognizers:
  - name: employee_id
    entity_type: EMPLOYEE_ID
    pattern: 'EMP-[A-Z]{2}\d{6}'
    score: 1.0
    source: custom

# operators.yaml
operators:
  EMPLOYEE_ID:
    method: replace
    value: "<社員番号>"

CLIで指定するだけで完了です:

python -m besshouka anonymize \
  --recognizers my_recognizers.yaml \
  --rules my_operators.yaml \
  "EMP-AB123456が対応しました"

正規表現では対応できない複雑な検出ロジックが必要な場合は、PythonでBaseRecognizerをサブクラス化することもできます。


使い方

インストール

Python 3.11以上3.14未満であれば、pipで直接インストールできます:

pip install besshouka

ソースから試す場合:

python -m venv env_besshouka
source env_besshouka/bin/activate
pip install -r requirements.txt

CLI

# インラインテキストを匿名化
python -m besshouka anonymize "田中太郎の電話番号は090-1234-5678です"

# ファイルから匿名化
python -m besshouka anonymize --input document.txt --output anonymized.txt

# カスタムルールで匿名化
python -m besshouka anonymize --rules my_rules.yaml --recognizers my_patterns.yaml "テキスト"

# 検出のみ表示
python -m besshouka analyze "田中太郎の電話番号は090-1234-5678です"

# スコア・ソース情報付きで表示
python -m besshouka analyze --explain "田中太郎の電話番号は090-1234-5678です"

プログラムからの利用

from besshouka.config.loader import load_recognizer_config, load_operator_config
from besshouka.orchestrator.pipeline import run

rec_config = load_recognizer_config("path/to/recognizers.yaml")
op_config = load_operator_config("path/to/operators.yaml")

ctx = run("田中太郎の電話番号は090-1234-5678です", rec_config, op_config)

print(ctx.engine_result.text)   # 匿名化されたテキスト
print(ctx.engine_result.items)  # 監査証跡

実行例

--explainフラグを使うと、何がどのスコアで検出されたかの一覧が表示されます。

正規表現ベースのエンティティ(メール・電話番号・クレジットカード等)はスコア1.0(設定どおりの完全一致)、GiNZA NERの検出スコアは0.85であることに注目してください。また、GiNZAは統計モデルなので誤検出もあります(例:住所の一部がTIMEに、社員番号がMONEYに分類されているケース)。

python -m besshouka analyze --explain \
  "顧客情報:氏名は山田花子様、連絡先はhanako.yamada@tokyo-corp.co.jpまたは03-5555-1234(会社)か080-9876-5432(携帯)。住所は〒150-0002渋谷区渋谷一丁目。クレジットカードはVISA 4532-0150-1234-5678、有効期限12/27。銀行口座は三菱UFJ銀行 普通 1234567。パスポート番号TK1234567、運転免許証番号012345678901。個人番号は123456789018です。なお、社員番号は291048375012ですが、これはマイナンバーではありません。株式会社東京テクノロジーズの佐藤健太部長にもご確認ください。"
               Detected Entities             
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Entity Type  ┃ Text                   ┃ Start ┃ End ┃ Score ┃ Source         ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ PERSON       │ 山田花子               │     8 │  12 │  0.85 │ ginza_ner      │
│ EMAIL        │ hanako.yamada@tokyo-c… │    18 │  48 │  1.00 │ regex_registry │
│ PHONE_NUMBER │ 03-5555-1234           │    51 │  63 │  1.00 │ regex_registry │
│ PHONE_NUMBER │ 080-9876-5432          │    68 │  81 │  1.00 │ regex_registry │
│ POSTAL_CODE  │ 〒150-0002             │    89 │  98 │  1.00 │ regex_registry │
│ TIME         │ 渋谷区渋谷一           │    98 │ 104 │  0.85 │ ginza_ner      │
│ CREDIT_CARD  │ 4532-0150-1234-5678    │   121 │ 140 │  1.00 │ regex_registry │
│ DATE         │ 12/27                  │   145 │ 150 │  0.20 │ ginza_ner      │
│ BANK_ACCOUNT │ 1234567                │   167 │ 174 │  0.50 │ regex_registry │
│ PASSPORT     │ TK1234567              │   182 │ 191 │  1.00 │ regex_registry │
│ PHONE_NUMBER │ 012345678901           │   199 │ 211 │  1.00 │ regex_registry │
│ MY_NUMBER    │ 123456789018           │   217 │ 229 │  1.00 │ my_number      │
│ MONEY        │ 291048375012           │   240 │ 252 │  0.85 │ ginza_ner      │
│ ORGANIZATION │ 株式会社東京テクノロ … │   273 │ 286 │  0.85 │ ginza_ner      │
│ PERSON       │ 佐藤健太               │   287 │ 291 │  0.85 │ ginza_ner      │
└──────────────┴────────────────────────┴───────┴─────┴───────┴────────────────┘

匿名化コマンドの出力:

python -m besshouka anonymize \
  "顧客情報:氏名は山田花子様、連絡先はhanako.yamada@tokyo-corp.co.jpまたは03-5555-1234(会社)か080-9876-5432(携帯)。住所は〒150-0002渋谷区渋谷一丁目。クレジットカードはVISA 4532-0150-1234-5678、有効期限12/27。銀行口座は三菱UFJ銀行 普通 1234567。パスポート番号TK1234567、運転免許証番号012345678901。個人番号は123456789018です。なお、社員番号は291048375012ですが、これはマイナンバーではありません。株式会社東京テクノロジーズの佐藤健太部長にもご確認ください。"
顧客情報:氏名は<氏名>様、連絡先は<メール>または03-5555-****(会社)か080-9876-****
(携帯)。住所は<郵便番号><時刻>丁目。クレジットカードはVISA
4532-0150-1234-****、有効期限12/27。銀行口座は三菱UFJ銀行 普通
1234***。パスポート番号、運転免許証番号01234567****。個人番号は<マイナンバー>で
す。なお、社員番号は<金額>ですが、これはマイナンバーではありません。<組織名>の<
氏名>部長にもご確認ください。

テスト

# 遅いGiNZAテストを除く全テスト
pytest tests/ -m "not slow"

# GiNZAを含む全テスト
pytest tests/

# カバレッジレポート付き
pytest tests/ --cov=besshouka --cov-report=term-missing

作って気づいたこと

GiNZAのNERは統計モデルなので誤検出します。 今回の例でも、住所の一部がTIMEとして、社員番号がMONEYとして検出されました。日本語はスペースで単語が区切られないため、トークナイゼーションが本質的に曖昧で、この問題をさらに複雑にします。

これがBesshoukaが正規表現Recognizerと信頼スコアを組み合わせている理由です。精度の高い正規表現を書けば完全にGiNZAをオーバーライドできますし、信頼スコアによるフィルタリングでモデルの低品質な検出を除外することもできます。

Besshoukaは非常にニッチな用途のツールです。これが以前から存在しなかった理由はおそらくそのためでしょう。しかし、AIパイプラインやLangChainスタイルのオーケストレーションが普及した今、外部APIにデータを送る前にローカルでPIIを除去できるツールは「当然あるべきもの」だと感じています。

もしよければ、GitHub をのぞいてみてください。スター・Issue・PRどれでも歓迎です。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?