💡 مثال كامل يعمل متوفر على 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: مدير السياق الذي يفتح الملف ويغلقه عند الخروج؛ نسخة واحدة لكل نسخة.
  • find_properties: يجوب الحقول المدمجة، الخصائص المخصصة، وXMP في تمريرة واحدة، ويعيد كل ما يقبله الشرط.
  • interpreted_value: الشكل القابل للقراءة للخاصية؛ استخدامه يجعل التواريخ والعدادات تُقارن كسلاسل يمكن طباعتها في تقرير.
  • الأسماء المؤهلة كمفاتيح: الحقول المدمجة والمخصصة لا يمكن أن تتصادم في القاموس، لذا يبقى منطق المجموعات آمناً.

لا شيء هنا يحلل بنية DOCX. توثيق المنتج يذكر أكثر من 170 تنسيقًا يدعم نفس الاستدعاء، لذا يمكن للسكريبت نفسه مقارنة أزواج PDF أو XLSX.

خاصية أخرى للتصميم تستحق الذكر: حد واجهة برمجة التطبيقات هو استدعائي 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 بمخطط ثابت من ثلاث خرائط للوحة التحكم وواجهات برمجة التطبيقات لإدارة الحالات.


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 لكل زوج؛ تكلفة كل زوج هي فتح ملفين، ويمكن دمج CSVs بسهولة للحصول على نظرة شاملة للمكتبة.
  • جدولته: main.py في المستودع يتحقق من كل خطوة ويعيد رمز خروج مناسب، ما يجعله جاهزًا للـ CI أو أي جدولة.
  • اتبع دليل البرنامج التعليمي: دليل حالة الاستخدام يبني نفس الخطوات في ثلاث دروس متدرجة.
  • اطلع على المشروع كاملًا: document-version-metadata-diff-python مع زوج الإصدارات المبدئي.

موارد