💡 完全な動作例は GitHub で入手可能です:
remove-pii-from-office-metadata-java
The Compliance Challenge: Why Manual Metadata Review Breaks at Scale
レコードチームが 200 件の文書を外部監査人に送ります。誰かがすべてのページを読んだとしても、プロパティは読まれていません。プロパティこそが個人データの所在です。Author にアナリスト、LastSavedBy に別のアナリスト、Manager に部門長、Company に子会社、締切前日の LastPrinted タイムスタンプ、そして SharePoint を通過したすべてのファイルには承認者 ID とワークフローパスが残ります。
Metadata のサニタイズは、Word、Excel、PowerPoint ファイルから個人情報を含むプロパティを除去し、結果を読み取って残存項目をレポートする GroupDocs.Metadata の Java ワークフローです。本稿では、コンプライアンスチームが構築する手順を追いながら、どのプロパティグループが存在し、どの除去ルールが適合し、完全なワイプが対象パスに取って代わるタイミング、そして検証がチェックリストではなく同一ジョブ内にあるべき理由を解説します。
スケールの問題は除去が難しいことではありません。手動レビューが記録を残さないことが問題です。監査人が「このファイルからどのフィールドがいつ削除されたか」を尋ねたとき、数値が必要ですが、プロパティダイアログは何も提供しません。
Why Generic Cleanup Tools Don’t Work Here
Windows のプロパティダイアログはファイルを 1 つずつしか編集できず、対象フィールドも限定的です。Office の Document Inspector は対話的に動作するため、夜間ジョブには不向きです。どちらもカスタム OOXML パーツには手を付けず、パイプラインが再取得できる形で書き出すこともありません。
docProps/core.xml や docProps/custom.xml を直接編集するチームは、メンテナンス負荷を背負います。フィールドごと、フォーマットごとに XPath 式を用意し、生成アプリが名前を変更するたびに見直しが必要です。また、分類の観点でも問題があります。パッケージ間でプロパティ名が異なるため、名前リストは静かに古くなり、マッチしなくなったルールはすでにクリーンなファイルと見分けがつきません。
The Solution: GroupDocs.Metadata in a Records Workflow
GroupDocs.Metadata for Java はすべてを単一のプロパティ検索エンジンに通します。Specification オブジェクトがどのプロパティがマッチするかを決定し、removeProperties が一致したものを削除して影響件数を返し、findProperties が同じ述語で読み取り専用検索を行います。プロパティにはタグが付与されているため、Tags.getPerson().getCreator() はフォーマットやパッケージに関係なく作者系フィールドを特定できます。
Java には removeProperties のラムダオーバーロードがないため、ここではむしろ利点となります。各ルールはオブジェクトとして表現され、再利用可能です。クリーンアップに使用した同じ Specification インスタンスを検証スキャンに渡すことで、チェックがクリーンアップから逸脱することを防げます。
Implementing the Sanitization Pipeline Step by Step
Step 1 - Clear the identity group by tag
.or(...) で結合した 4 つのタグ仕様が、作成者、編集者、マネージャー、会社をカバーします。その他は移動させませんので、Title、Subject、Keywords はレコードインデックスで利用可能です。
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.removeProperties(
new ContainsTagSpecification(Tags.getPerson().getCreator())
.or(new ContainsTagSpecification(Tags.getPerson().getEditor()))
.or(new ContainsTagSpecification(Tags.getPerson().getManager()))
.or(new ContainsTagSpecification(Tags.getCorporate().getCompany())));
metadata.save(outputPath);
return affected;
}
FileFormat.Unknown ガードは見た目以上に重要です。これがないと、読めないファイルでも 0 件の削除とみなされ、呼び出し側は「最初からクリーンだった」のか「読めなかった」のか区別できません。
Step 2 - Clear field families by name
コメントスレッド、リビジョンカウンタ、サーバーフィールドにはタグが付与されていないため、名前でマッチさせます。小さな Specification サブクラスが可変長のサブストリングリストを受け取り、1 つのクラスで 3 回のパスを実装します。
public class NameContainsSpec extends Specification {
private final String[] needles;
public NameContainsSpec(String... needles) {
this.needles = needles;
}
@Override
public boolean isSatisfiedBy(MetadataProperty candidate) {
String name = candidate.getName();
if (name == null) return false;
for (String n : needles) {
if (name.contains(n)) return true;
}
return false;
}
}
編集タイムラインはコンプライアンスチームが最も関心を持つ項目で、リビジョン数や最終印刷日が文書の生成過程を示します。
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.removeProperties(new NameContainsSpec(
"Revision", "TrackedChange", "LastPrinted", "TotalEditingTime", "EditTime"));
metadata.save(outputPath);
return affected;
}
コメントパスと SharePoint パスは、サブストリングリストを変えるだけの同一呼び出しです。レビュー痕跡用に Comment, Reviewer, Reviewed、ドキュメントサーバーフィールド用に Server, Workflow, Approver, ContentType, Template を指定します。
Step 3 - Wipe at the boundary
組織を出るファイルに対しては選択的除去は意味を成しません。1 回の呼び出しでライブラリが検出するすべてのメタデータパッケージ、カスタム OOXML パーツも含めて一括削除します。
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.sanitize();
metadata.save(outputPath);
return affected;
}
Step 4 - Verify and classify what is left
スキャンはすべてのルールの合併集合を findProperties で走査し、ヒットを 2 つのリストに振り分けます。空値や 0 カウンタは除外し、名前が Comment, Revision, Inspection で始まるエントリはメタデータではなく本文コンテンツのラッパーとして扱います。
String value = "";
if (p.getValue() != null && p.getValue().getRawValue() != null) {
value = String.valueOf(p.getValue().getRawValue());
}
if (value.isEmpty() || value.equals("0") || value.equals("0.0")) continue;
String entry = p.getName() + "=" + value;
String name = p.getName() == null ? "" : p.getName();
if (name.startsWith("Comment") || name.startsWith("Revision")
|| name.startsWith("Inspection")) {
report.contentLevelLeaks.add(entry);
} else {
report.metadataLeaks.add(entry);
}
この分割が合否シグナルを正確に保ちます。メタデータリークは空でなければなりません。コンテンツレベルのリークは情報提供に留めます。Word のコメントやトラッキング変更の作者は word/document.xml 内にあり、メタデータライブラリは編集せずに報告します。これらを除去するには Aspose.Words のようなコンテンツ編集ライブラリが必要です。
When is a targeted pass better than a full sanitize?
文書がまだ共同作業中である場合です。レビューア間で回るファイルは検索やレコード分類のために Title、Subject、Keywords が必要ですが、sanitize() はそれらすべてを削除します。共同作業中は身元情報とコメントパスだけを実行し、記述フィールドは残しておき、外部に渡す瞬間にだけ完全ワイプを行います。
Real Workflow: An Export Job for an External Auditor
夜間ジョブを想像してください。文書 ID のリストを読み取り、各ファイルをステージングパスにコピーし、身元パスとサーバーパスを適用し、組織外へ出るものに対して sanitize() を呼び出し、保存コピーに対してリークチェックを実行します。各ステップはファイルごとの影響件数を 1 行のログに加算し、メタデータリークリストが空でなければジョブは失敗として扱われ、警告ではなくエラーになります。
かつて 40 ファイルで「削除件数 0」のレポートが出て「クリーン」だと思ったことがありますが、入力フォルダにレガシーな .doc バイナリがあり、フォーマットガードがすべて早期リターンしていたためです。ログにフォーマット情報と件数を併記すれば問題は解決しました。
Business Impact: What This Changes
| 項目 | 手動レビュー | GroupDocs.Metadata パイプライン |
|---|---|---|
| カバレッジ | プロパティダイアログに表示されるフィールド | ライブラリが検出するすべてのパッケージ(カスタム OOXML パーツ含む) |
| 作業記録 | メモ(書いた人が残す) | ファイルごとの影響件数とプロパティグループ別件数 |
| 再現性 | 担当者に依存 | ルールごとの Specification が全ファイルに同一に適用 |
| 検証 | ファイルを再度開いて目視 | findProperties スキャンと 2 段階分類 |
| スケール | 1 ファイルずつ | エクスポートフォルダ全体を同一 6 操作でループ処理 |
Other Scenarios Where GroupDocs.Metadata Fits
同じプロパティエンジンは読み取りも削除も行います。2 つのバージョン間のメタデータ比較は、どの編集ラウンドで変更があったかを示し、所有権争いの解決やプロセス外で再執筆されたファイルの特定に有用です。名前ではなく metadata tags を使うことで、DOCX、XLSX、PPTX、PDF、画像フォーマット間でポータブルに利用できます。
Getting Started with GroupDocs.Metadata for Java
pom.xml に GroupDocs Java リポジトリを追加し、com.groupdocs:groupdocs-metadata に依存します。ライセンスなしの評価モードでもサンプル DOCX に対して 6 操作すべてを実行し、件数を確認できます。まず身元パスを実装し、文書ソースに応じてフィールドファミリーパスを追加し、いずれかが本番に入る前にリークチェックを組み込みます。