💡 完整可執行範例可於 GitHub 取得:
remove-pii-from-office-metadata-java

合規挑戰:為何手動檢查中繼資料在大規模時失效

一個紀錄團隊向外部稽核員送出 200 份文件。有人已閱讀每一頁,卻沒有人看過屬性,而個人資料正隱藏在屬性中:Author 中的分析師、LastSavedBy 中的第二位分析師、Manager 中的部門主管、Company 中的子公司、截止日前一晚的 LastPrinted 時間戳,以及任何經過 SharePoint 的文件,都會帶有批准者 ID 與工作流程路徑。

Metadata 清理是 GroupDocs.Metadata 在 Java 中的工作流程,會從 Word、Excel、PowerPoint 檔案中移除這些帶有身分資訊的屬性,然後讀回結果以報告仍然存留的項目。本文將依照合規團隊的建構方式說明此工作流程:有哪些屬性群組、每個群組適用哪種移除規則、何時以完整抹除取代目標式清理,以及為何驗證必須與清理同屬一個工作,而非放在清單中。

規模問題不在於移除困難,而在於手動檢查不會產生紀錄。稽核員若問「此檔案移除了哪些欄位、何時移除」需要一個數字,而屬性對話框根本不會提供。

為何一般清理工具在此無法使用

Windows 屬性對話框一次只能編輯一個檔案,且只能觸及部份欄位。Office 的 Document Inspector 需要互動操作,無法納入夜間工作。兩者都不會處理自訂 OOXML 部分,且也不會寫入任何管線可再次讀取的資訊。

若直接編輯 docProps/core.xmldocProps/custom.xml,則必須為每個欄位、每種格式維護一個 XPath 表達式,且每當產生應用程式變更名稱時都要重新檢視。這樣的做法也會錯誤地處理分類問題:屬性名稱在不同套件間不同,名稱清單會在不知情的情況下過時,而不再匹配的規則看起來會與已清潔的檔案相同。

解決方案:在紀錄工作流程中使用 GroupDocs.Metadata

GroupDocs.Metadata for Java 會將所有操作都導向同一個屬性搜尋引擎。Specification 物件決定哪些屬性符合條件,removeProperties 會刪除每個符合的屬性並回傳受影響的數量,findProperties 則以唯讀方式執行相同的判斷。屬性帶有標籤,因此 Tags.getPerson().getCreator() 能辨識作者類型的欄位,無論其來源格式或套件為何。

Java 沒有 removeProperties 的 lambda 重載,這在此反而是優勢:每條規則都是一個物件,且物件可重複使用。用來清除某個群組的同一個 Specification 實例可以直接交給驗證掃描,讓檢查不會偏離原本要測試的清理行為。

逐步實作清理管線

步驟 1 – 依標籤清除身分群組

四個以 .or(...) 結合的標籤規格涵蓋 creator、editor、manager 與 company。其他欄位不受影響,Title、Subject、Keywords 仍可供紀錄索引使用。

try (Metadata metadata = new Metadata(inputPath)) {
    if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
    int affected = metadata.removeProperties(
            new ContainsTagSpecification(Tags.getPerson().getCreator())
                .or(new ContainsTagSpecification(Tags.getPerson().getEditor()))
                .or(new ContainsTagSpecification(Tags.getPerson().getManager()))
                .or(new ContainsTagSpecification(Tags.getCorporate().getCompany())));
    metadata.save(outputPath);
    return affected;
}

FileFormat.Unknown 的防護比看起來更重要。若未檢查,無法讀取的檔案會回傳零次移除,呼叫端無法分辨這是「本來就乾淨」還是「根本沒讀到」的情況。

步驟 2 – 依名稱清除欄位族群

評論串、修訂計數與伺服器欄位沒有標籤,只能依名稱匹配。小型的 Specification 子類別接受可變長度的子字串清單,讓同一個類別服務三個不同的通過:

public class NameContainsSpec extends Specification {
    private final String[] needles;

    public NameContainsSpec(String... needles) {
        this.needles = needles;
    }

    @Override
    public boolean isSatisfiedBy(MetadataProperty candidate) {
        String name = candidate.getName();
        if (name == null) return false;
        for (String n : needles) {
            if (name.contains(n)) return true;
        }
        return false;
    }
}

編輯時間線是群組合規最關心的,因為修訂次數與最後列印日期說明了文件的產出方式:

try (Metadata metadata = new Metadata(inputPath)) {
    if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
    int affected = metadata.removeProperties(new NameContainsSpec(
            "Revision", "TrackedChange", "LastPrinted", "TotalEditingTime", "EditTime"));
    metadata.save(outputPath);
    return affected;
}

