💡 完整可运行示例已在 GitHub 上提供:
office-metadata-pii-cleanup-nodejs

介绍

一个上传端点接受来自员工的 DOCX 文件并将其存储到客户工单中。文本本身没有问题,属性却不行:文件记录了起草者、最后保存者、公司模板中的部门经理,以及因为文件来自 SharePoint 而记录的批准人。

元数据清理器是一个小脚本,在文件存储之前删除这些属性,然后自行检查结果。本文教程使用 Node.js 与 GroupDocs.Metadata 构建此脚本,分四步完成:按标签选择属性、按名称选择属性、在选择性失效时一次性清除所有属性、以及验证剩余内容。每一步只需几行代码,完整脚本不足百行。

为什么元数据清理很重要

这些数据会在无人主动选择的情况下累积。Word 在每次保存时会写入操作系统账户的 AuthorLastSavedBy,保持修订计数,跟踪 TotalEditingTime,记录 LastPrinted。文档服务器会在签入时添加工作流路径、批准人标识和内容类型 URI。这些信息在阅读或打印时并不显示,因此校对时永远捕捉不到。

在 Node.js 中实现而不是手工操作的意义在于脚本会返回数字:每次删除调用会报告删除了多少属性,这个计数可以写入日志、在测试中断言,或附加到文档所属的记录上。

还有第二个原因——在批处理作业运行时更为明显。手动清理是由当时处理文件的人员一次性决定的,同一种文档由不同人清理会产生不同结果。脚本把规则固定在一个地方:相同的四个标签、相同的子字符串列表,无论队列中有三份文件还是三千份,都以完全相同的方式应用。

先决条件

该包通过 Java 在 Node.js 中运行,因此机器需要同时具备 Java 运行时和 Node。

安装

npm install @groupdocs/groupdocs.metadata

示例项目锁定了 26.7 版本,并在 overrides 中将 nan 设置为 ^2.22.0,以保证在当前 Node 发行版上能够成功构建原生绑定。没有许可证文件时,库会以评估模式运行,足以完成本文的所有步骤。

步骤 1 - 按含义选择属性

不同格式和包的属性名称各不相同,因此第一条规则改为按标签匹配。ContainsTagSpecification 接受一个标签并匹配携带该标签的任何属性;.or() 将多个规格合并为一个。

const T = groupdocs.Tags;
const spec = new groupdocs.ContainsTagSpecification(T.getPerson().getCreator())
  .or(new groupdocs.ContainsTagSpecification(T.getPerson().getEditor()))
  .or(new groupdocs.ContainsTagSpecification(T.getPerson().getManager()))
  .or(new groupdocs.ContainsTagSpecification(T.getCorporate().getCompany()));
const affected = metadata.removeProperties(spec);
metadata.save(outputPath);

关键要点:

  • 四个标签覆盖了身份组:创建者、编辑者、经理以及公司字段。
  • TitleSubjectKeywords 保持不变,因而基于这些字段的记录索引仍可工作。
  • removeProperties 返回受影响的计数,而不是布尔值。

请将整个过程放在 try/finally 中,并在 finally 里调用 metadata.close()。绑定会一直保持文件打开状态,若不关闭,在循环中会耗尽句柄。

步骤 2 - 按名称选择属性

评论线程、修订计数和服务器字段没有标签。对于这些情况,WithNameSpecification(needle, false) 匹配名称中包含指定子串的任何属性,下面的四行构造器会为每个子串链式创建规格:

let spec = null;
for (const needle of needles) {
  const s = new groupdocs.WithNameSpecification(needle, false /* fullMatch */);
  spec = spec ? spec.or(s) : s;
}
return spec;

三次遍历使用该构造器并提供不同的列表。首先处理评论:

const affected = metadata.removeProperties(
  nameContainsSpec(['Comment', 'Reviewer', 'Reviewed']));
metadata.save(outputPath);

编辑时间线是最容易被遗忘的组,也是描述文档生成方式的组:

const affected = metadata.removeProperties(nameContainsSpec([
  'Revision', 'TrackedChange', 'LastPrinted', 'TotalEditingTime', 'EditTime',
]));
metadata.save(outputPath);

