💡 مثال کامل و قابل اجرا در گیت‌هاب موجود است:
document-version-metadata-diff-python

What You’ll Build

در این راهنما، متادیتای هر ویژگی بین دو نسخهٔ یک سند را مقایسه می‌کنید و دقیقاً آنچه اضافه، حذف یا تغییر کرده است را چاپ می‌کنید. مقایسهٔ نسخهٔ متادیتا، مقایسهٔ سطح‑ویژگی دو بازنگری از یک فایل است و سیگنال‌هایی را می‌گیرد که مقایسهٔ متنی هرگز نمی‌بیند: یک سازندهٔ جدید، شمارهٔ بازنگری افزایش یافته، یا یک جلسهٔ ویرایشی که پس از بسته شدن بازبینی ثبت شده است. در پایان، یک راه‌حل عملی به همراه دو آشکارساز متمرکز و دو قالب خروجی خواهید داشت که همهٔ آن‌ها از مخزنی قابل اجرا و شامل یک جفت بازنگری نمونه استخراج شده‌اند.

سطح مهارت: توسعه‌دهندهٔ Python متوسط
آنچه نیاز دارید: Python 3، pip، و دو بازنگری از یک سند

اولین اجرای من از این اسکریپت، تغییری در مقدار «Company» را نشان داد که هیچ‌یک از اعضای تیم به یاد نداشتند که انجام داده‌اند؛ همان یک خط هزینهٔ راه‌اندازی را جبران کرد. تمام مطالب زیر آمادهٔ کپی‑پیست هستند و به‌خوبی زیر صد خط می‌گنجند.

خط لوله عمداً ساده است: دو بار باز کردن فایل، سه عبارت دیکشنری، یک حلقهٔ چاپ. سادگی هدف است. اختلافات نسخه بر پایهٔ قابلیت توضیح و تکرار روش تصمیم‌گیری می‌شوند و اسکریپتی به این اندازه می‌تواند به‌صورت کامل توسط هر کسی که نتیجه را به چالش می‌کشد، خوانده شود.


1. Install

pip install groupdocs-metadata-net==26.5

مخزن همراه این نسخه را قفل می‌کند و document-v1.docx و document-v2.docx را فراهم می‌آورد، بنابراین کد زیر به‌صورت مستقیم اجرا می‌شود. نسخه‌ای را که حسابرسی شما با آن اجرا شده قفل کنید؛ قابلیت بازتولید بخشی از شواهد است.


2. The Core Code

درخت ویژگی‌های هر دو فایل را بخوانید، سپس دلتا را با منطق مجموعه‌ها طبقه‌بندی کنید. این کل مقایسه است:

# 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. How It Works

  • Metadata: مدیر زمینه (context manager) که فایلی را باز می‌کند و هنگام خروج آن را آزاد می‌سازد؛ یک نمونه برای هر بازنگری.
  • find_properties: فیلدهای داخلی، ویژگی‌های سفارشی و XMP را در یک عبور می‌گرداند و تمام مواردی که پیش‌شرط می‌پذیرد برمی‌گرداند.
  • interpreted_value: شکل قابل‌خواندن برای انسان یک ویژگی؛ استفاده از آن باعث می‌شود تاریخ‌ها و مقادیر شمارشی به‌صورت رشته‌ای مقایسه شوند که می‌توانید در گزارش چاپ کنید.
  • نام‌های واجد شرایط به‌عنوان کلید: فیلدهای داخلی و سفارشی نمی‌توانند در دیکشنری با هم تداخل داشته باشند، بنابراین منطق مجموعه‌ها ایمن می‌ماند.

در اینجا هیچ‌یک از ساختارهای DOCX تجزیه نمی‌شود. مستندات محصول بیش از ۱۷۰ فرمت را پشت یک فراخوانی یکسان پشتیبانی می‌کند، بنابراین همین اسکریپت می‌تواند جفت‌های PDF یا XLSX را نیز مقایسه کند.

یک ویژگی دیگر طراحی که شایستگی ذکر دارد این است که مرز API در دو فراخوانی read_props پایان می‌یابد. پس از آن همه چیز با کتابخانهٔ استاندارد Python است، بنابراین تست‌های واحد، آستانه‌ها و قوانین هشدار هرگز به لایهٔ سند دست نمی‌زنند. تیم‌هایی که این را در سرویس می‌پیچند معمولاً دیکشنری‌های استخراج‌شده را برای هر بازنگری کش می‌کنند و اجازه می‌دهند هر بررسی بعدی از آن‌ها استفاده کند، به‌طوری که ورودی‑خروجی فایل فقط یک بار برای هر نسخه انجام می‌شود، صرف‌نظر از تعداد سؤالات.


4. Common Customizations

Detect ownership changes only

وقتی سؤال این است که «چه کسی این فایل را دست‌کاری کرده است»، به‌جای فیلتر کردن پس از مقایسه، در زمان خواندن با پیش‌شرط‌های برچسب فیلتر کنید:

# 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

همان حلقهٔ دلتا را روی دو دیکشنری این‌چنینی اجرا کنید و برای فیلدی که ناپدید شده <missing> را به‌عنوان مقدار پیش‌فرض بگذارید تا همچنان ظاهر شود. نام پیش‌شرط هیچ فیلدی را مشخص نمی‌کند، که این امکان را می‌دهد یک آشکارساز برای تمام قالب‌هایی که کتابخانه می‌خواند، کار کند.

Track the editing timeline

پیش‌شرط را برای 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)))

Export an audit report

یافته‌هایی که فقط در کنسول می‌مانند، در همانجا «می‌میرند». چهار ستون برای صفحه‌گسترده و کیس‌های SIEM کافی است:

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های مدیریت کیس‌ها دارد.


