💡 完全な動作例はGitHubで利用可能です:
sanitize-office-document-pii-python

送信前に誰もレビューしないデータ

四半期ごとの取締役会レポートが外部監査人に送られます。テキストは完璧で、3回のレビューサイクルで確認されています。しかし、ファイル自体は別です。プロパティには、ドラフトしたアナリスト、再編集したマネージャー、テンプレートを所有する子会社、締め切り前夜の LastPrinted タイムスタンプ、内部承認ワークフローの SharePoint 承認者 ID が残っています。これらはページ上には表示されませんが、ファイルと一緒に搬送されます。

PII の除去は、.NET 経由の Python 用 GroupDocs.Metadata ワークフローで、Word、Excel、PowerPoint ファイルからこれらの個人情報をプログラム的に削除します。本記事では、API が提供する 3 つのアプローチを比較します。タグ駆動の ID フィールド除去、コメントやリビジョンなどのプロパティファミリー向けの名前パターン除去、そしてすべてをクリアするワンコール sanitize()。また、ほとんどのサニタイズスクリプトが省略しがちな、クリーンアップが実際に行われたことを証明する検証スキャンも紹介します。

なぜメタデータ PII は独自のパイプラインが必要か

コンテンツレビュー ツールは人が読むものをチェックしますが、ファイルシステムが保存しているものはチェックしません。このギャップがコンプライアンスインシデントの原因となります。GDPR の要求は、Author や Manager フィールドにある個人データをテキスト中のデータと同等に扱います。法的ディスカバリーはリビジョンカウンタや編集時間合計を読み取り、ポジションペーパーがどれだけ交渉されたかを再構築します。入札審査員は SharePoint ワークフロープロパティから組織構造を把握でき、プレスリリースのコメントフィールドはレビューア名とドラフト段階のコメントを保持します。これらはすべて調査対象ですが、文書本文には表示されません。

前提条件

開始する前に以下を確認してください。

  • Python 3 と pip
  • .NET 経由の Python 用 GroupDocs.Metadata、サンプルリポジトリでバージョン 26.5 に固定
  • 実際のプロパティが付いた Office ファイル(練習用)

インストール

pip install groupdocs-metadata-net==26.5

companion repository にはサンプル DOCX が用意されており、以下のスニペットがすべて検証済みパイプラインとして実行されます。

方法 1: タグ駆動の ID 除去

最も機密性の高い 4 つのフィールド、Author、LastSavedBy、Manager、Company は、Office フォーマットごとに内部名が異なります。タグシステムはこれを解決します。プロパティ名を指定する代わりに、人物または企業としてタグ付けされたすべてを対象にします。

# フォーマット固有の名前ではなく意味で ID プロパティをマッチ
with Metadata("board-report.docx") as metadata:
    removed = metadata.remove_properties(lambda p:
        Tags.person.creator in list(p.tags)     # Author, LastSavedBy
        or Tags.person.editor in list(p.tags)
        or Tags.person.manager in list(p.tags)
        or Tags.corporate.company in list(p.tags))
    metadata.save("board-report-clean.docx")

print(f"{removed} identity properties removed")

重要ポイント:

  • フォーマット非依存: 同じ lambda が DOCX、XLSX、PPTX をクリーンにできるのは、タグが役割で分類するためです。
  • カウント可能な結果: remove_properties はマッチしたプロパティ数を返し、監査ログに記録できます。
  • コピーセマンティクス: 新しいパスに保存することで、元ファイルを記録として残せます。

💡 ヒント: このパスは Title、Subject、その他の記述フィールドを保持するため、検索や DMS インデックス付けに優しいままです。

方法 2: プロパティファミリー向け名前パターン除去

タグは分類された概念をカバーしますが、漏れやすいフィールドのファミリー(コメントプロパティ、リビジョンカウンタ、SharePoint ワークフロースタンプ)はタグ分類の外にあります。これらはプロパティ名自体でマッチさせます。

# コメントフィールドはタグシステムが分類しないカスタムプロパティに
# 多く存在するため、名前の部分文字列でマッチ
with Metadata("board-report.docx") as metadata:
    removed = metadata.remove_properties(lambda p:
        p.name is not None and (
            "Comment" in p.name
            or "Reviewer" in p.name
            or "Reviewed" in p.name))
    metadata.save("board-report-no-comments.docx")

同じ形で他の 2 つのファミリーも処理できます。変更するのは部分文字列リストだけです。

ファミリー マッチさせる部分文字列
リビジョントレイル Revision, TrackedChange, LastPrinted, TotalEditingTime, EditTime
サーバー / ワークフロー Server, Workflow, Approver, ContentType, Template

精度より網羅性を重視したアプローチです。"Comment"CommentsCommentCount も捕捉し、サニタイズ時に期待される動作です。広い部分文字列は無害なテンプレートフィールドもマッチする可能性があるため、返されたカウントを期待値と照合してください。

