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

آنچه می‌سازید

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

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

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

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


1. نصب

pip install groupdocs-metadata-net==26.5

مخزن همراه این نسخه را قفل می‌کند و document-v1.docx و document-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: مدیر زمینه (context manager) که فایلی را باز می‌کند و هنگام خروج آن را آزاد می‌سازد؛ یک نمونه برای هر بازنگری.
  • find_properties: فیلدهای داخلی، ویژگی‌های سفارشی و XMP را در یک عبور می‌گرداند و همهٔ مواردی که پیش‌شرط می‌پذیرد برمی‌گرداند.
  • interpreted_value: شکل قابل‌خواندن انسان برای یک ویژگی؛ استفاده از آن به این معناست که تاریخ‌ها و شمارش‌ها به‌صورت رشته مقایسه می‌شوند و می‌توانید در گزارش چاپ کنید.
  • نام‌های واجد شرایط به‌عنوان کلید: فیلدهای داخلی و سفارشی نمی‌توانند در دیکشنری با هم تداخل داشته باشند، بنابراین منطق مجموعه‌ها ایمن می‌ماند.

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

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


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

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

ردیابی خط زمان ویرایش

پیش‌شرط را برای 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 کافی است:

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


جایی که این در عمل اجرا می‌شود

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

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


چه چیزی به‌عنوان تغییری که ارزش پرچم‌گذاری دارد محسوب می‌شود؟

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


5. مرجع سریع: فراخوانی‌های کلیدی

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

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


6. مشکلات رایج و راه‌حل‌ها

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

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

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

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


گام‌های بعدی؟

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

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

منابع