SharePoint 相关的遍历同样调用,只是使用 ServerWorkflowApproverContentTypeTemplate。采用子串匹配是有意为之:它可以一次捕获 CommentsCountComment,而无需为每种格式维护精确的名称列表。

步骤 3 - 当选择性不再有帮助时清除所有内容

对于最终要离开组织的副本,一次调用即可替代前面的四次遍历:

const affected = metadata.sanitize();
metadata.save(outputPath);

sanitize() 会清除库检测到的所有元数据包(包括自定义 OOXML 部分),其计数通常会超过所有针对性遍历的总和。它还会删除 TitleSubject,因此应在最终阶段使用,而不是放在审查循环中。

步骤 4 - 验证,因为静默的遗漏看起来像成功

扫描再次使用相同的规格,通过 findProperties 只读不写。返回的是 Java 集合,需要通过索引遍历:

const props = metadata.findProperties(tagSpec.or(nameSpec));
for (let i = 0; i < props.getCount(); i++) {
  const p = props.get_Item(i);
  const val = p.getValue && p.getValue();
  const value = val ? String(val.getRawValue ? val.getRawValue() : val) : '';
  if (!value || value === '0' || value === '0.0') continue;
  leaks.push(`${p.getName()}=${value}`);
}

空值和零值过滤是有原因的。我在一次运行中发现一个已被清零的修订计数仍被报告为残留属性,于是加入了这段过滤逻辑。

完整工作示例

仓库将六个函数组合到 index.js 中,代码会加载许可证,对 resources/pii-sample.docx 依次执行每个遍历,断言每个输出文件均已生成,并最终断言泄漏列表为空。若断言失败,进程会以非零码退出,这使得整个脚本可以作为 CI 检查而不是仅供阅读的演示。

有一点值得在自己的实现中复制:每次遍历都读取同一源文件并写入独立的输出,而不是把一次清理后的文件作为下一次的输入。这样可以保持各遍历的受影响计数相互独立,评论遍历的日志行会显示该规则本身发现的属性数量,而不是在身份规则执行后剩余的数量。

何时应运行针对性步骤而不是 sanitize()

只要文档仍在使用中就应如此。处于审阅流程中的文件依赖 TitleSubjectKeywords 进行搜索和分类,而 sanitize() 会同时删除这三项以及个人数据。协作期间运行身份和评论遍历,保留描述性字段;在文档最终离开组织时再执行完整清除。

实际应用

上传处理程序

Express 路由在将附件写入存储之前先进行清理,将受影响计数记录到工单上,并在泄漏列表非空时拒绝上传。

每夜导出任务

后台工作遍历导出文件夹,执行身份和服务器遍历;若文档仍报告残留 PII,则任务直接失败,而不是仅记录警告。

发布前门禁

构建步骤在发布前清理文档附件,使用 sanitize() 因为这些文件不需要保留任何元数据。

最佳实践与提示

  • 始终写入新路径,以便原始文件在争议解决时仍可保留。
  • finally 块中关闭元数据对象,尤其是在循环中。
  • 在批处理作业中使用 .or() 合并规格;一次打开一次保存要比四次更高效。
  • 记录每次遍历的受影响计数,即使为零,这样未识别的格式也能被发现。

常见问题排查

受影响计数为零,但文档明显脏
先确认输入格式被库识别;未识别的文件和干净的文件都会返回零。

泄漏检查报告了刚刚删除的属性
请将扫描指向已保存的输出路径,而不是输入路径。扫描会读取它收到的文件。

Word 中仍然看到评论气泡
评论文本存放在文档正文中,而不是元数据包。GroupDocs.Metadata 只会清除与评论相关的属性,若要去除气泡本身需要使用如 Aspose.Words 之类的内容编辑库。

结论

四步、六个函数、一个脚本即可报告所做的工作。标签规格处理跨格式的身份组,名称规格覆盖标签未分类的族群,sanitize() 负责边界清理,泄漏扫描将整个过程转化为检查。克隆仓库,对经过真实审阅的文档运行脚本,观察计数后再决定流水线需要哪些遍历。

附加资源