💡 דוגמה מלאה עובדת זמינה ב‑GitHub:
document-version-metadata-diff-python

מה אתה תבנה

במדריך זה תבצע השוואת הבדל (diff) לכל תכונת מטא‑נתונים בין שתי גרסאות של מסמך ותדפיס בדיוק מה נוסף, הוסר או השתנה. diff של גרסת מטא‑נתונים הוא השוואה ברמת תכונה של שני תיקונים של קובץ אחד, והוא תופס אותות שהשוואת טקסט לעולם לא רואה: יוצר חדש, מספר גרסה שהועלה, סשן עריכה שנרשם אחרי שהסקירה נסגרה. בסיום יהיה לך פתרון עובד יחד עם שני גלאים ממוקדים ושני פורמטים של ייצוא, כולם נלקחים ממאגר קוד שניתן להריץ עם זוג גרסאות לדוגמה.

רמת מיומנות: מפתח Python ברמת ביניים
מה אתה צריך: Python 3, pip, ושתי גרסאות של מסמך אחד

הריצה הראשונה שלי של הסקריפט סימנה שינוי בערך Company שמישהו מהצוות לא זכר שהזין; השורה ההיא שילמה על ההקמה. כל מה שלמטה מוכן להעתקה והדבקה ונמצא מתחת למאה שורות.

הצינור (pipeline) מתוכנן להיות משעמם: שני פתיחות קבצים, שלוש הבנות מילון (dict comprehensions), לולאת הדפסה. השעמום הוא המטרה. מחלוקות גרסה נפתרות על פי האם ניתן להסביר ולחזור על השיטה, וסקריפט קטן זה ניתן לקריאה מלאה על ידי כל מי שמאתגר את הממצא.


1. התקנה

pip install groupdocs-metadata-net==26.5

ה‑מאגר המלווה קובע גרסה זו ומשגר את document-v1.docx ו‑document-v2.docx כך שהקוד למטה ירוץ כפי שהוא. קבע את הגרסה שבה בוצע האודיט שלך; שחזור הוא חלק מההוכחה.


2. הקוד המרכזי

קרא את שני עצי התכונות, ואז סווג את ההפרש בעזרת לוגיקה של קבוצות. זהו כל ה‑diff:

# 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 בפעם אחת, ומחזיר את כל מה שהפונקציה המקבלת (predicate) מקבלת.
  • interpreted_value: הצורה הקריאה לבני אדם של תכונה; העדפתה משמעותה שתאריכים ו‑enumerations יושוו כמחרוזות שניתן להדפיס בדוח.
  • שמות מוסמכים כמפתחות: שדות מובנים ומותאמים לא יכולים להתנגש במילון, ולכן הלוגיקה של הקבוצות נשארת בטוחה.

שום דבר כאן אינו מנתח מבני DOCX. ה‑תיעוד המוצר מציין יותר מ‑170 פורמטים מאחורי אותה קריאה, כך שהסקריפט הזה מבצע diff גם לזוגות PDF או XLSX.

תכונה נוספת של העיצוב שמגיעה לציון: גבול ה‑API נגמר בשתי קריאות read_props. כל מה שאחרי זה ספריית Python של הספרייה הסטנדרטית, ולכן מבחני יחידה, ספים, וכללי התראה לעולם לא נוגעים לשכבת המסמך. צוותים שעוטפים זאת בשירות בדרך כלל מאחסנים במטמון את המילונים המופקים לכל גרסה ומאפשרים לכל בדיקה במטה‑נתונים להשתמש בהם, כך שה‑IO של הקבצים נשאר פתיחה אחת לכל גרסה לא משנה כמה שאלות נשאלות.


4. התאמות נפוצות

גלה רק שינויי בעלות

כאשר השאלה היא “מי נגע בקובץ הזה”, סנן בזמן הקריאה עם תחזיות תגים במקום לסנן אחרי ה‑diff המלא:

# 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

הפעל את לולאת ה‑delta על שני מילונים כאלה, תוך שימוש ב‑<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 עם סכמת שלושה מפות יציבה ללוחות מחוונים ו‑APIs של ניהול מקרים.


5. היכן זה רץ בפועל

