💡 Full working example available on 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 sanitization は Java 用 GroupDocs.Metadata のワークフローで、Word、Excel、PowerPoint ファイルから個人情報が含まれるプロパティを削除し、結果を再度読み取って残存した項目をレポートします。本稿では、コンプライアンスチームが構築する手順を追いながら、どのプロパティグループが存在し、どの削除ルールが各グループに適合し、完全消去がターゲット削除に置き換わるタイミング、そして検証がチェックリストではなく同一ジョブに組み込まれるべき理由を解説します。
スケールの問題は「削除が難しい」ことではありません。手動レビューが記録を残さないことが問題です。監査人が「どのフィールドがいつ削除されたか」を尋ねる際、数値が必要ですが、プロパティダイアログは何も提供しません。
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
Java 用 GroupDocs.Metadata はすべての検索を 1 つのプロパティ検索エンジンで実行します。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 ガードは見た目以上に重要です。読み取れないファイルがゼロ件の削除として扱われると、呼び出し側は「クリーンだった」のか「読めなかった」のか区別できません。
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 つのリストに振り分けます。空値やゼロカウントは除外し、名前が 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() はそれらすべてを削除します。共同作業中は ID とコメントのパスだけを実行し、記述的フィールドは残し、外部へ渡す瞬間に完全消去を行うのがベストです。
Real Workflow: An Export Job for an External Auditor
夜間ジョブを想像してください。ドキュメント ID のリストを読み込み、各ファイルをステージングパスへコピーし、ID パスとサーバーパスを適用し、組織外へ出ると判定されたものに sanitize() を呼び出し、保存コピーに対して漏洩チェックを実行します。各ステップはファイルごとの影響件数を 1 行のログに集約し、メタデータ漏洩リストが空でなければジョブは失敗として扱われ、警告ではなくエラーになります。
かつてこのジョブのバージョンで 40 ファイルすべてが「削除件数 0」と報告され、クリーンバッチに見えたことがあります。原因は入力フォルダにレガシーな .doc バイナリが混在しており、フォーマットガードがすべて早期リターンしていたため、ログに「削除対象なし」と「読み取れなかった」の区別がつかなかったことです。フォーマット情報と件数を同時にログ出力すれば解決しました。
Business Impact: What This Changes
| Aspect | Manual review | GroupDocs.Metadata pipeline |
|---|---|---|
| Coverage | properties ダイアログに表示されるフィールド | ライブラリが検出するすべてのパッケージ(カスタム OOXML パーツ含む) |
| Record of the work | メモ(書いた人がいる場合) | ファイルごとの影響件数とプロパティグループごとの件数 |
| Repeatability | 実施者に依存 | 1 つの Specification がすべてのファイルに同一に適用 |
| Verification | ファイルを再度開いて目視 | findProperties による二方向分類スキャン |
| Scale | 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 つの操作を実行し件数を確認できます。まずは ID パスを実装し、ドキュメントソースに応じてフィールドファミリーパスを追加し、いずれかが本番に入る前に漏洩チェックを組み込みます。