💡 전체 작동 예제는 GitHub에서 확인할 수 있습니다:
document-version-metadata-diff-python

구축할 내용

이 가이드에서는 두 버전의 문서 사이의 모든 메타데이터 속성을 비교하고, 추가, 삭제, 변경된 내용을 정확히 출력합니다. 메타데이터 버전 차이는 하나의 파일 두 개정에 대한 속성 수준 비교이며, 텍스트 비교에서는 절대 볼 수 없는 신호를 포착합니다: 새로운 Creator, 증가된 RevisionNumber, 검토 종료 후 기록된 편집 세션 등. 최종적으로 실행 가능한 솔루션과 두 개의 집중 탐지기, 두 가지 내보내기 형식을 얻게 되며, 샘플 개정 쌍이 포함된 저장소에서 바로 실행할 수 있습니다.

숙련도: 중급 Python 개발자
필요 사항: Python 3, pip, 하나의 문서 두 개정

제가 처음 이 스크립트를 실행했을 때 팀에서 누가 변경했는지 기억하지 못했던 Company 값 변경을 감지했습니다; 그 한 줄이 설정 비용을 상쇄했습니다. 아래 내용은 복사‑붙여넣기만 하면 100줄 미만으로 동작합니다.

파이프라인은 의도적으로 단순합니다: 파일 두 개 열기, 딕셔너리 세 번 생성, 출력 루프. 단순함이 핵심입니다. 버전 분쟁은 방법을 설명하고 재현할 수 있느냐에 따라 결정되며, 이렇게 작은 스크립트는 결과에 이의를 제기하는 사람도 전체를 읽을 수 있습니다.


1. 설치

pip install groupdocs-metadata-net==26.5

동반 저장소는 이 버전을 고정하고 document-v1.docxdocument-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: 파일을 열고 종료 시 해제하는 컨텍스트 매니저; 개정당 하나의 인스턴스.
  • find_properties: 내장 필드, 사용자 정의 속성, XMP를 한 번에 탐색하며, 조건에 맞는 모든 항목을 반환합니다.
  • interpreted_value: 속성의 사람이 읽을 수 있는 형태; 이를 우선 사용하면 날짜와 열거형이 문자열로 비교되어 보고서에 바로 출력할 수 있습니다.
  • 키로서의 정규화된 이름: 내장 및 사용자 정의 필드가 딕셔너리에서 충돌하지 않으므로 집합 논리가 안전하게 동작합니다.

여기서는 DOCX 구조를 파싱하지 않습니다. 제품 문서에는 동일한 호출로 170개 이상의 형식을 지원한다고 나와 있으므로, 동일 스크립트로 PDF나 XLSX 쌍도 차이를 낼 수 있습니다.

디자인상의 또 다른 특징은 API 경계가 read_props 두 호출에서 끝난다는 점입니다. 그 이후는 표준 라이브러리 Python만 사용하므로, 단위 테스트, 임계값, 알림 규칙이 문서 레이어에 전혀 영향을 주지 않습니다. 이 코드를 서비스에 래핑하는 팀은 보통 개정당 추출된 딕셔너리를 캐시하고, 하위 검사에서 재사용해 파일 I/O를 버전당 한 번만 수행하도록 합니다.


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])

저장소에는 대시보드와 케이스 관리 API용 안정적인 3‑맵 스키마를 가진 JSON 내보내기도 포함되어 있습니다.


5. 실제 적용 사례

세 가지 배포 패턴이 흔합니다. 인제스트 파이프라인은 도착 문서를 기록된 사본과 차이내어 신원 변경이 있는 쌍을 격리합니다. 컴플라이언스 작업은 일정에 따라 diff를 실행하고 CSV를 쌍별로 보관해 나중에 속성 타임라인을 재구성할 필요가 없게 합니다. 분쟁 도구는 필요 시 두 탐지기를 모두 실행하는데, 청구가 들어오면 항상 “누가 언제 파일을 건드렸는가”가 첫 질문이며, 네 번째 문단의 내용은 중요하지 않습니다.

