Microsoft製のPII検出・匿名化SDK「Presidio」でテキストの個人情報をマスクしてみる
はじめに
データ事業本部のkobayashiです。
テキストデータを扱っていると、氏名・メールアドレス・電話番号・クレジットカード番号といった個人情報(PII: Personally Identifiable Information)を検出してマスクしたいということはよくあるかと思います。ログをそのまま保存できない、LLMにプロンプトとして渡す前に伏せておきたい、データセットを共有する前に匿名化したい、といった場面です。
正規表現を自前で書くという方法もありますが、氏名や住所のような文脈に依存する情報は正規表現だけでは検出が難しく、メンテナンスも大変です。
そこで今回はMicrosoftがOSSで公開しているPII検出・匿名化SDKPresidioの基本的な使い方をまとめます。
Presidioとは
Presidioはテキストや画像に含まれる個人情報(PII)を検出し、匿名化するためのMicrosoft製のSDKです。固有表現抽出(NER)・正規表現・チェックサム・コンテキスト(文脈)などを組み合わせてPIIを検出し、置換・マスク・ハッシュ化・暗号化といった方法で匿名化を行います。
主な特徴としては以下になります。
- 氏名・メールアドレス・電話番号・クレジットカード番号・IPアドレスなど、多数のPIIエンティティを標準でサポート
- spaCyやStanza、TransformersなどのNLPエンジンと連携した文脈ベースの検出
- 正規表現やdeny-list(禁止語リスト)によるカスタムRecognizer(検出器)を簡単に追加できる
- 置換・マスク・ハッシュ化・暗号化・削除など複数の匿名化方式(Operator)を選択できる
- 検出器(presidio-analyzer)と匿名化器(presidio-anonymizer)がパッケージとして分離されている
- 画像(presidio-image-redactor)に対する匿名化もサポート
検出を担うpresidio-analyzerと、匿名化を担うpresidio-anonymizerの2つのパッケージで構成されており、本記事ではこの2つを使ってテキストのPIIを検出・匿名化していきます。
では早速試してみます。
Presidioを使ってみる
環境
今回使用した環境は以下の通りです。
presidio-analyzer 2.2.362
presidio-anonymizer 2.2.362
インストール
presidio-analyzerとpresidio-anonymizerをインストールします。presidio-analyzerはNLPエンジンとしてspaCyを利用するため、spaCyの言語モデルも併せてダウンロードします。
$ uv pip install presidio_analyzer presidio_anonymizer
$ python -m spacy download en_core_web_lg
en_core_web_lgは英語向けの大きめのモデル(約400MB超)で、Presidioが推奨しているモデルです。
なお、環境によってはpython -m spacy downloadの実行時にModuleNotFoundError: No module named 'click'が発生する場合があります。その際はuv pip install clickでclickを追加するとダウンロードできるようになります。
基本的な使い方
まずは最もシンプルな使い方として、AnalyzerEngineでPIIを検出し、AnonymizerEngineでその箇所を匿名化してみます。
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
text = (
"My name is David Johnson and my email is david.johnson@example.com. "
"My phone number is (212) 555-1234 and my credit card is 4095-2609-9393-4932."
)
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
# 1. PII を検出する
results = analyzer.analyze(text=text, language="en")
print("=== 検出結果 ===")
for r in results:
print(f"{r.entity_type:<16} start={r.start:<3} end={r.end:<3} score={r.score:.2f} value={text[r.start:r.end]!r}")
# 2. 検出した PII を匿名化する
anonymized = anonymizer.anonymize(text=text, analyzer_results=results)
print("\n=== 匿名化後のテキスト ===")
print(anonymized.text)
analyzer.analyze()に対象テキストと言語(language="en")を渡すと、検出されたPIIのリストが返ってきます。各要素はエンティティの種類(entity_type)・テキスト中の位置(start/end)・信頼度スコア(score)を持ちます。
そのままanonymizer.anonymize()に検出結果を渡すと、デフォルトでは各PIIが<エンティティ名>の形で置換されます。
実行結果は以下のようになります。
$ python basic.py
=== 検出結果 ===
EMAIL_ADDRESS start=41 end=66 score=1.00 value='david.johnson@example.com'
CREDIT_CARD start=124 end=143 score=1.00 value='4095-2609-9393-4932'
PERSON start=11 end=24 score=0.85 value='David Johnson'
PHONE_NUMBER start=87 end=101 score=0.75 value='(212) 555-1234'
URL start=41 end=49 score=0.50 value='david.jo'
URL start=55 end=66 score=0.50 value='example.com'
=== 匿名化後のテキスト ===
My name is <PERSON> and my email is <EMAIL_ADDRESS>. My phone number is <PHONE_NUMBER> and my credit card is <CREDIT_CARD>.
メールアドレス・クレジットカード番号・人名・電話番号がそれぞれEMAIL_ADDRESS・CREDIT_CARD・PERSON・PHONE_NUMBERとして検出されていることがわかります。クレジットカード番号やメールアドレスはチェックサムや形式から信頼度1.00で、人名はNERによって0.85で検出されています。
なお、メールアドレスの一部がURLとしても重複して検出されています(score=0.50)。Presidioは複数のRecognizerがそれぞれ独立に検出を行うため、このように位置が重なる検出結果が出ることがあります。anonymizer.anonymize()は、ある検出結果が別の検出結果の範囲に包含されている場合や、まったく同じ範囲で複数検出されスコアが低い場合に、その検出結果を取り除きます。今回はURLの検出範囲(41-49, 55-66)がEMAIL_ADDRESSの範囲(41-66)に包含されているためURL側が取り除かれ、最終的な匿名化後のテキストではEMAIL_ADDRESSとして正しく1つにまとめられていることが確認できます。
匿名化手法を制御する
匿名化のやり方はエンティティの種類ごとに変えることができます。anonymize()のoperators引数に、エンティティ名とOperatorConfigの対応を渡します。代表的なOperatorは以下の通りです。
| Operator | 説明 |
|---|---|
replace |
指定した固定文字列に置換する |
redact |
検出箇所を削除する |
mask |
指定文字でマスクする(マスクする文字数・方向を指定可能) |
hash |
ハッシュ値に置換する(hash_typeにsha256またはsha512を指定) |
encrypt |
暗号化する(復号にはキーが必要) |
例えば「人名は削除」「電話番号は末尾をマスク」「クレジットカード番号はハッシュ化」「それ以外は固定文字列に置換」という方針で匿名化してみます。
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig
text = (
"My name is David Johnson and my email is david.johnson@example.com. "
"My phone number is (212) 555-1234 and my credit card is 4095-2609-9393-4932."
)
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
results = analyzer.analyze(text=text, language="en")
operators = {
# 既定: 検出値を <REDACTED> に置換
"DEFAULT": OperatorConfig("replace", {"new_value": "<REDACTED>"}),
# 人名: 完全に削除
"PERSON": OperatorConfig("redact", {}),
# 電話番号: 末尾10文字を * でマスク
"PHONE_NUMBER": OperatorConfig(
"mask", {"masking_char": "*", "chars_to_mask": 10, "from_end": True}
),
# クレジットカード: SHA-256 でハッシュ化(salt を固定して再現性を確保)
"CREDIT_CARD": OperatorConfig(
"hash", {"hash_type": "sha256", "salt": "presidio-demo-fixed-salt-sample"}
),
}
anonymized = anonymizer.anonymize(
text=text, analyzer_results=results, operators=operators
)
print(anonymized.text)
DEFAULTキーは、個別に指定しなかったエンティティすべてに適用される既定のOperatorです。ここではメールアドレスがDEFAULTのreplaceの対象になります。
実行結果は以下の通りです。
$ python operators.py
My name is and my email is <REDACTED>. My phone number is (212********** and my credit card is 498b7ea434e932ed06ac0ca8bd0a23e8ae9f3004c5c0f8ace51324c52998c705.
人名が削除され(My name is の後が空になっています)、メールアドレスは<REDACTED>に置換、電話番号は末尾10文字が*でマスク、クレジットカード番号はSHA-256のハッシュ値に置換されていることが確認できます。このようにエンティティごとに匿名化方法を細かく制御できます。
なお、hashオペレータはsaltを指定しなかった場合、エンティティごとにランダムなsalt(os.urandom)が付与される実装になっています。そのため、saltを指定しないと同じ入力でも実行のたびにハッシュ値が変わり、上記のような特定の値は再現できません。同じ入力を常に同じ値へ変換したい(例: 突合用の疑似化を行いたい)場合は、上記のようにsaltに16バイト以上の固定値を指定します。
カスタムRecognizerで独自のPIIを検出する
標準でサポートされているエンティティ以外にも、正規表現を使って独自のPIIを検出するRecognizerを追加できます。ここでは日本のマイナンバー(個人番号: 12桁)を検出するPatternRecognizerを作成してみます。
from presidio_analyzer import AnalyzerEngine, Pattern, PatternRecognizer
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig
text = "私のマイナンバーは 1234-5678-9012 です。連絡先は test@example.com です。"
# マイナンバー(個人番号: 12桁)を検出するカスタム Recognizer
my_number_pattern = Pattern(
name="jp_my_number",
regex=r"\b\d{4}-?\d{4}-?\d{4}\b",
score=0.5,
)
my_number_recognizer = PatternRecognizer(
supported_entity="JP_MY_NUMBER",
patterns=[my_number_pattern],
context=["マイナンバー", "個人番号", "マイナンバーカード"],
)
analyzer = AnalyzerEngine()
analyzer.registry.add_recognizer(my_number_recognizer)
results = analyzer.analyze(text=text, language="en")
print("=== 検出結果 ===")
for r in results:
print(f"{r.entity_type:<16} score={r.score:.2f} value={text[r.start:r.end]!r}")
anonymizer = AnonymizerEngine()
anonymized = anonymizer.anonymize(
text=text,
analyzer_results=results,
operators={"JP_MY_NUMBER": OperatorConfig("replace", {"new_value": "<マイナンバー>"})},
)
print("\n=== 匿名化後のテキスト ===")
print(anonymized.text)
Patternで正規表現と基本スコアを定義し、PatternRecognizerに検出対象のエンティティ名(JP_MY_NUMBER)と一緒に渡します。contextには、検出箇所の周辺に現れると信頼度を引き上げる文脈ワードを指定しています。作成したRecognizerはanalyzer.registry.add_recognizer()で登録するだけで使えるようになります。
実行結果は以下の通りです。
$ python custom_recognizer.py
=== 検出結果 ===
EMAIL_ADDRESS score=1.00 value='test@example.com'
JP_MY_NUMBER score=0.85 value='1234-5678-9012'
URL score=0.50 value='example.com'
=== 匿名化後のテキスト ===
私のマイナンバーは <マイナンバー> です。連絡先は <EMAIL_ADDRESS> です。
1234-5678-9012がJP_MY_NUMBERとして検出され、<マイナンバー>に置換されていることがわかります。Patternの基本スコアは0.5に設定していましたが、検出結果は0.85になっています。これは周辺に文脈ワードの「マイナンバー」が存在したため、contextによってスコアが引き上げられた結果です。presidio-analyzerのデフォルトでは検出箇所の前方の数語から文脈ワードを探索し、一致するとスコアを加算します。標準のEMAIL_ADDRESSRecognizerも同時に動作しており、メールアドレスも併せて匿名化されています。
なお、PatternRecognizerのような正規表現ベースの検出は言語設定(language)に関わらず動作するため、上記のように日本語テキストに対しても利用できます。氏名や住所のような文脈依存のエンティティを日本語で検出したい場合は、spaCyの日本語モデル(ja_core_news_lgなど)をNLPエンジンに設定する必要があります。
まとめ
MicrosoftのPII検出・匿名化SDKPresidioを使って、テキストに含まれる個人情報の検出と匿名化を試してみました。
AnalyzerEngineでPIIを検出し、AnonymizerEngineで匿名化するという2ステップの流れで、メールアドレスや人名といった個人情報を簡単にマスクできることが確認できました。さらに、Operatorによってエンティティごとに匿名化方法を制御したり、PatternRecognizerで独自のPIIを検出したりと、柔軟にカスタマイズできる点も魅力です。
ログのマスキングやLLMに渡すプロンプトの前処理など、個人情報を扱う場面でぜひ活用していきたいと思います。
最後まで読んで頂いてありがとうございました。