שלושה פריסות חוזרות על עצמן. צינורות קבלה מבצעים diff לכל מסמך שמגיע מול העותק שכבר נמצא ברשומה ומבודדים זוגות עם שינויי זהות. עבודות ציות מריצות את ה‑diff בלו"ז ומאחסנות את ה‑CSV לכל זוג, בונות קו זמן של תכונות שמישהו לא צריך לבנות מחדש מאוחר יותר. וכלי מחלוקות מריץ את שני הגלאים לפי דרישה, כי כאשר תביעה מגיעה השאלה הפתיחה היא תמיד “מי נגע בקובץ ומתי”, ולא “מה השתנה בפסקה הרביעית”.

תבנית רביעית, diff של קובץ מול הצילום האחרון הידוע שלו, משתמשת באותו קוד עם מילון שמאוחסן בצד אחד. בכל המקרים, קובץ הייצוא הוא המוצר; פלט הקונסול הוא רק רעש של התקדמות. תבנית קוד יציאת הסקריפט מתאימה ל‑main.py של המאגר, כך שמתזמנים ו‑CI מתייחסים לכישלון אסרט ככישלון ריצה ללא חיבורים נוספים. אף אחד מהם לא נדרש קוד מעבר למה שמוצג כאן.


6. מה נחשב לשינוי שמצריך סימון?

כל מה שה‑diff מסווג בתוספת הקשר שאתה מוסיף. תכונות שנוספו והוסרו תמיד ראויות לבדיקה כי הן משמעותן שהמבנה השתנה ולא ערך. עבור ערכים שהשתנו, רוב הצוותים מתריעים על קבוצות הזהות והגרסה ראשית ומתייחסים לשאר כמידע בלבד. הגלאים קיימים כדי שהמעבר הראשון יעלה עלות של קריאת פונקציה אחת בלבד.


7. הפנייה המהירה: קריאות מפתח

קריאה מה היא עושה
Metadata(path) פותחת את הקובץ; מנהל ההקשר מטפל בשחרור
find_properties(predicate) מחזירה כל תכונה שה‑predicate מקבל, בכל השכבות
p.interpreted_value ערך קריא לבני אדם; נופל ל‑p.value אם חסר
Tags.person.* / Tags.corporate.company סיווג זהות, בלתי תלוי בפורמט
Tags.time.* סיווג חותמות זמן עבור גלאי הגרסה

ראה את ה‑הפנייה המלאה ל‑API לקבלת החיפוש המלא ומשטח התיוג. אוצר המילים של התגים גדול יותר משורות אלו; קבוצות מקור, תוכן, ומשפטיות חוקיות פועלות באותו מבחן חברות.


8. בעיות נפוצות & תיקונים

ה‑diff ענק וקורא כמו רעש
→ שני הנתיבים כנראה אינם גרסאות של אותו מסמך. תקן: אמת את המקור לפני ה‑diff; קבצים בלתי קשורים מייצרים delta חסר משמעות.

שדה מחבר ידוע לעולם לא מופיע בגלאי הבעלות
→ חלק מהיצרנים שומרים זהות בשדות מותאמים ללא תגים. תקן: הרץ diff מלא פעם אחת, מצא את שם השדה האמיתי, והרחב את ה‑predicate עם כלל שם.

הקונסול מציג אזהרת מצב הערכה
→ לא נמצא קובץ רישיון. תקן: הפנה את LICENSE_PATH ב‑main.py לקובץ .lic שלך, או שמור על מצב הערכה לפיתוח; הלוגיקה זהה.

תאריכים מודפסים כמספרים סדרתיים גולמיים
p.value הגולמי חמק למקום קריאה כלשהו. תקן: שמור על דפוס interpreted_value‑first מ‑read_props; זה הסיבה שהדוחות נשארים קריאים.


9. מה הלאה?

יש לך diff מטא‑נתונים עובד. הנה כמה דרכים להמשיך:

  • אצור אותו: הרץ את הסקריפט על זוגות מסמכים ושמור CSV לכל זוג; העלות לכל זוג היא שני פתיחות קבצים, וה‑CSV‑ים מתלכדים בקלות לתצוגה ספרייתית.
  • תזמן אותו: main.py של המאגר מבצע אסרטים בכל שלב ומחזיר קוד יציאה מתאים, שניתן לשלב ישירות ב‑CI או במתזמן.
  • המשך במדריך גרסה: ה‑מדריך מקרה השימוש בונה את אותו צינור בשלושה מדריכים מדורגים.
  • ראה את הפרויקט המלא: document-version-metadata-diff-python עם זוג הגרסאות המוזרק.

משאבים