💡 Full working example available on GitHub:
sanitize-office-document-pii-python

在點擊發送前沒有人審查的資料

一份季度董事會報告會送交外部稽核師。文字已經完美無瑕;經過三次審核循環才確保如此。檔案本身則是另一回事。它的屬性仍然保留了起草者的分析師姓名、重新編輯的經理、擁有範本的公司子公司、截止日前一晚的 LastPrinted 時間戳記,以及內部簽核工作流程的 SharePoint 批准者 ID。這些資訊不會出現在任何頁面上,卻隨檔案一起流動。

PII 移除是透過 .NET 的 GroupDocs.Metadata for Python 工作流程,能以程式方式剝除 Word、Excel 與 PowerPoint 檔案中承載身分資訊的屬性。本文比較 API 提供的三種做法:針對身分欄位的標籤驅動移除、針對如註解與修訂等屬性族群的名稱模式移除,以及一次性呼叫 sanitize() 以清除全部。您還會看到大多數清理腳本會略過的步驟——驗證掃描,以證明清理確實生效。

為何 Metadata PII 應該擁有自己的管線

內容審查工具檢查人們閱讀的文字,卻不會檢查檔案系統儲存的資訊,而這個缺口正是合規事件的根源。GDPR 請求會涵蓋 Author 與 Manager 欄位中的個人資料,就像涵蓋文字內容一樣。法律發現會讀取修訂計數與編輯時間總計,以重建立場文件的協商過程。投標審查者可以從 SharePoint 工作流程屬性中繪製您的組織結構,而新聞稿的註解欄位則會保留審閱者姓名與草稿階段的備註。每一項都是發現點,卻都不會出現在文件正文中。

前置條件

開始之前,請確保您已具備:

  • Python 3 與 pip
  • 透過 .NET 的 GroupDocs.Metadata for Python,範例倉庫固定使用 26.5 版
  • 一個帶有真實屬性的 Office 檔案,以便練習

安裝

pip install groupdocs-metadata-net==26.5

companion repository 會提供範例 DOCX,並將下列每段程式碼作為斷言管線執行。

方法 1:標籤驅動的身份移除

最敏感的四個欄位——Author、LastSavedBy、Manager 與 Company——在不同 Office 格式中有不同的內部名稱。標籤系統解決了這個問題:不必指定屬性名稱,只要要求所有被標記為人員或公司的項目即可。

# Match identity properties by meaning, not by format-specific name
with Metadata("board-report.docx") as metadata:
    removed = metadata.remove_properties(lambda p:
        Tags.person.creator in list(p.tags)     # Author, LastSavedBy
        or Tags.person.editor in list(p.tags)
        or Tags.person.manager in list(p.tags)
        or Tags.corporate.company in list(p.tags))
    metadata.save("board-report-clean.docx")

print(f"{removed} identity properties removed")

重點:

  • 格式獨立性:相同的 lambda 可清理 DOCX、XLSX 與 PPTX,因為標籤是依角色分類的。
  • 可計數的結果remove_properties 會回傳匹配的屬性數量,適合寫入稽核日誌。
  • 複製語意:儲存至新路徑可保留原始檔供紀錄使用。

💡 Tip:此步驟會保留 Title、Subject 等描述性欄位,使檔案仍然友善於搜尋與 DMS 索引。

方法 2:屬性族群的名稱模式移除

標籤涵蓋已分類的概念。某些洩漏欄位族群則不在此分類之列:註解屬性、修訂計數、SharePoint 工作流程印記。對於這些,直接以屬性名稱本身匹配。

# Comment fields often live in custom properties the tag system
# does not classify, so match them by name substring
with Metadata("board-report.docx") as metadata:
    removed = metadata.remove_properties(lambda p:
        p.name is not None and (
            "Comment" in p.name
            or "Reviewer" in p.name
            or "Reviewed" in p.name))
    metadata.save("board-report-no-comments.docx")

相同的寫法也適用於其他兩個族群,只需更換子字串清單:

族群 要匹配的子字串
修訂軌跡 Revision, TrackedChange, LastPrinted, TotalEditingTime, EditTime
伺服器 / 工作流程 Server, Workflow, Approver, ContentType, Template

此方式以廣度換取精確度:"Comment" 也會捕捉 CommentsCommentCount,這通常正是清理時想要的。過寬的子字串可能會匹配到無害的範本欄位,因此請將回傳的計數與預期值比對。

