Sensitive Data Protectionを利用して各匿名化手法を試してみた

Sensitive Data Protectionを利用して各匿名化手法を試してみた

Google CloudのSensitive Data Protectionを使用した個人情報の自動検出と匿名化手法を解説。削除、マスキング、暗号化など11の変換方式とPython実装例を紹介。開発環境やログのPII漏洩防止に最適です。
2026.07.16

はじめに

こんにちは。
クラウド事業本部コンサルティング部の渡邉です。

個人情報(PII)を含むデータを扱う際、本番データを開発環境に持ち込んだり、ログに個人情報が出力されてしまったりといったリスクは多くの現場で課題になっています。Google Cloud の Sensitive Data Protection(旧 Cloud DLP)を使えば、テキスト中の PII を自動的に検出し、マスキングや置換などで匿名化することができます。

本記事では、Sensitive Data Protection による PII 匿名化(非識別化)の概要と、実際に Python クライアントライブラリを使って試してみたいと思います。

Sensitive Data Protection とは

Sensitive Data Protection は、データの検査・分類・非識別化(匿名化)を行うマネージドサービスです。テキスト、画像、Cloud Storage / BigQuery / Datastore などのストレージに対して機密データのスキャン・変換が可能です。

非識別化とは、個人を特定できる情報をデータから取り除いたり置換したりするプロセスです。API は送信したデータの中から指定した種類の機密情報を検出し、変換ルールに従って匿名化されたデータを返します。

PII の検出:infoType とは

Sensitive Data Protection はどのような種類の機密情報を検出するかを infoType で指定します。100 以上の組み込み infoType が用意されており、グローバルなタイプから国・地域固有のタイプまで幅広くカバーしています。

代表的な PII 系 infoType の例

infoType 説明
PERSON_NAME 人名
EMAIL_ADDRESS メールアドレス
PHONE_NUMBER 電話番号
CREDIT_CARD_NUMBER クレジットカード番号
US_SOCIAL_SECURITY_NUMBER 米国社会保障番号(SSN)
JAPAN_INDIVIDUAL_NUMBER 日本のマイナンバー
DATE 日付
PASSWORD パスワード

全 infoType の一覧は 公式ドキュメントで確認できます。
https://docs.cloud.google.com/sensitive-data-protection/docs/infotypes-reference

匿名化(非識別化)の手法

Sensitive Data Protection では、検出した PII に対してさまざまな変換を適用できます。

