💡 完整的工作示例可在 GitHub 上获取:
office-metadata-pii-cleanup-nodejs
Introduction
上传端点接受来自员工的 DOCX 文件并将其存储到客户工单中。文本本身没有问题,属性却不行:文件记录了起草者、最后保存者、公司模板中的部门经理,以及因为文件来自 SharePoint 而记录的审批人。
元数据清理器是一个小脚本,在文件存储之前删除这些属性,并随后检查自己的工作。本文教程使用 Node.js 与 GroupDocs.Metadata 构建这样一个脚本,分四步完成:按标签选择属性、按名称选择属性、在选择性失效时一次性清除所有属性、以及验证剩余内容。每一步只需几行代码,完整脚本不足百行。
Why Metadata Sanitization Matters
这些数据会在无人主动选择的情况下累积。Word 在每次保存时会写入 Author 和 LastSavedBy(来源于操作系统账户),维护修订计数、记录 TotalEditingTime,以及 LastPrinted。文档服务器会在签入时添加工作流路径、审批人标识和内容类型 URI。上述信息在文档阅读或打印时并不显示,因此校对时永远捕捉不到。
在 Node.js 中实现而不是手工操作的意义在于脚本会返回数字:每次删除调用会报告删除了多少属性,这个计数可以写入日志、在测试中断言,或附加到文档所属的记录上。
还有第二个原因——在批处理作业运行时更为明显。手动清理是由当时处理文件的人员一次性决定的,同一种文档由不同人清理会产生不同结果。脚本把规则固定在一个地方:相同的四个标签、相同的子字符串列表,无论队列中有三份文件还是三千份,都以完全相同的方式应用。
Prerequisites
该包通过 Java 在 Node.js 中运行,因此机器需要同时具备 Java 运行时和 Node。
Installation
npm install @groupdocs/groupdocs.metadata
示例项目锁定在 26.7 版本,并在 overrides 中将 nan 设置为 ^2.22.0,以保证在当前 Node 版本上仍能构建本机绑定。没有许可证文件时,库会以评估模式运行,足以完成本文的所有步骤。
Step 1 - Select properties by what they mean
不同格式和包的属性名称各不相同,因此第一条规则改为按标签匹配。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);
关键要点:
- 四个标签覆盖了身份组:创建者、编辑者、经理以及公司字段。
- Title、Subject 和 Keywords 保持不变,因而基于这些字段的记录索引仍能正常工作。
removeProperties返回受影响的计数,而不是布尔值。
请将整个过程放在 try/finally 中,并在 finally 里调用 metadata.close()。绑定会一直保持文件打开状态,若不关闭,在循环中会耗尽句柄。
Step 2 - Select properties by name
评论线程、修订计数和服务器字段没有标签。对于这些情况,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 相关属性,调用方式相同,只需传入 Server、Workflow、Approver、ContentType、Template 等子串。使用子串匹配是有意为之:它能一次捕获 CommentsCount 与 Comment,无需为每种格式维护完整的属性名列表。
Step 3 - Wipe everything when selectivity stops helping
对于最终要离开组织的副本,只需一次调用即可替代前面的四次遍历:
const affected = metadata.sanitize();
metadata.save(outputPath);
sanitize() 会清除库检测到的所有元数据包(包括自定义 OOXML 部分),其返回的计数通常会超过所有针对性遍历的总和。它同样会删除 Title 和 Subject,这也是它应在最终阶段而非审查循环中使用的原因。
Step 4 - Verify, because a silent miss looks like success
扫描再次使用相同的规格,通过 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}`);
}
空值和零值过滤是有原因的。我在一次运行中发现一个已被清零的修订计数仍被报告为残留属性,于是加入了这段过滤逻辑。
Complete Working Example
仓库将六个函数组合进 index.js,该文件加载许可证、对 resources/pii-sample.docx 依次执行每个遍历、断言每个输出文件存在,并最终断言泄漏列表为空。若断言失败,进程会以非零码退出,这使得整个脚本可以作为 CI 检查而非仅供演示的示例。
有一点值得在自己的实现中复制:每次遍历都读取同一源文件并写入独立的输出,而不是把一次清理后的文件作为下一次的输入。这样可以保持各遍历的受影响计数相互独立,评论遍历的日志行会显示评论规则本身找到的属性数量,而不是在身份规则执行后剩余的数量。
When should I run a targeted pass instead of sanitize()?
只要文档仍在使用中就应采用针对性遍历。处于审阅流程的文件依赖 Title、Subject 和 Keywords 进行搜索和分类,而 sanitize() 会同时删除这三者以及个人数据。协作期间运行身份和评论遍历,保留描述性字段;在文档最终离开组织时再执行完整的 sanitize()。
Real-World Applications
Upload handler
Express 路由在将附件写入存储之前先进行清理,将受影响计数记录到工单上,并在泄漏列表非空时拒绝上传。
Nightly export job
后台任务遍历导出文件夹,执行身份和服务器遍历;若仍检测到残留 PII,则任务直接失败,而不是仅记录警告。
Pre-publication gate
构建步骤在发布前清理文档附件,使用 sanitize() 因为这些文件不需要保留任何元数据。
Best Practices and Tips
- 始终写入新路径,以便原始文件在争议解决时仍可保留。
- 在
finally块中关闭 metadata 对象,尤其是在循环中使用时。 - 在批处理作业中使用
.or()合并规格;一次打开一次保存要比四次打开四次保存更高效。 - 记录每次遍历的受影响计数,即使为零,这样可以发现未被识别的格式。
Troubleshooting Common Issues
受影响计数为零,但文档明显脏 先确认输入格式是否被库识别;未识别的文件和干净的文件都会返回零。
泄漏检查报告了刚刚删除的属性 请将扫描指向已保存的输出路径,而不是原始输入路径。扫描会读取它所收到的文件。
Word 中仍然显示评论气泡 评论文本存放在文档正文中,而不是元数据包。GroupDocs.Metadata 会清除与评论相关的属性,但要去除气泡本身需要使用如 Aspose.Words 之类的内容编辑库。
Conclusion
四步、六个函数、一个脚本即可报告所做的全部工作。标签规格处理跨格式的身份组,名称规格覆盖标签未分类的属性,sanitize() 负责边界清理,泄漏扫描将整个过程转化为检查。克隆仓库,对经过真实审阅的文档运行它,观察计数后再决定流水线需要哪些遍历。