評論通過與 SharePoint 通過只是不同行的子字串清單:CommentReviewerReviewed 用於審查痕跡;ServerWorkflowApproverContentTypeTemplate 用於文件伺服器欄位。

步驟 3 – 在邊界處全面抹除

當檔案離開組織時,選擇性已不再重要。一次呼叫即可清除程式庫偵測到的所有中繼資料套件,包含自訂 OOXML 部分,這些部分不會被任何目標式謂詞捕捉:

try (Metadata metadata = new Metadata(inputPath)) {
    if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
    int affected = metadata.sanitize();
    metadata.save(outputPath);
    return affected;
}

步驟 4 – 驗證並分類剩餘項目

掃描會將所有規則的聯集透過 findProperties 執行,並將命中結果分成兩個清單。空值與零計數會被略過,名稱以 CommentRevisionInspection 開頭的條目屬於正文內容的包裝,而非純粹的中繼資料:

String value = "";
if (p.getValue() != null && p.getValue().getRawValue() != null) {
    value = String.valueOf(p.getValue().getRawValue());
}
if (value.isEmpty() || value.equals("0") || value.equals("0.0")) continue;
String entry = p.getName() + "=" + value;
String name = p.getName() == null ? "" : p.getName();
if (name.startsWith("Comment") || name.startsWith("Revision")
        || name.startsWith("Inspection")) {
    report.contentLevelLeaks.add(entry);
} else {
    report.metadataLeaks.add(entry);
}

這樣的分割能讓通過/失敗訊號保持誠實。Metadata 漏洞必須為空;內容層面的漏失則僅作資訊呈現,因為 Word 評論與追蹤變更的作者資訊位於 word/document.xml,而中繼資料程式庫只能報告而不會編輯;若要移除必須使用如 Aspose.Words 之類的內容編輯程式庫。

何時使用目標式通過勝過完整抹除?

只要文件仍在協作中。流轉於審閱者之間的檔案需要 Title、Subject、Keywords 以供搜尋與紀錄分類,而 sanitize() 會把這三個欄位也抹除。於協作階段執行身分與評論通過,保留描述性欄位,待檔案跨越至外部時再執行完整抹除。

真實工作流程:外部稽核員的匯出作業

想像一個夜間作業。它會讀取文件 ID 清單,將每個檔案複製到暫存路徑,套用身分通過與伺服器通過,對任何標記為離開組織的檔案呼叫 sanitize(),最後對已儲存的副本執行漏失檢查。每個步驟都會把受影響的計數加到每個檔案的一行日誌中,若 metadata‑leak 清單非空則整個作業失敗,而不是僅記警告。

我曾花一個下午調整此作業的版本,結果 40 檔案全部顯示零次移除,看起來像是乾淨批次。原因是輸入資料夾裡都是舊版 .doc 二進位檔,格式防護在每個檔案上提前返回,且日誌中沒有區分「沒有可移除項目」與「根本未讀取」的資訊。將格式資訊與計數一起記錄後問題即解決。

商業影響:此變更帶來的好處

方面 手動審查 GroupDocs.Metadata 管線
覆蓋範圍 屬性對話框可見的欄位 程式庫偵測到的每個套件,包含自訂 OOXML 部分
工作紀錄 只有筆記(若有人寫的話) 每個檔案及每個屬性群組的受影響計數
可重複性 依執行者而異 每條規則一個 Specification,對每個檔案皆以相同方式套用
驗證方式 重新開啟檔案檢視 使用 findProperties 掃描並進行雙向分類
規模 一次只能處理一個檔案 同樣的六個操作在匯出資料夾的迴圈中執行

其他適用 GroupDocs.Metadata 的情境

相同的屬性引擎既能讀取也能移除。比較兩個文件版本的中繼資料,可看出哪一次編輯改變了什麼,這對所有權爭議與偵測文件在流程外重新授權非常有用。使用metadata tags 而非名稱,使得上述兩種情況在 DOCX、XLSX、PPTX、PDF 與影像格式間皆具可移植性。

快速上手 GroupDocs.Metadata for Java

pom.xml 中加入 GroupDocs Java 套件庫,並以 com.groupdocs:groupdocs-metadata 為依賴。程式庫在未授權的評估模式下即可執行所有六項操作於範例 DOCX,並看到計數結果。先從身分通過開始,依文件來源需求逐步加入欄位族群通過,最後在投入生產前加入漏失檢查。

資源