💡 Full working example available on GitHub: document-version-metadata-diff-python

Những gì bạn sẽ xây dựng

Trong hướng dẫn này, bạn sẽ so sánh mọi thuộc tính metadata giữa hai phiên bản của một tài liệu và in ra chính xác những gì đã được thêm, xóa hoặc thay đổi. Một diff phiên bản metadata là so sánh ở mức thuộc tính giữa hai bản sửa đổi của cùng một tệp, và nó bắt được những tín hiệu mà so sánh văn bản không bao giờ thấy: một Creator mới, một RevisionNumber tăng lên, một phiên chỉnh sửa được ghi lại sau khi đánh giá đã đóng. Khi kết thúc, bạn sẽ có một giải pháp hoạt động cùng hai bộ phát hiện tập trung và hai định dạng xuất, tất cả được rút ra từ một kho lưu trữ có thể chạy được và đã được cung cấp một cặp phiên bản mẫu.

Skill level: nhà phát triển Python trung cấp
What you need: Python 3, pip, và hai phiên bản của một tài liệu

Lần chạy đầu tiên của tôi với script này đã phát hiện một thay đổi giá trị Company mà không ai trong nhóm nhớ đã thực hiện; một dòng mã đó đã trả tiền cho việc thiết lập. Mọi thứ dưới đây đã sẵn sàng để sao chép và dán và tổng cộng chưa tới một trăm dòng.

Quy trình được thiết kế cố ý đơn giản: hai lần mở tệp, ba dict comprehension, một vòng lặp in. Sự đơn giản là mục tiêu. Các tranh chấp về phiên bản được quyết định dựa trên việc phương pháp có thể giải thích và lặp lại được không, và một script nhỏ như vậy có thể được đọc toàn bộ bởi bất kỳ ai thách thức kết quả.


1. Cài đặt

pip install groupdocs-metadata-net==26.5

companion repository đã cố định phiên bản này và cung cấp document-v1.docxdocument-v2.docx nên mã dưới đây chạy ngay như vậy. Hãy cố định phiên bản mà cuộc kiểm toán của bạn đã sử dụng; khả năng tái tạo là một phần của bằng chứng.


2. Mã lõi

Đọc cả hai cây thuộc tính, sau đó phân loại delta bằng logic tập hợp. Đây là toàn bộ 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}")

Đó là mức tối thiểu bạn cần. Mong đợi số lượng nhỏ trên các cặp phiên bản thực tế; một delta hàng chục thường có nghĩa là tệp đã đi qua một thay đổi mẫu hoặc một di chuyển lưu trữ trên đường. Các phần tiếp theo sẽ giải thích các lời gọi chính và hiển thị các tùy chỉnh mà hầu hết các nhóm thường thêm vào đầu tiên.


3. Cách hoạt động

  • Metadata: trình quản lý ngữ cảnh mở tệp và giải phóng nó khi thoát; một thể hiện cho mỗi phiên bản.
  • find_properties: duyệt các trường tích hợp, thuộc tính tùy chỉnh và XMP trong một lần, trả về mọi thứ mà predicate chấp nhận.
  • interpreted_value: dạng giá trị dễ đọc cho con người; ưu tiên nó có nghĩa là ngày tháng và các enum được so sánh dưới dạng chuỗi mà bạn có thể in trong báo cáo.
  • Qualified names as keys: các trường tích hợp và tùy chỉnh không thể trùng nhau trong dict, vì vậy logic tập hợp vẫn an toàn.

Không có gì ở đây phân tích cấu trúc DOCX. [product documentation] (https://docs.groupdocs.com/metadata/python-net/) liệt kê hơn 170 định dạng phía sau cùng một lời gọi, vì vậy script giống hệt này cũng diff được các cặp PDF hoặc XLSX.

Một thuộc tính nữa của thiết kế đáng đề cập: ranh giới API kết thúc ở hai lời gọi read_props. Mọi thứ sau chúng là Python chuẩn, vì vậy các unit test, ngưỡng và quy tắc cảnh báo không bao giờ chạm tới lớp tài liệu. Các nhóm bọc đoạn này trong một dịch vụ thường cache các dict đã trích xuất cho mỗi phiên bản và cho phép mọi kiểm tra hạ nguồn tái sử dụng chúng, giữ việc I/O tệp ở một lần mở cho mỗi phiên bản bất kể có bao nhiêu câu hỏi được đặt ra.


4. Tùy chỉnh thường gặp

Phát hiện chỉ thay đổi quyền sở hữu

Khi câu hỏi là “ai đã chạm vào tệp này”, lọc ngay khi đọc bằng các predicate tag thay vì lọc sau khi có diff đầy đủ:

# 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

Chạy cùng vòng lặp delta trên hai dict này, sử dụng <missing> làm giá trị mặc định để một trường biến mất vẫn được hiện ra. Predicate không đặt tên trường, đó là lý do một bộ phát hiện có thể phục vụ mọi định dạng mà thư viện đọc.

Theo dõi dòng thời gian chỉnh sửa

Thay đổi predicate thành Tags.time cộng với các quy tắc tên bộ đếm và bộ phát hiện sẽ báo cáo các di chuyển của RevisionNumber, TotalEditingTime và 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)))

