💡 完全に動作するサンプルは GitHub で入手できます:
document-version-metadata-diff-python

作成するもの

このガイドでは、ドキュメントの 2 つのバージョン間のすべてのメタデータ プロパティを比較し、追加・削除・変更された項目を正確に出力します。メタデータ バージョン差分は、同一ファイルの 2 つのリビジョンをプロパティ単位で比較するもので、テキスト比較では見逃しがちなシグナル(新しい Creator、RevisionNumber の増加、レビュー終了後に記録された編集セッションなど)を捕らえます。最終的に、サンプルのリビジョン ペアが含まれた実行可能リポジトリから取得した、動作するソリューションと 2 つの特化検出器、2 つのエクスポート形式を手に入れることができます。

スキルレベル: 中級 Python 開発者
必要なもの: Python 3、pip、1 つのドキュメントの 2 つのリビジョン

私がこのスクリプトを最初に実行したとき、チームの誰も覚えていなかった Company 値の変更が検出されました。その 1 行がセットアップ費用を相殺しました。以下の内容はすべてコピー&ペーストで使用でき、行数は 100 行未満です。

パイプラインは意図的にシンプルです: ファイルを 2 回開き、辞書内包表記を 3 回、プリントループを 1 回。シンプルさがポイントです。バージョン争いは、手法が説明可能で再現可能かどうかで決まります。この程度の小さなスクリプトであれば、結果に異議を唱える者も全体を読めます。


1. インストール

pip install groupdocs-metadata-net==26.5

companion repository はこのバージョンを固定し、document-v1.docxdocument-v2.docx を同梱しているため、以下のコードはそのまま実行できます。監査で使用したバージョンを固定してください。再現性は証拠の一部です。


2. コアコード

両方のプロパティ ツリーを読み取り、集合ロジックで差分を分類します。これが全体の差分ロジックです:

# Flatten a file's complete property tree into a dict
def read_props(path):
    props = {}
    with Metadata(path) as metadata:
        for p in metadata.find_properties(lambda p: p.name is not None):
            props[p.name] = (str(p.interpreted_value) if p.interpreted_value is not None
                             else (str(p.value) if p.value is not None else ""))
    return props

v1 = read_props("resources/document-v1.docx")
v2 = read_props("resources/document-v2.docx")

# Classify every key; changed entries keep both values
added = {k: v for k, v in v2.items() if k not in v1}
removed = {k: v for k, v in v1.items() if k not in v2}
changed = {k: (v1[k], v2[k]) for k in v1 if k in v2 and v1[k] != v2[k]}

print(f"added={len(added)} removed={len(removed)} changed={len(changed)}")
for k, (old_v, new_v) in changed.items():
    print(f"  {k}: {old_v} -> {new_v}")

これが最低限必要なコードです。本物のリビジョン ペアではカウントは少なくなるはずです。数十件の差分が出た場合は、テンプレート変更やストレージ移行が途中で行われた可能性があります。次のセクションでは主要な呼び出しを解説し、チームが最初に追加しがちなカスタマイズ例を示します。


3. 動作概要

  • Metadata: ファイルを開き、終了時に解放するコンテキストマネージャ。リビジョンごとに 1 インスタンス。
  • find_properties: 組み込みフィールド、カスタム プロパティ、XMP を一括で走査し、述語が受け入れるすべてを返す。
  • interpreted_value: 人が読める形のプロパティ値。これを優先すると、日付や列挙型が文字列として比較でき、レポートにそのまま出力できる。
  • キーとしての完全修飾名: 組み込みフィールドとカスタム フィールドが辞書内で衝突しないため、集合ロジックが安全に機能する。

ここでは DOCX 構造を解析していません。product documentation には同一呼び出しで 170 以上のフォーマットがサポートされていると記載されているので、PDF や XLSX のペアでも同じスクリプトで差分が取れます。

設計上もう 1 つ重要なのは、API の境界が read_props の 2 呼び出しで止まっている点です。その後はすべて標準ライブラリの Python だけなので、ユニットテストやしきい値、アラート ルールがドキュメント層に触れることはありません。多くのチームはこのロジックをサービス化する際、リビジョンごとに抽出した辞書をキャッシュし、下流のチェックがそれを再利用できるようにしています。これにより、質問の数に関わらずファイル I/O はバージョンごとに 1 回のオープンに抑えられます。


4. 一般的なカスタマイズ

所有権変更のみを検出

「誰がこのファイルに触れたか?」という質問に対しては、フル差分を後からフィルタリングするのではなく、読み取り時にタグ述語で絞り込みます:

# Identity fields only, whatever the format calls them
def read_ownership(path):
    result = {}
    with Metadata(path) as metadata:
        props = metadata.find_properties(lambda p:
            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))
        for prop in props:
            result[prop.name] = (str(prop.interpreted_value)
                                 if prop.interpreted_value is not None
                                 else (str(prop.value) if prop.value is not None else ""))
    return result