네 번째 패턴은 파일을 자체 최신 정상 스냅샷과 비교하는 것으로, 한쪽에 저장된 딕셔너리를 재사용합니다. 모든 경우에서 내보내기 파일이 최종 산출물이며, 콘솔 출력은 진행 상황에 불과합니다. 스크립트의 종료 코드 패턴은 저장소의 main.py와 동일하므로 스케줄러와 CI는 별도 설정 없이 실패를 감지합니다. 여기서 보여준 코드 외에 추가된 코드는 없습니다.


6. 플래그를 달아야 할 변경 사항은?

diff가 분류한 모든 항목에 여러분이 추가하는 컨텍스트가 포함됩니다. 추가·삭제된 속성은 구조가 바뀐 것이므로 항상 검토 대상이며, 변경된 항목은 대부분의 팀이 먼저 신원·리비전 그룹에 알림을 설정하고 나머지는 정보 수준으로 처리합니다. 탐지기는 첫 번째 패스가 한 번의 함수 호출만으로 끝나도록 설계되었습니다.


7. 핵심 호출 요약

호출 기능
Metadata(path) 파일을 열고, 컨텍스트 매니저가 해제 처리
find_properties(predicate) 모든 레이어를 아우르는, 조건에 맞는 속성을 반환
p.interpreted_value 사람이 읽을 수 있는 값; 없을 경우 p.value 사용
Tags.person.* / Tags.corporate.company 형식에 구애받지 않는 신원 분류
Tags.time.* 리비전 탐지기를 위한 타임스탬프 분류

전체 검색 및 태깅 인터페이스는 전체 API 레퍼런스에서 확인할 수 있습니다. 태그 어휘는 여기서 소개한 행보다 더 풍부하며, origin, content, legal 태그 그룹도 동일한 멤버십 테스트로 활용됩니다.


8. 흔한 문제와 해결 방법

diff가 너무 커서 잡음처럼 보인다
→ 두 경로가 같은 문서의 개정이 아닐 가능성이 높습니다. 차이를 내기 전에 출처를 검증하세요; 관련 없는 파일은 의미 없는 delta를 생성합니다.

알려진 author 필드가 소유권 탐지기에 나타나지 않는다
→ 일부 제작자는 태그가 없는 사용자 정의 필드에 신원을 저장합니다. 전체 diff를 한 번 실행해 실제 필드명을 찾은 뒤, 이름 규칙을 추가해 조건을 확장하세요.

콘솔에 평가 모드 경고가 표시된다
→ 라이선스 파일을 찾을 수 없습니다. main.pyLICENSE_PATH.lic 파일 위치로 지정하거나, 개발 단계에서는 평가 모드로 유지해도 로직은 동일합니다.

날짜가 원시 시리얼 번호로 출력된다
→ 어느 곳에서든 p.value가 직접 사용된 것입니다. read_props에서 interpreted_value를 먼저 사용하도록 유지하면 보고서가 읽기 쉬워집니다.


9. 다음 단계는?

작동하는 메타데이터 diff를 확보했습니다. 이제 할 일은 다음과 같습니다:

  • 배치 처리: 스크립트를 문서 쌍에 대해 반복 실행하고, 각 쌍마다 CSV를 저장합니다. 쌍당 비용은 파일 두 번 열기뿐이며, CSV를 연결하면 라이브러리 전체 뷰를 얻을 수 있습니다.
  • 스케줄링: 저장소의 main.py는 모든 단계를 검증하고 적절한 종료 코드를 반환하므로 CI나 스케줄러에 바로 연결할 수 있습니다.
  • 튜토리얼 버전 따라하기: 사용 사례 가이드에서 동일 파이프라인을 세 단계 튜토리얼로 배울 수 있습니다.
  • 전체 프로젝트 보기: document-version-metadata-diff-python에서 샘플 개정 쌍을 확인하세요.

참고 자료