💡 דוגמה מלאה עובדת זמינה ב‑GitHub:
document-version-metadata-diff-python
What You’ll Build
במדריך זה תבצעו השוואת diff של כל מאפיין מטא‑נתונים בין שני גרסאות של מסמך ותדפיסו בדיוק מה נוסף, הוסר או השתנה. diff של גרסת מטא‑נתונים הוא השוואה ברמת המאפיין של שני תיקונים של קובץ אחד, והוא תופס אותות שהשוואת טקסט רגילה לעולם לא רואה: יוצר חדש, מספר גרסה שהועלה, סשן עריכה שנרשם אחרי שהסקירה נסגרה. בסיום יהיה לכם פתרון עובד יחד עם שני גלאים ממוקדים ושני פורמטים לייצוא, כולם נלקחים ממאגר קוד שמכיל זוג גרסאות לדוגמה.
רמת מיומנות: מפתח Python ברמת ביניים
מה נדרש: Python 3, pip, ושתי גרסאות של מסמך אחד
הריצה הראשונה שלי של הסקריפט סימנה שינוי בערך Company שמישהו מהצוות לא זכר שהחליף; השורה ההיא כיסתה את עלות ההקמה. כל מה שלמטה מוכן להעתקה והדבקה ומסתכם בפחות ממאה שורות.
הצינור (pipeline) מתוכנן להיות משעמם: פתיחת שני קבצים, שלושה הבנות dict, לולאת הדפסה. השעמום הוא המטרה. מחלוקות גרסה נפתרות על‑פי האם ניתן להסביר ולחזור על השיטה, וסקריפט קטן זה ניתן לקריאה מלאה על‑ידי כל מי שמאתגר את הממצא.
1. Install
pip install groupdocs-metadata-net==26.5
ה‑מאגר המשלים מצמיד גרסה זו ומשגר את הקבצים document-v1.docx ו‑document-v2.docx כך שהקוד למטה פועל כפי שהוא. הצמידו את הגרסה שבה בוצע האודיט שלכם; שחזור הוא חלק מההוכחה.
2. The Core Code
קראו את שני עצי המאפיינים, ואז סווגו את ההפרש בעזרת לוגיקה של קבוצות. זהו ה‑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. How It Works
Metadata: מנהל ההקשר (context manager) שפותח קובץ ומשחרר אותו ביציאה; מופע אחד לכל גרסה.find_properties: משוטט בין שדות מובנים, מאפיינים מותאמים ו‑XMP במעבר אחד, ומחזיר את כל מה שה‑predicate מקבל.interpreted_value: הצורה הקריאה לבני אדם של המאפיין; העדפתו גורמת לתאריכים ולמינויים להיות מושווים כמחרוזות שניתן להדפיס בדוח.- Qualified names as keys: שדות מובנים ומותאמים לא יכולים להתנגש במילון, ולכן לוגיקת הקבוצות נשארת בטוחה.
שום דבר כאן אינו מנתח מבני DOCX. ה‑תיעוד המוצר מציין יותר מ‑170 פורמטים מאחורי אותה קריאה, ולכן הסקריפט הזה יכול לבצע diff גם על קבצי PDF או XLSX.
עוד מאפיין של העיצוב שכדאי לציין: גבול ה‑API מסתיים בשתי הקריאות read_props. כל מה שאחרי זה הוא Python מהספרייה הסטנדרטית, ולכן בדיקות יחידה, ספים וכללי התראה אינם נוגעים לשכבת המסמך. צוותים שמעטפים זאת בשירות בדרך כלל מאחסנים במטמון את המילונים המופקים לכל גרסה ומאפשרים לכל בדיקה במורד השימוש להשתמש בהם, כך שה‑IO של הקבצים נשאר פתיחה אחת לכל גרסה, לא משנה כמה שאלות נשאלות.
4. Common Customizations
Detect ownership changes only
כאשר השאלה היא “מי נגע בקובץ הזה”, סנוּרו בזמן הקריאה עם predicate של תגים במקום לסנן את ה‑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> כברירת מחדל כך ששדה שנעלם עדיין יופיע. שמות ה‑predicate אינם מתייחסים לשדה ספציפי, וזה מה שמאפשר לגלאי אחד לשרת כל פורמט שהספרייה קוראת.
Track the editing timeline
החליפו את ה‑predicate ל‑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 עם סכמת שלושה‑מפות יציבה ללוחות מחוונים ו‑APIs של ניהול מקרים.
Where This Runs in Practice
שלושה תרחישים נפוצים מופיעים. צינורות קבלה מבצעים diff לכל מסמך שמגיע מול העותק שכבר נמצא ברשומה ומבודדים זוגות עם שינויי זהות. עבודות ציות מריצות את ה‑diff בתזמון ומאחסנות את ה‑CSV לכל זוג, בונות קו זמן של מאפיינים שמישהו לא צריך לבנות מאוחר יותר. וכלי מחלוקת מריץ את שני הגלאים לפי דרישה, כי כאשר תביעה מגיעה השאלה הפתיחה היא תמיד “מי נגע בקובץ ומתי”, לא “מה השתנה בפסקה הרביעית”.
תבנית רביעית, diff של קובץ מול הצילום האחרון הידוע שלו, משתמשת באותו קוד עם מילון שמור בצד אחד. בכל המקרים, קובץ הייצוא הוא המוצר; פלט הקונסול הוא רק רעש של התקדמות. תבנית קוד ה‑exit‑code של הסקריפט מתיישבת עם main.py של המאגר, כך שמתזמנים ו‑CI מתייחסים לכישלון של assert ככישלון ריצה ללא חיבורים נוספים. אף אחד מהם לא נדרש קוד מעבר למה שמופיע כאן.
What counts as a change worth flagging?
כל מה שה‑diff מסווג יחד עם ההקשר שאתם מוסיפים. מאפיינים שנוספו והוסרו תמיד ראויים לבחינה כי הם מצביעים על שינוי במבנה ולא רק בערך. עבור ערכים שהשתנו, רוב הצוותים מתריעים על קבוצות הזהות והגרסה תחילה ומתייחסים לשאר כמידע בלבד. הגלאים קיימים כדי שהמעבר הראשון יעלה קריאה אחת של פונקציה.
5. Quick Reference: Key Calls
| קריאה | מה היא עושה |
|---|---|
Metadata(path) |
פותחת את הקובץ; מנהל ההקשר מטפל בשחרור |
find_properties(predicate) |
מחזירה כל מאפיין שה‑predicate מקבל, בכל השכבות |
p.interpreted_value |
ערך קריא לבני אדם; נופל ל‑p.value אם חסר |
Tags.person.* / Tags.corporate.company |
סיווג זהות, בלתי תלוי בפורמט |
Tags.time.* |
סיווג חותמות זמן עבור גלאי הגרסה |
ראו את ה‑הפנייה המלאה ל‑API לקבלת החיפוש המלא ומשטח התיוגים. אוצר המילים של התגים גדול יותר משורות אלו; קבוצות origin, content ו‑legal פועלות באותו מבחן חברות.
6. Common Issues & Fixes
ה‑diff ענק וקורא כמו רעש
→ שני הנתיבים כנראה אינם גרסאות של אותו מסמך. תקנו: אמתו מקוריות לפני ה‑diff; קבצים בלתי קשורים מייצרים דלטים חסרי משמעות.
שדה מחבר ידוע אף פעם לא מופיע בגלאי הבעלות
→ חלק מהיוצרים מאחסנים זהות בשדות מותאמים ללא תגים. תקנו: הריצו את ה‑diff המלא פעם אחת, מצאו את שם השדה האמיתי, והרחיבו את ה‑predicate עם כלל שם.
הקונסול מציג אזהרת מצב הערכה
→ לא נמצא קובץ רישיון. תקנו: הפנו את LICENSE_PATH ב‑main.py לקובץ .lic שלכם, או השאירו מצב הערכה לפיתוח; הלוגיקה זהה.
תאריכים מודפסים כמספרים סידוריים גולמיים
→ p.value הגולמי נשפך לקורא במקום כלשהו. תקנו: שמרו על תבנית interpreted_value‑first מ‑read_props; היא הסיבה שהדוחות נשארים קריאים.
What’s Next?
יש לכם diff של מטא‑נתונים עובד. הנה כמה דרכים להמשיך:
- אצורו באצוות: הריצו את הסקריפט על זוגות מסמכים ושמרו CSV לכל זוג; העלות לכל זוג היא שני פתיחות קבצים, וה‑CSV‑ים מתלכדים בקלות לתצוגה ספרייתית.
- תזמנו:
main.pyשל המאגר מבצע assert בכל שלב ומחזיר קוד יציאה מתאים, מה שמאפשר לשלב אותו ישירות ב‑CI או במתזמן. - המשיכו במדריך גרסה: ה‑מדריך מקרה שימוש בונה את הצינור באותו שלושה מדריכים מדורגים.
- ראו את הפרויקט המלא: document-version-metadata-diff-python עם זוג הגרסאות המוזרק.