この辞書 2 つに対して同じ差分ループを実行し、欠損フィールドは <missing> をデフォルトとして扱えば、消えたフィールドも表面化します。述語はフィールド名に依存しないため、ライブラリが読み取れるすべてのフォーマットで 1 つの検出器が機能します。

編集タイムラインの追跡

Tags.time とカウンタ名ルールに置き換えると、検出器は RevisionNumber、TotalEditingTime、LastPrinted の変化を報告します:

props = metadata.find_properties(lambda p:
    Tags.time.modified in list(p.tags)
    or Tags.time.created in list(p.tags)
    or Tags.time.printed in list(p.tags)
    or (p.name is not None and ("Revision" in p.name
        or "EditTime" in p.name or "EditingTime" in p.name)))

監査レポートのエクスポート

コンソールだけに出力したままでは情報が失われます。スプレッドシートや SIEM ケース向けに 4 列の CSV を作成します:

with open("output/diff.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["change_type", "property", "old_value", "new_value"])
    for k, v in added.items():
        writer.writerow(["added", k, "", v])
    for k, v in removed.items():
        writer.writerow(["removed", k, v, ""])
    for k, (old_v, new_v) in changed.items():
        writer.writerow(["changed", k, old_v, new_v])

リポジトリには JSON エクスポーターも同梱されており、ダッシュボードやケース管理 API 用の安定した 3 マップ スキーマを提供します。


実際の運用例

3 つの典型的な導入パターンがあります。インテーク パイプラインは到着したドキュメントを既存のコピーと比較し、所有権が変わったペアを隔離します。コンプライアンス ジョブはスケジュールに従って差分を取り、ペアごとに CSV をアーカイブし、後で再構築する必要のないプロパティ タイムラインを構築します。争点ツールはオンデマンドで両方の検出器を走らせ、クレームが発生したときに常に「誰がいつファイルに触れたか」を答えます。

4 番目のパターンは、ファイルを自分自身の最後に確認されたスナップショットと比較するもので、片側に保存した辞書を使って同じコードを再利用します。すべてのケースでエクスポート ファイルが成果物となり、コンソール出力は進捗ノイズに過ぎません。スクリプトの終了コードパターンはリポジトリの main.py に従っているため、スケジューラや CI は失敗アサートを追加配線なしで失敗として扱えます。ここに示したコード以上の実装は不要です。


フラグすべき変更とは?

差分が分類したものすべてが対象です。追加・削除されたプロパティは常にチェックすべきです。構造が変わったことを示します。変更されたエントリについては、ほとんどのチームがまず Identity と Revision グループにアラートを設定し、残りは情報提供として扱います。検出器は最初のパスで 1 回の関数呼び出しだけで済むように設計されています。


5. クイックリファレンス: 主な呼び出し

呼び出し 機能
Metadata(path) ファイルを開く。コンテキストマネージャが解放を担当
find_properties(predicate) すべてのレイヤーを横断し、述語が受け入れるプロパティを返す
p.interpreted_value 人が読める値。p.value がフォールバック
Tags.person.* / Tags.corporate.company フォーマットに依存しない Identity 分類
Tags.time.* Revision 検出器用のタイムスタンプ分類

complete API reference で検索とタグ付けの全体像を確認してください。タグ語彙はここに示した行よりも多く、origin、content、legal などのタググループも同様のメンバーシップテストで利用できます。


6. よくある問題と対処法

差分が膨大でノイズのように見える
→ 2 つのパスが同一ドキュメントのリビジョンでない可能性があります。差分を取る前に出所を検証してください。無関係なファイルは意味のない差分を生みます。

所有権検出器に既知の author フィールドが表示されない
→ 一部のプロデューサはタグ付けされていないカスタム フィールドに Identity を保存します。全差分を一度実行し、実際のフィールド名を特定して、述語に名前ルールを追加してください。

コンソールに評価モード警告が表示される
→ ライセンス ファイルが見つかりません。main.pyLICENSE_PATH.lic ファイルの場所に設定するか、開発用に評価モードのまま使用してください。ロジックは同一です。

日付が生のシリアル番号で出力される
→ どこかで p.value がそのまま使用されています。read_propsinterpreted_value 優先パターンを維持すれば、レポートは可読性を保ちます。


次のステップは?

メタデータ差分が動作するようになりました。ここからの展開例:

  • バッチ処理: スクリプトをドキュメント ペアのループに組み込み、ペアごとに CSV を保存。ペアあたりのコストはファイルを 2 回開くだけで、CSV を結合すればライブラリ全体のビューが得られます。
  • スケジューリング: リポジトリの main.py は各ステップをアサートし、適切な終了コードを返すので、CI やスケジューラにそのまま組み込めます。
  • チュートリアル版を体験: use case guide では、同じパイプラインを 3 段階のチュートリアルで構築できます。
  • プロジェクト全体を見る: document-version-metadata-diff-python にはシードされたリビジョン ペアが含まれています。

リソース