変換手法 設定オブジェクト 説明 可逆性
削除(Redaction) RedactConfig 検出した値を完全に削除する 不可
文字マスキング CharacterMaskConfig *# など指定文字で置き換える 不可
infoType 名で置換 ReplaceWithInfoTypeConfig 検出した値を infoType 名(例: [EMAIL_ADDRESS])に置き換える 不可
固定値で置換 ReplaceValueConfig 指定した固定文字列に置き換える 不可
辞書からランダム置換 ReplaceDictionaryConfig 指定した単語リストからランダムに選んだ値に置き換える 不可
固定サイズバケット化 FixedSizeBucketingConfig 数値を固定間隔の範囲ラベルに変換する(例: 25 → 20-30 不可
カスタムバケット化 BucketingConfig 任意の範囲と置換値でバケット化する 不可
日時一部抽出 TimePartConfig 日時から指定した部分(年・月・曜日など)のみを残す 不可
日付シフト DateShiftConfig 指定した日数の範囲内でランダムに日付をずらす(文脈キーにより同一レコード内で一貫性を持たせることも可能) 不可
暗号ハッシュ CryptoHashConfig HMAC-SHA256 でハッシュ化し Base64 エンコードした値に置き換える 不可
決定論的暗号化 CryptoDeterministicConfig AES-SIV で暗号化したトークンに置き換える(再識別が可能)
形式保持暗号化 CryptoReplaceFfxFpeConfig 元データと同じ長さ・文字種のトークンに置き換える(再識別が可能)

用途に応じて手法を選択します。例えば開発環境への安全なデータ提供には文字マスキングや infoType 名置換が手軽で、参照整合性を保ちながら本格的にトークン化したい場合は決定論的暗号化が適しています。各変換手法の詳細なパラメーターは 公式ドキュメントをご参照ください。

https://docs.cloud.google.com/sensitive-data-protection/docs/transformations-reference

実際に試してみる

前提条件

  • Google Cloud プロジェクトが作成済みであること
  • Sensitive Data Protection API(dlp.googleapis.com)が有効化されていること
  • Python 3.12 以上がインストールされていること
  • Application Default Credentials(ADC)の設定が完了していること

ステップ 0:環境変数の設定

以降のコマンドで使用するプロジェクト ID と暗号化キーを環境変数に設定します。

export PROJECT_ID="YOUR_PROJECT_ID"

# AES-256 鍵(32 バイト)を生成して base64 エンコード
export UNWRAPPED_KEY=$(python3 -c "import os, base64; print(base64.b64encode(os.urandom(32)).decode())")
echo "UNWRAPPED_KEY=${UNWRAPPED_KEY}"

UNWRAPPED_KEY は決定論的暗号化・形式保持暗号化のステップで共通して使います。同じキーを使えば、後から暗号化したデータを reidentify_content メソッド で元の値に復元できます。

ステップ 1:Sensitive Data Protection API の有効化

利用するGoogle CloudプロジェクトでSensitive Data Protection API を有効化する

# Sensitive Data Protection API を有効化する
gcloud services enable dlp.googleapis.com \
  --project=${PROJECT_ID}

ステップ 2:Python クライアントライブラリのインストール

Python 3.12 以降の環境(Debian/Ubuntu など)ではシステム Python へのパッケージ直インストールが制限されています。仮想環境を作成してからインストールします。

まず python3-venv パッケージが未インストールの場合はインストールします。

sudo apt install -y python3.12-venv

次に仮想環境を作成してアクティベートし、ライブラリをインストールします。

python3 -m venv .venv
source .venv/bin/activate
pip install google-cloud-dlp

以降のコマンドはこの仮想環境がアクティブな状態で実行してください。シェルを開き直した場合は source .venv/bin/activate を再実行してください。

ステップ 3:文字マスキングによる匿名化

検出した PII の各文字を #* などのマスキング文字で上書きする、最もシンプルな変換手法です。変換後もテキストの文字数や構造がある程度維持されるため、「PII が含まれていたことは示しつつ内容は見せたくない」ログ出力やデバッグ用途に向いています。

この手法の特性として、元の値を一切復元できない不可逆変換である点を理解しておく必要があります。また、異なるメールアドレスでも同じ文字数のマスクパターンになるため、レコード間で値の対応関係を保つ用途には使えません。あくまでも「その場でデータを隠す」ための手法です。

メールアドレスを # でマスキングする例です。.@ は記号として残します。

01_masking.py
import google.cloud.dlp

def deidentify_with_mask(project: str, input_str: str) -> None:
    """メールアドレスを # でマスキングして匿名化する"""
    dlp = google.cloud.dlp_v2.DlpServiceClient()
    parent = f"projects/{project}/locations/global"

    inspect_config = {
        "info_types": [{"name": "EMAIL_ADDRESS"}]
    }

    deidentify_config = {
        "info_type_transformations": {
            "transformations": [
                {
                    "info_types": [{"name": "EMAIL_ADDRESS"}],
                    "primitive_transformation": {
                        "character_mask_config": {
                            "masking_character": "#",
                            "characters_to_ignore": [
                                {"characters_to_skip": ".@"}
                            ],
                        }
                    },
                }
            ]
        }
    }

    response = dlp.deidentify_content(
        request={
            "parent": parent,
            "deidentify_config": deidentify_config,
            "inspect_config": inspect_config,
            "item": {"value": input_str},
        }
    )
    print(response.item.value)

# 実行例
import os
deidentify_with_mask(
    project=os.environ["PROJECT_ID"],
    input_str="私の名前は田中太郎です。メールアドレスは tanaka.taro@example.com です。",
)

実行すると、以下のように @. を残してメールアドレスがマスキングされることがわかりました。

python 01_masking.py
私の名前は田中太郎です。メールアドレスは ##########@#######.### です。

ステップ 4:infoType 名による置換(複数 infoType)

検出した PII を [EMAIL_ADDRESS][PHONE_NUMBER] のような種別名のプレースホルダーに置き換える手法です。変換後のテキストを見るだけで「どこにどの種類の PII があったか」が構造的に把握できるため、データの品質確認や、どの infoType が検出されるかを確かめる開発・テスト段階に適しています。

この手法の最大の制約は、値の個別性が完全に失われる点です。田中さんのメールアドレスも山田さんのメールアドレスも、変換後は同じ [EMAIL_ADDRESS] になります。そのため、匿名化後に「誰のデータか区別しながら処理を続けたい」ケース(例えば、複数テーブルの JOIN など)には使えません。データの中身ではなく「PII の在り処を把握する」フェーズで活用し、本番データの匿名化には後続の暗号化手法を検討してください。

複数の PII 種別を同時に処理し、infoType 名で置き換える例です。

02_replacement.py
import google.cloud.dlp
from typing import List

def deidentify_with_replace_infotype(
    project: str, item: str, info_types: List[str]
) -> None:
    """検出した PII を infoType 名(例: [EMAIL_ADDRESS])に置き換える"""
    dlp = google.cloud.dlp_v2.DlpServiceClient()
    parent = f"projects/{project}/locations/global"

    inspect_config = {
        "info_types": [{"name": t} for t in info_types]
    }

    deidentify_config = {
        "info_type_transformations": {
            "transformations": [
                {
                    "primitive_transformation": {
                        "replace_with_info_type_config": {}
                    }
                }
            ]
        }
    }

    response = dlp.deidentify_content(
        request={
            "parent": parent,
            "deidentify_config": deidentify_config,
            "inspect_config": inspect_config,
            "item": {"value": item},
        }
    )
    print(response.item.value)

# 実行例
import os
deidentify_with_replace_infotype(
    project=os.environ["PROJECT_ID"],
    item="お名前:山田花子、電話:090-1234-5678、メール:hanako@example.com",
    info_types=["PERSON_NAME", "PHONE_NUMBER", "EMAIL_ADDRESS"],
)

実行すると、各 PII がその種別名に置き換えられます。

python 02_replacement.py 
お名前:[PERSON_NAME]、電話:[PHONE_NUMBER]、メール:[EMAIL_ADDRESS]

ステップ 5:決定論的暗号化による匿名化

同じ入力値と同じキーの組み合わせから、常に同じトークンを生成する暗号化手法です。「決定論的」という名のとおり、出力が一意に決まります。これにより、複数のテーブルやシステムにまたがって PII を匿名化しても、レコード間の対応関係が壊れないという特性があります。たとえばユーザーテーブルとオーダーテーブルがどちらもメールアドレスを持っている場合、決定論的暗号化で両方を匿名化すれば、同じメールアドレスは両テーブルで同じトークンになるため JOIN が成立します。

また、同じキーを保持していれば後から reidentify_content API を使って元の値に戻す再識別が可能です。匿名化したデータを第三者に提供しつつ、鍵の管理者だけが必要なときに復元できる、という運用に対応しています。

一方で、出力トークンは内部的に base64 エンコードされた暗号文であるため、入力とは文字数・形式が大きく変わります。既存システムが桁数チェックや正規表現でフォーマットを検証している場合は、後続のステップ 6(形式保持暗号化)を検討してください。

ここでは同じメールアドレスが 2 箇所に登場するテキストを処理し、同じトークンが割り当てられることを確認します。

03_deterministic_encryption.py
import base64
import os

import google.cloud.dlp

def deidentify_with_deterministic(
    project: str,
    input_str: str,
    info_types: list,
    surrogate_type: str,
    unwrapped_key: str,
) -> None:
    """検出した PII を決定論的暗号化でトークン化する(再識別可能)"""
    dlp = google.cloud.dlp_v2.DlpServiceClient()
    parent = f"projects/{project}/locations/global"

    key_bytes = base64.b64decode(unwrapped_key)

    inspect_config = {"info_types": [{"name": t} for t in info_types]}

    deidentify_config = {
        "info_type_transformations": {
            "transformations": [
                {
                    "primitive_transformation": {
                        "crypto_deterministic_config": {
                            "crypto_key": {
                                "unwrapped": {"key": key_bytes}
                            },
                            "surrogate_info_type": {"name": surrogate_type},
                        }
                    }
                }
            ]
        }
    }

    response = dlp.deidentify_content(
        request={
            "parent": parent,
            "deidentify_config": deidentify_config,
            "inspect_config": inspect_config,
            "item": {"value": input_str},
        }
    )
    print(response.item.value)

# 実行例
deidentify_with_deterministic(
    project=os.environ["PROJECT_ID"],
    input_str=(
        "1件目:tanaka@example.com、"
        "2件目:yamada@example.com、"
        "再掲:tanaka@example.com"
    ),
    info_types=["EMAIL_ADDRESS"],
    surrogate_type="EMAIL_TOKEN",
    unwrapped_key=os.environ["UNWRAPPED_KEY"],
)

tanaka@example.com の 2 箇所が同じトークンに、yamada@example.com は別のトークンに変換されます。

python 03_deterministic_encryption.py
1件目:EMAIL_TOKEN(48):ARRGU4NzfhwfXsufc8uj/AKaO1W3GfumrPYiyGA8I3JRnWU=、2件目:EMAIL_TOKEN(48):AafRn7FF5FMg4piIW99N8OcXCEtpKw8AKJVOCH0WvzBW3A0=、再掲:EMAIL_TOKEN(48):ARRGU4NzfhwfXsufc8uj/AKaO1W3GfumrPYiyGA8I3JRnWU=

ステップ 6:形式保持暗号化(FPE)による匿名化

元データと同じ文字数・同じ文字種のトークンを生成する暗号化手法です。10 桁の数字は 10 桁の数字に、英数字混在の ID は同じ長さの英数字 ID に変換されます。そのため、既存システムの桁数バリデーションや正規表現チェックをそのまま通過でき、アプリケーション側の変更なしに匿名化を組み込めます。

ステップ 5 の決定論的暗号化との本質的な違いは 出力の「形」 です。決定論的暗号化は暗号文を base64 エンコードするため、電話番号 11 桁を入力しても出力トークンは文字数・形式がまったく異なります。一方、FPE は暗号化後も入力と同じフォーマットを維持します。データベースの VARCHAR カラム幅や、クレジットカード番号・電話番号など「そのフォーマットであること」が前提となっているフィールドには FPE が適しています。

どちらも同じキーを使えば再識別可能であり、同じ入力から常に同じトークンを生成するという決定論性も共通しています。違いは形式の制約があるかどうかで選択してください。

NUMERIC アルファベットを指定して電話番号を匿名化します。FPE は入力値の全文字が指定アルファベットに含まれている必要がありますNUMERIC0-9 のみを対象とするため、ハイフン区切り(090-1234-5678)ではなく連続数字(09012345678)形式で渡します。

04_format_preserving_encryption.py
import base64
import os

import google.cloud.dlp

def deidentify_with_fpe(
    project: str,
    input_str: str,
    info_type: str,
    surrogate_type: str,
    alphabet: str,
    unwrapped_key: str,
) -> None:
    """形式保持暗号化(FPE)で PII をトークン化する"""
    dlp = google.cloud.dlp_v2.DlpServiceClient()
    parent = f"projects/{project}/locations/global"

    key_bytes = base64.b64decode(unwrapped_key)

    inspect_config = {
        "info_types": [{"name": info_type}],
        "min_likelihood": google.cloud.dlp_v2.Likelihood.UNLIKELY,
    }

    deidentify_config = {
        "info_type_transformations": {
            "transformations": [
                {
                    "info_types": [{"name": info_type}],
                    "primitive_transformation": {
                        "crypto_replace_ffx_fpe_config": {
                            "crypto_key": {"unwrapped": {"key": key_bytes}},
                            "common_alphabet": alphabet,
                            "surrogate_info_type": {"name": surrogate_type},
                        }
                    },
                }
            ]
        }
    }

    response = dlp.deidentify_content(
        request={
            "parent": parent,
            "deidentify_config": deidentify_config,
            "inspect_config": inspect_config,
            "item": {"value": input_str},
        }
    )
    print(response.item.value)

# 実行例
deidentify_with_fpe(
    project=os.environ["PROJECT_ID"],
    input_str="担当者の電話番号は 09012345678 です。",
    info_type="PHONE_NUMBER",
    surrogate_type="PHONE_TOKEN",
    alphabet="NUMERIC",
    unwrapped_key=os.environ["UNWRAPPED_KEY"],
)

電話番号(11 桁)が同じ桁数の数字列トークンに置き換えられます。

python 04_format_preserving_encryption.py
担当者の電話番号は PHONE_TOKEN(11):69389318423 です。

ステップ 7:辞書からランダム置換

検出した PII を、あらかじめ用意した単語リストからランダムに選んだ値に置き換える手法です。[EMAIL_ADDRESS] のような機械的なラベルではなく、「Aさん」「Bさん」など読み手が自然に受け取れるダミー値に置換したい場合に有用です。

毎回ランダムに値が選ばれるため、同じ入力値でも実行のたびに異なる置換値になります。参照整合性は保たれないので、複数テーブルにまたがる匿名化や、JOIN を維持したいケースには使えません。ダミーデータを手動管理したくない場面(例: 開発環境へのデータ投入)で活用してください。

人名をダミー名リストからランダムに置き換える例です。

05_replace_dictionary.py
import os

import google.cloud.dlp

def deidentify_with_replace_dictionary(project: str, input_str: str) -> None:
    """検出した人名をダミー名リストからランダムに置き換える"""
    dlp = google.cloud.dlp_v2.DlpServiceClient()
    parent = f"projects/{project}/locations/global"

    inspect_config = {"info_types": [{"name": "PERSON_NAME"}]}

    deidentify_config = {
        "info_type_transformations": {
            "transformations": [
                {
                    "info_types": [{"name": "PERSON_NAME"}],
                    "primitive_transformation": {
                        "replace_dictionary_config": {
                            "word_list": {"words": ["Aさん", "Bさん", "Cさん"]}
                        }
                    },
                }
            ]
        }
    }

    response = dlp.deidentify_content(
        request={
            "parent": parent,
            "deidentify_config": deidentify_config,
            "inspect_config": inspect_config,
            "item": {"value": input_str},
        }
    )
    print(response.item.value)

deidentify_with_replace_dictionary(
    project=os.environ["PROJECT_ID"],
    input_str="担当者の田中太郎と山田花子が会議に出席した。",
)

検出された人名がリストの中からランダムに選んだ値に置き換えられます(実行ごとに結果が変わります)。

python 05_replace_dictionary.py
担当者のAさんとCさんが会議に出席した。

ステップ 8:固定サイズバケット化

数値フィールドを固定間隔の範囲ラベルに変換する手法です。年齢を 10 歳刻み・収入を 100 万円刻みにグルーピングすることで、個人特定に繋がる細かい数値を粗くします。以降のステップ 8〜11 は 構造化データ(テーブル) を対象とした RecordTransformations を使用します。テキストではなく Table 形式で入力します。

バケットは lower_boundupper_bound の範囲を bucket_size 間隔で自動分割します。境界値の扱いは「lower_bound 以上 upper_bound 未満」となります。

年齢を 10 歳刻みのバケットに変換する例です。

06_fixed_size_bucketing.py
import os

import google.cloud.dlp

def _val(v):
    if v.string_value:
        return v.string_value
    if v.integer_value:
        return str(v.integer_value)
    return ""

def deidentify_with_fixed_size_bucketing(project: str) -> None:
    """年齢フィールドを 10 歳刻みのバケットに変換する"""
    dlp = google.cloud.dlp_v2.DlpServiceClient()
    parent = f"projects/{project}/locations/global"

    table = {
        "headers": [{"name": "名前"}, {"name": "年齢"}],
        "rows": [
            {"values": [{"string_value": "田中太郎"}, {"integer_value": 28}]},
            {"values": [{"string_value": "山田花子"}, {"integer_value": 45}]},
            {"values": [{"string_value": "鈴木次郎"}, {"integer_value": 67}]},
        ],
    }

    deidentify_config = {
        "record_transformations": {
            "field_transformations": [
                {
                    "fields": [{"name": "年齢"}],
                    "primitive_transformation": {
                        "fixed_size_bucketing_config": {
                            "lower_bound": {"integer_value": 0},
                            "upper_bound": {"integer_value": 100},
                            "bucket_size": 10.0,
                        }
                    },
                }
            ]
        }
    }

    response = dlp.deidentify_content(
        request={
            "parent": parent,
            "deidentify_config": deidentify_config,
            "item": {"table": table},
        }
    )

    result = response.item.table
    print(" | ".join(h.name for h in result.headers))
    for row in result.rows:
        print(" | ".join(_val(v) for v in row.values))

deidentify_with_fixed_size_bucketing(project=os.environ["PROJECT_ID"])

各年齢が属するバケットの範囲に置き換えられます。

python 06_fixed_size_bucketing.py
名前 | 年齢
田中太郎 | 20:30
山田花子 | 40:50
鈴木次郎 | 60:70

ステップ 9:カスタムバケット化

ステップ 8 の固定サイズバケット化と同じく数値を範囲に変換しますが、バケットの境界と置換ラベルを自由に定義できます。業務上意味のある区分(「低・中・高」「合格・不合格」など)に変換したい場合に使用します。

最小側のバケットは min を省略(マイナス無限大)、最大側のバケットは max を省略(プラス無限大)することで端の範囲を表現します。

スコアを「低・中・高」に分類する例です。

07_bucketing.py
import os

import google.cloud.dlp

def _val(v):
    if v.string_value:
        return v.string_value
    if v.integer_value:
        return str(v.integer_value)
    return ""

def deidentify_with_bucketing(project: str) -> None:
    """スコアフィールドを任意の区分ラベルに変換する"""
    dlp = google.cloud.dlp_v2.DlpServiceClient()
    parent = f"projects/{project}/locations/global"

    table = {
        "headers": [{"name": "名前"}, {"name": "スコア"}],
        "rows": [
            {"values": [{"string_value": "田中太郎"}, {"integer_value": 25}]},
            {"values": [{"string_value": "山田花子"}, {"integer_value": 55}]},
            {"values": [{"string_value": "鈴木次郎"}, {"integer_value": 80}]},
        ],
    }

    deidentify_config = {
        "record_transformations": {
            "field_transformations": [
                {
                    "fields": [{"name": "スコア"}],
                    "primitive_transformation": {
                        "bucketing_config": {
                            "buckets": [
                                {
                                    "max": {"integer_value": 30},
                                    "replacement_value": {"string_value": "低"},
                                },
                                {
                                    "min": {"integer_value": 30},
                                    "max": {"integer_value": 60},
                                    "replacement_value": {"string_value": "中"},
                                },
                                {
                                    "min": {"integer_value": 60},
                                    "replacement_value": {"string_value": "高"},
                                },
                            ]
                        }
                    },
                }
            ]
        }
    }

    response = dlp.deidentify_content(
        request={
            "parent": parent,
            "deidentify_config": deidentify_config,
            "item": {"table": table},
        }
    )

    result = response.item.table
    print(" | ".join(h.name for h in result.headers))
    for row in result.rows:
        print(" | ".join(_val(v) for v in row.values))

deidentify_with_bucketing(project=os.environ["PROJECT_ID"])

各スコアが対応するラベルに置き換えられます。

python 07_bucketing.py
名前 | スコア
田中太郎 |
山田花子 |
鈴木次郎 |

ステップ 10:日時一部抽出

日付・時刻フィールドから指定した部分のみを残し、残りを削除する手法です。part_to_extractYEARMONTHDAY_OF_MONTHDAY_OF_WEEKHOUR_OF_DAY などを指定できます。

生年月日から年だけを残すことで、個人特定リスクを下げながら年代別の統計分析に必要な情報を保持できます。入力は date_value 型のフィールドを持つ Table で渡します。

生年月日から年のみを残す例です。

08_time_part.py
import os

import google.cloud.dlp

def _val(v):
    if v.string_value:
        return v.string_value
    if v.integer_value:
        return str(v.integer_value)
    d = v.date_value
    if d.year:
        if d.month:
            return f"{d.year}-{d.month:02d}-{d.day:02d}" if d.day else f"{d.year}-{d.month:02d}"
        return str(d.year)
    return ""

def deidentify_with_time_part(project: str) -> None:
    """生年月日フィールドから年のみを残す"""
    dlp = google.cloud.dlp_v2.DlpServiceClient()
    parent = f"projects/{project}/locations/global"

    table = {
        "headers": [{"name": "名前"}, {"name": "生年月日"}],
        "rows": [
            {
                "values": [
                    {"string_value": "田中太郎"},
                    {"date_value": {"year": 1990, "month": 3, "day": 15}},
                ]
            },
            {
                "values": [
                    {"string_value": "山田花子"},
                    {"date_value": {"year": 1985, "month": 7, "day": 22}},
                ]
            },
        ],
    }

    deidentify_config = {
        "record_transformations": {
            "field_transformations": [
                {
                    "fields": [{"name": "生年月日"}],
                    "primitive_transformation": {
                        "time_part_config": {"part_to_extract": "YEAR"}
                    },
                }
            ]
        }
    }

    response = dlp.deidentify_content(
        request={
            "parent": parent,
            "deidentify_config": deidentify_config,
            "item": {"table": table},
        }
    )

    result = response.item.table
    print(" | ".join(h.name for h in result.headers))
    for row in result.rows:
        print(" | ".join(_val(v) for v in row.values))

deidentify_with_time_part(project=os.environ["PROJECT_ID"])

月・日が削除され、年のみが残ります。

python 08_time_part.py
名前 | 生年月日
田中太郎 | 1990
山田花子 | 1985

ステップ 11:日付シフト

日付を指定した日数の範囲内でランダムにずらす手法です。生年月日・診察日・購入日など厳密な日付は隠しつつ、「入院期間の長さ」「イベント間の間隔」といった相対的な時間関係を維持できます。医療・行動ログ系のデータを統計分析に提供する際に特に有用です。

lower_bound_daysupper_bound_days でシフト幅を制御します。シフト量は毎回ランダムに決まるため、複数レコードをまたいで参照整合性を保ちたい場合は、date_shift_config.context にフィールドを指定して同一レコード内での一貫性を持たせます。

生年月日を ±30 日の範囲でランダムにシフトする例です。

09_date_shift.py
import os

import google.cloud.dlp

def _val(v):
    if v.string_value:
        return v.string_value
    d = v.date_value
    if d.year:
        return f"{d.year}-{d.month:02d}-{d.day:02d}"
    return ""

def deidentify_with_date_shift(project: str) -> None:
    """生年月日フィールドの日付を ±30 日の範囲でランダムにシフトする"""
    dlp = google.cloud.dlp_v2.DlpServiceClient()
    parent = f"projects/{project}/locations/global"

    table = {
        "headers": [{"name": "名前"}, {"name": "生年月日"}],
        "rows": [
            {
                "values": [
                    {"string_value": "田中太郎"},
                    {"date_value": {"year": 1990, "month": 3, "day": 15}},
                ]
            },
            {
                "values": [
                    {"string_value": "山田花子"},
                    {"date_value": {"year": 1985, "month": 7, "day": 22}},
                ]
            },
        ],
    }

    deidentify_config = {
        "record_transformations": {
            "field_transformations": [
                {
                    "fields": [{"name": "生年月日"}],
                    "primitive_transformation": {
                        "date_shift_config": {
                            "lower_bound_days": -30,
                            "upper_bound_days": 30,
                        }
                    },
                }
            ]
        }
    }

    response = dlp.deidentify_content(
        request={
            "parent": parent,
            "deidentify_config": deidentify_config,
            "item": {"table": table},
        }
    )

    result = response.item.table
    print(" | ".join(h.name for h in result.headers))
    for row in result.rows:
        print(" | ".join(_val(v) for v in row.values))

deidentify_with_date_shift(project=os.environ["PROJECT_ID"])

日付が ±30 日の範囲でランダムにずれた値に置き換えられます(実行ごとに結果が変わります)。

python 09_date_shift.py
名前 | 生年月日
田中太郎 | 1990-03-18
山田花子 | 1985-08-20

まとめ

今回は、Sensitive Data Protectionを利用して各匿名化手法を試してみました。

Sensitive Data Protection の content.deidentify メソッド を使うことで、テキスト中の PII をわずかなコードで検出・匿名化できます。

100 以上の組み込み infoType が用意されており、メールアドレス・電話番号・人名といった多様な PII を追加実装なしに検出できます。変換手法もマスキング・infoType 名置換・暗号化など用途に応じて選択でき、開発環境への安全なデータ提供から参照整合性を保ったトークン化まで幅広いシナリオに対応しています。REST API と各言語のクライアントライブラリが用意されているため、既存のアプリケーションへの組み込みも容易です。

一方で、いくつかの点に注意が必要です。組み込み infoType はパターンマッチングや機械学習を組み合わせた検出を行いますが完全な精度は保証されず、偽陽性・偽陰性が発生することがあるため、本番利用前には実データで十分な検証を行ってください。また、content.deidentify メソッド にはリクエストサイズの制限があり、大量データを処理する場合は Cloud Storage や BigQuery と連携したバッチジョブ(DLP Job)の利用を検討する必要があります。なお、組み込みでは対応していない独自の機密データ(社員 ID など)には、正規表現や辞書を使ったカスタム infoType を作成して対応できます。

データを扱う開発者やデータエンジニアの方は、開発環境への本番データ持ち込みや、ログの PII 漏洩防止にぜひ Sensitive Data Protection の導入を検討してみてはいかがでしょうか。

この記事が誰かの助けになれば幸いです。

以上、クラウド事業本部コンサルティング部の渡邉でした!

この記事をシェアする

関連記事