💡 Tip:當稽核日誌需要按類別計數時,請將每個族群分別執行;若不需要,可將所有子字串合併成單一判斷式。

方法 3:一次性完整清理

當檔案即將離開組織,且不希望任何 metadata 層級的資訊存留時,直接停止寫判斷式。

# One call, every detected metadata package
with Metadata("board-report.docx") as metadata:
    removed = metadata.sanitize()
    metadata.save("board-report-final.docx")

print(f"sanitize() removed {removed} properties")

sanitize() 會清除程式庫偵測到的所有套件:文件資訊身分欄位、註解、修訂歷史、追蹤變更作者以及自訂 OOXML 部分。其行為在 Clean metadata 頁面有說明。它的優勢也是代價:Title 與 Subject 會與 PII 一起消失,因此適合放在匯出關卡,而非協作流程的中間。

我需要所有四個目標化的步驟嗎?

不需要。每個步驟的存在是因為不同團隊負責不同風險。身分欄位會讓隱私官關注,註解軌跡會讓法務關注,修訂計數會讓談判者關注,伺服器欄位會讓資安關注。依照您的審查者需求執行相應的步驟,順序不拘,因為每一步都會寫入自己的輸出副本。若不需要保留任何欄位,直接跳到 sanitize() 並驗證即可。

比較三種方法

方法 最適用情境 主要優勢 限制
標籤驅動移除 工作副本、多格式管線 格式獨立、保留描述性欄位 僅涵蓋已標籤的概念
名稱模式移除 註解、修訂、伺服器欄位 能觸及標籤未涵蓋的自訂屬性 子字串需依環境調整
完整 sanitize() 組織外部最終匯出 不會遺漏任何屬性 會同時清除無害欄位

這三種做法可以自然組合:文件仍在使用時執行目標化的步驟,匯出時使用 sanitize()

在信任之前先驗證

僅憑移除呼叫回傳的計數並不能證明檔案已乾淨。倉庫在每次執行完畢後,都會重新開啟已清理的輸出,並使用 find_properties 搭配結合所有前述標籤與名稱規則的判斷式進行掃描。

def is_pii(p):
    if p.name is None:
        return False
    return (
        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)
        or any(n in p.name for n in (
            "Comment", "Reviewer", "Revision", "TrackedChange",
            "Classification", "Department", "Server", "Workflow")))

with Metadata("board-report-final.docx") as metadata:
    for p in metadata.find_properties(is_pii):
        value = (str(p.interpreted_value) if p.interpreted_value is not None
                 else (str(p.value) if p.value is not None else ""))
        if value and value not in ("0", "0.0"):
            print(f"LEAK {p.name}={value}")

完整版本會將殘留項目分成兩個桶,且這個區分很重要。Metadata 洩漏必須為零。內容層面的遺留(如 Word 註解氣泡與 word/document.xml 內的追蹤變更)屬於正文內容,metadata API 無法處理;需要使用如 Aspose.Words 之類的內容編輯函式庫。誠實的報告會同時列出兩個桶,而不是在第一個桶就宣告勝利。第一次在「乾淨」檔案上執行此掃描時,我發現一個 Department 欄位被企業範本悄悄重新加入了好幾個月。

最佳實踐與提示

  • 清理副本,絕不直接修改原檔:本文所有程式碼片段皆寫入新路徑,以保留來源檔供紀錄與保存規則使用。
  • 記錄計數remove_propertiessanitize() 的回傳值即為稽核軌跡。請依檔案與步驟儲存。
  • 將驗證寫入 CI:漏檢檢查若失敗即可阻止建置,讓模板回歸問題在發生當天即被捕捉,而非等客戶發現。
  • 注意 metadata 與內容的界線:若正文仍有註解等內容,千萬不要宣稱檔案已乾淨;應將其列為另一項發現。
  • 授權:評估模式會在本文所有範例中留下評估標記,正式環境請使用授權,以免評估痕跡出現在外發檔案。

結論

三種做法,一條決策規則。概念已被標籤分類且檔案仍需保持可用時,使用標籤匹配;概念屬於自訂屬性族群時,使用名稱匹配;檔案跨越信任邊界時,呼叫 sanitize(),最後以讀回掃描驗證所走的路徑。

想更深入了解嗎?以下是後續步驟:

其他資源

有問題或想分享您的實作嗎?請前往 support forum 與我們交流。