Xuất báo cáo kiểm toán

Những phát hiện chỉ ở console sẽ chết ở đó. Bốn cột bao phủ bảng tính và trường hợp 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])

Kho lưu trữ cũng bao gồm một exporter JSON với schema ba bản đồ ổn định cho các bảng điều khiển và API quản lý trường hợp.


Nơi áp dụng thực tế

Ba triển khai liên tục xuất hiện. Các pipeline tiếp nhận diff mỗi tài liệu đến với bản sao đã có trong hồ sơ và cách ly các cặp có thay đổi quyền sở hữu. Các công việc tuân thủ chạy diff theo lịch và lưu trữ CSV cho mỗi cặp, xây dựng một dòng thời gian thuộc tính mà không ai phải tái tạo sau này. Và công cụ tranh chấp chạy cả hai bộ phát hiện theo yêu cầu, vì khi một khiếu nại xuất hiện câu hỏi mở đầu luôn là ai đã chạm vào tệp và khi nào, không phải thay đổi gì ở đoạn văn thứ tư.

Mẫu thứ tư, diff một tệp với bản snapshot cuối cùng được biết là tốt, tái sử dụng cùng mã với một dict đã lưu ở một phía. Trong tất cả các trường hợp, tệp xuất là sản phẩm cuối; output console chỉ là tiếng ồn tiến trình. Mẫu exit-code của script tuân theo main.py trong kho, vì vậy các scheduler và CI coi một assert thất bại là một lần chạy thất bại mà không cần dây nối thêm. Không có trường hợp nào cần code ngoài những gì trang này hiển thị.


Điều gì được coi là thay đổi đáng chú ý?

Bất kỳ gì diff phân loại cộng với ngữ cảnh bạn thêm vào. Các thuộc tính được thêm và xóa luôn đáng xem xét vì chúng cho thấy cấu trúc đã thay đổi chứ không chỉ giá trị. Đối với các mục đã thay đổi, hầu hết các nhóm cảnh báo trước tiên trên các nhóm danh tính và phiên bản, còn phần còn lại chỉ là thông tin. Các bộ phát hiện tồn tại để lần đầu tiên chỉ tốn một lời gọi hàm.


5. Tham chiếu nhanh: Các lời gọi chính

Lời gọi Mô tả
Metadata(path) Mở tệp; trình quản lý ngữ cảnh xử lý giải phóng
find_properties(predicate) Trả về mọi thuộc tính mà predicate chấp nhận, trên tất cả các lớp
p.interpreted_value Giá trị dễ đọc; nếu không có sẽ dùng p.value
Tags.person.* / Tags.corporate.company Phân loại danh tính, không phụ thuộc vào định dạng
Tags.time.* Phân loại thời gian cho bộ phát hiện phiên bản

Xem [complete API reference] (https://reference.groupdocs.com/metadata/python-net/) để biết toàn bộ khả năng tìm kiếm và gắn thẻ. Từ vựng thẻ lớn hơn các hàng này; các nhóm thẻ origin, content và legal tuân theo cùng một kiểm tra thành viên.


6. Vấn đề thường gặp & Giải pháp

Diff quá lớn và đọc giống như nhiễu
→ Hai đường dẫn có thể không phải là các phiên bản của cùng một tài liệu. Sửa: xác thực nguồn gốc trước khi diff; các tệp không liên quan sẽ tạo ra delta vô nghĩa.

Trường tác giả đã biết không bao giờ xuất hiện trong bộ phát hiện quyền sở hữu
→ Một số nhà sản xuất lưu danh tính trong các trường tùy chỉnh không được gắn thẻ. Sửa: chạy diff đầy đủ một lần, tìm tên trường thực tế, và mở rộng predicate bằng quy tắc tên.

Console shows an evaluation-mode warning
→ Không tìm thấy tệp giấy phép. Sửa: chỉ định LICENSE_PATH trong main.py tới tệp .lic của bạn, hoặc giữ chế độ đánh giá cho phát triển; logic vẫn giống nhau.

Dates print as raw serial numbers
p.value thô đã lọt vào một phần đọc nào đó. Sửa: giữ mẫu interpreted_value-first trong read_props; đó là lý do báo cáo vẫn đọc được.


Tiếp theo là gì?

Bạn đã có một diff metadata hoạt động. Dưới đây là các bước tiếp theo:

Tài nguyên