Where This Runs in Practice

سه الگوی پیاده‌سازی مکرراً مشاهده می‌شود. خطوط ورودی هر سند جدید را در مقابل نسخهٔ موجود مقایسه می‌کنند و جفت‌هایی که تغییر هویت دارند را قرنطینه می‌نمایند. کارهای انطباق این مقایسه را به‌صورت زمان‌بندی اجرا می‌کنند و CSV هر جفت را بایگانی می‌نمایند تا یک خط زمان ویژگی ساخته شود که دیگر نیازی به بازسازی آن نیست. ابزارهای حل اختلاف همزمان دو آشکارساز را به‌صورت درخواست اجرا می‌کنند، زیرا وقتی ادعایی مطرح می‌شود سؤال اولیه همیشه «چه کسی و کی این فایل را دست‌کاری کرده است» است، نه «چه چیزی در پاراگراف چهار تغییر کرده است».

الگوی چهارم، مقایسهٔ یک فایل با آخرین اسنپ‌شات سالم خود است؛ همان کد با یک دیکشنری ذخیره‌شده در یک طرف استفاده می‌شود. در همهٔ این موارد، فایل خروجی تحویل نهایی است؛ خروجی کنسول فقط نویز پیشرفت است. الگوی کد خروجی اسکریپت با main.py مخزن هم‌خوانی دارد، بنابراین زمان‌بندها و CI یک شکست ادعا را به‌عنوان اجرای ناموفق می‌پذیرند بدون نیاز به سیم‌کشی اضافی. هیچ‌یک از این موارد به کدی بیش از آنچه در این صفحه نشان داده شده نیاز نداشتند.


What counts as a change worth flagging?

هر چیزی که مقایسه طبقه‌بندی می‌کند به‌همراه زمینه‌ای که شما اضافه می‌کنید. ویژگی‌های اضافه‌شده و حذف‌شده همیشه شایستگی بررسی دارند چون نشان می‌دهند ساختار تغییر کرده است نه فقط مقدار. برای ورودی‌های تغییر یافته، اکثر تیم‌ها ابتدا بر گروه‌های هویت و بازنگری هشدار می‌دهند و بقیه را صرفاً اطلاعاتی می‌پندارند. آشکارسازها طوری طراحی شده‌اند که عبور اولیه تنها یک فراخوانی تابع باشد.


5. Quick Reference: Key Calls

فراخوانی عملکرد
Metadata(path) فایل را باز می‌کند؛ مدیر زمینه آزادسازی را مدیریت می‌کند
find_properties(predicate) تمام ویژگی‌هایی که پیش‌شرط می‌پذیرد را در تمام لایه‌ها برمی‌گرداند
p.interpreted_value مقدار قابل‌خواندن برای انسان؛ در صورت عدم وجود به p.value می‌پرد
Tags.person.* / Tags.corporate.company طبقه‌بندی هویت، مستقل از قالب
Tags.time.* طبقه‌بندی زمان‌مهر برای آشکارساز بازنگری

مرجع کامل API را برای جستجو و برچسب‌گذاری کامل ببینید. واژگان برچسب‌ها بزرگ‌تر از این ردیف‌ها هستند؛ گروه‌های منبع، محتوا و قانونی نیز از همان تست عضویت استفاده می‌کنند.


6. Common Issues & Fixes

مقایسه بسیار بزرگ است و شبیه نویز به نظر می‌رسد
→ احتمالاً دو مسیر یک سند نیستند. رفع: پیش از مقایسه منبع را تأیید کنید؛ فایل‌های نامرتبط دلتاهای بی‌معنی تولید می‌کنند.

یک فیلد نویسندهٔ شناخته‌شده در آشکارساز مالکیت ظاهر نمی‌شود
→ برخی تولیدکنندگان هویت را در فیلدهای سفارشی بدون برچسب ذخیره می‌کنند. رفع: یک بار مقایسهٔ کامل را اجرا کنید، نام واقعی فیلد را پیدا کنید و پیش‌شرط را با قانون نام گسترش دهید.

هشدار حالت ارزیابی در کنسول نمایش داده می‌شود
→ فایل لایسنس یافت نشد. رفع: LICENSE_PATH در main.py را به فایل .lic خود اشاره دهید، یا برای توسعه حالت ارزیابی را نگه دارید؛ منطق یکسان است.

تاریخ‌ها به‌صورت عدد سریال خام چاپ می‌شوند
→ مقدار خام p.value به‌جای interpreted_value در جایی از خواننده عبور کرده است. رفع: الگوی «interpreted_value اول» را در read_props حفظ کنید؛ این دلیل قابل‌خواندن ماندن گزارش‌هاست.


What’s Next?

شما یک مقایسهٔ متادیتای عملی دارید. قدم‌های بعدی:

  • بچ کنید: اسکریپت را روی جفت‌های سندی حلقه بزنید و CSV هر جفت را ذخیره کنید؛ هزینهٔ هر جفت دو بار باز کردن فایل است و CSVها به‌راحتی برای نمای کلی کتابخانه ترکیب می‌شوند.
  • زمان‌بندی کنید: main.py مخزن هر گام را اعتبارسنجی می‌کند و کد خروجی مناسب می‌دهد، که مستقیماً در CI یا زمان‌بندها قابل استفاده است.
  • راهنمای آموزشی را دنبال کنید: راهنمای موارد استفاده همان خط لوله را در سه آموزش درجه‌بندی شده می‌سازد.
  • کل پروژه را ببینید: document-version-metadata-diff-python با جفت بازنگری نمونه.

Resources