💡 ヒント: 監査ログでカテゴリ別カウントが必要な場合は各ファミリーを個別に実行し、必要なければ部分文字列を 1 つの述語に統合してください。

方法 3: ワンコール フルサニタイズ

組織外へファイルを出す際、メタデータ層に残すものが何もない場合は、述語を書かずに実行します。

# ワンコールで検出されたすべてのメタデータパッケージを削除
with Metadata("board-report.docx") as metadata:
    removed = metadata.sanitize()
    metadata.save("board-report-final.docx")

print(f"sanitize() removed {removed} properties")

sanitize() はライブラリが検出するすべてのパッケージをクリアします。ドキュメント情報の ID フィールド、コメント、リビジョン履歴、トラッキング変更の作者、カスタム OOXML パーツなどです。動作は Clean metadata ページに記載されています。その強みは同時にコストでもあります。Title と Subject も PII と共に消えるため、コラボレーション途中ではなくエクスポートゲートで使用すべきです。

すべての 4 つのターゲットパスが必要?

いいえ。各パスはリスクを所有するチームが異なるために存在します。ID フィールドはプライバシー担当者、コメントトレイルは法務、リビジョンカウンタは交渉担当者、サーバーフィールドはセキュリティを悩ませます。レビュー担当者に合わせて任意の順序で実行してください。残すフィールドが不要な場合は sanitize() に直行し、検証を行います。

3 つのアプローチの比較

方法 最適な用途 主な利点 制限事項
タグ駆動除去 作業コピー、マルチフォーマットパイプライン フォーマット非依存、記述フィールドを保持 タグで分類された概念のみ対象
名前パターン除去 コメント、リビジョン、サーバーフィールド タグが見逃すカスタムプロパティに到達 環境ごとに部分文字列の調整が必要
フル sanitize() 組織外への最終エクスポート 見落としがない 無害なフィールドも削除される

これらは自然に組み合わせられます。ドキュメントが生きている間はターゲットパスで、出荷時は sanitize() を使用します。

信頼する前に検証する

除去呼び出しがカウントを返しただけでは、ファイルがクリーンである証拠にはなりません。リポジトリはすべての実行後にサニタイズ済み出力を再度開き、find_properties で上記すべてのタグルールと名前ルールを組み合わせた述語でスキャンします。

def is_pii(p):
    if p.name is None:
        return False
    return (
        Tags.person.creator in list(p.tags)
        or Tags.person.editor in list(p.tags)
        or Tags.person.manager in list(p.tags)
        or Tags.corporate.company in list(p.tags)
        or any(n in p.name for n in (
            "Comment", "Reviewer", "Revision", "TrackedChange",
            "Classification", "Department", "Server", "Workflow")))

with Metadata("board-report-final.docx") as metadata:
    for p in metadata.find_properties(is_pii):
        value = (str(p.interpreted_value) if p.interpreted_value is not None
                 else (str(p.value) if p.value is not None else ""))
        if value and value not in ("0", "0.0"):
            print(f"LEAK {p.name}={value}")

リポジトリの完全版は残存項目を 2 つのバケットに分類し、区別が重要であることを示しています。メタデータ漏洩はゼロでなければなりません。コンテンツレベルの残存物(Word のコメントバルーンや word/document.xml 内のトラッキング変更)はメタデータ API では到達できないため、Aspose.Words などのコンテンツ編集ライブラリが必要です。正直なレポートは両バケットを列挙し、最初のバケットだけで勝利宣言しません。私が「クリーン」ファイルでこのスキャンを初めて走らせたとき、数か月間静かに再追加されていたテンプレートの Department フィールドがフラッグされました。

ベストプラクティスとヒント

  • コピーをサニタイズし、元ファイルは残す: ここにあるすべてのスニペットは新しいパスに書き込み、ソースは記録と保持ポリシーのために残します。
  • カウントをログに残す: remove_propertiessanitize() の戻り値が監査トレイルになります。ファイルごと、パスごとに保存してください。
  • CI に検証を組み込む: ビルドを失敗させる漏洩チェックは、テンプレートのリグレッションをクライアントが気付く前に検出します。
  • メタデータとコンテンツの境界を意識: 本文レベルのコメントが残っている状態で「クリーン」と報告しないでください。別の検出項目として提示します。
  • ライセンス: 評価モードは本記事のすべてを再現しますが、本番環境ではライセンスを使用し、評価マークが外部ファイルに残らないようにしてください。

結論

3 つのアプローチ、1 つの判断基準。概念がタグ付けされていてファイルを有用に保ちたい場合はタグでマッチ。カスタムプロパティに属するファミリーは名前でマッチ。信頼境界を越えるときは sanitize() を呼び、どのルートを取っても再読スキャンで検証します。

さらに深く学びたいですか?次のステップをご覧ください。

追加リソース

質問がある、または実装を共有したい場合は、support forum でお問い合わせください。