💡 完整可运行示例位于 GitHub: extract-annotations-from-pdf-using-groupdocs-parser-dotnet
Introduction
经过审阅的 PDF 往往不仅仅包含可见的文字——还有审阅者留下的便签、突出显示的批注以及行内评论。要在每一页中手动滚动查找这些信息,随着文档经过多轮反馈后几乎不可行。GroupDocs.Parser 是一个 .NET 库,能够以编程方式读取文档中嵌入的批注,将分散的审阅评论转换为结构化数据,供代码进一步处理。 本教程演示如何一次性提取整个 PDF 的批注、按页拆分、将批注与文档文本一起提取,并将结果导出为 CSV 或 JSON。
我在为文档团队构建审阅跟踪器时遇到了这个问题:一份 40 页的发行说明经过了三位审阅者,手动打开文件查找每条评论的时间比实际修复问题的时间还长。用几行代码提取批注后,这项工作只需两分钟即可完成。
在接下来的章节中,你将学习如何:
- 一次性提取 PDF 中的所有批注。
- 为每条批注标记所属页码。
- 将文档文本与批注文本一起读取。
- 将结果序列化为 CSV 或 JSON,以供下游工具使用。
Why Extracting PDF Annotations Matters
以编程方式读取 PDF 批注的价值体现在:
- 审阅工作流:无需在 PDF 查看器中打开文件,即可收集所有审阅者的评论。
- 协作:直接在自己的工具中呈现高亮或标注的内容。
- 审计:记录文档随时间留下的标记,即使文档已被扁平化或定稿。
GroupDocs.Parser 在 26.7 版本中通过 GetAnnotations 方法为 PDF 文档新增了原生批注提取功能,并在 TextOptions 上加入了 IncludeAnnotations 选项,以便在普通文本读取时一起获取批注文本。
Prerequisites
- .NET 6.0 或更高版本
- GroupDocs.Parser for .NET 26.7+(临时许可证)
- 包含批注的 PDF 文件(例如
document-with-annotations.pdf)
通过 NuGet 安装:
dotnet add package GroupDocs.Parser
How do I extract annotations from a PDF document?
Answer: 使用 Parser 加载文件,然后调用 GetAnnotations() 获取整篇文档的批注,或调用 GetAnnotations(pageIndex) 获取单页批注。每个返回结果都是 AnnotationItem 对象的集合,其 Value 属性保存了评论文本。如果希望将评论直接嵌入文档的常规内容中,只需在 TextOptions 上设置 IncludeAnnotations 并调用 GetText。
Whole‑Document Extraction
以下代码片段一次性提取文件中的所有批注,是检查文档是否存在任何未处理评论的最快方式。
// Extract every annotation from the whole document
var result = new List<string>();
using (var parser = new Parser(path))
{
IEnumerable<AnnotationItem> annotations = parser.GetAnnotations();
if (annotations == null)
{
return result; // format doesn't support annotations
}
foreach (var item in annotations)
{
result.Add(item.Value); // annotation text
}
}
return result;
Key points:
- 当文档格式不支持批注提取时,
GetAnnotations()返回null;如果文档本身没有批注,则返回空集合。 - 每个
AnnotationItem通过Value属性暴露其文本——这是 SDK 当前唯一提供的数据点。 - 此处不包含页码信息;如果需要页码,请使用下面的按页重载。
Per‑Page Extraction
当评论所在位置很重要时,可遍历文档的每一页并对每页调用 GetAnnotations(pageIndex)。
// Tag each annotation with its zero-based page index
var result = new List<AnnotationRecord>();
using (var parser = new Parser(path))
{
if (!parser.Features.Annotations)
{
return result;
}
var info = parser.GetDocumentInfo();
if (info == null || info.PageCount == 0)
{
return result;
}
for (int pageIndex = 0; pageIndex < info.PageCount; pageIndex++)
{
IEnumerable<AnnotationItem> pageAnnotations = parser.GetAnnotations(pageIndex);
if (pageAnnotations == null)
{
continue;
}
foreach (var item in pageAnnotations)
{
result.Add(new AnnotationRecord { PageIndex = pageIndex, Value = item.Value });
}
}
}
return result;
Key points:
- 循环依据
GetDocumentInfo().PageCount;没有单独的“批注页数”概念。 GetAnnotations(pageIndex)使用零基索引,与 API 中的其他页级方法保持一致。- 最终得到的
AnnotationRecord列表正好符合 CSV 或 JSON 导出的数据结构。
Extracting Text Together with Annotations
无需对文档进行两次遍历,可以直接在普通文本提取输出中合并批注文本。
// Read document text with annotation text included
using (var parser = new Parser(path))
{
var options = new TextOptions
{
IncludeAnnotations = true
};
using (TextReader reader = parser.GetText(options))
{
return reader?.ReadToEnd() ?? string.Empty;
}
}
Key points:
IncludeAnnotations是TextOptions的属性,因此可以在已有的GetText调用中直接使用,无需额外代码。- 当你需要一次性得到完整的文字记录(包括批注)而不是单独的评论列表时,这种方式非常实用。
- 如只需对单页进行此操作,可使用
GetText(pageIndex, options)。
Checking Annotation Support First
并非所有格式都支持批注,因此在编写基于 GetAnnotations 的逻辑前,先检查一下更为稳妥。
// Returns true if the loaded document format supports annotation extraction
using (var parser = new Parser(path))
{
return parser.Features.Annotations;
}
Key points:
Features.Annotations是Parser实例上的一个布尔标记。- 预先检查可以让意图更加明确,尽管
GetAnnotations已经会在不支持时返回null。
Exporting the Annotations to CSV
CSV 导出可以让审阅者直接在 Excel 中打开评论列表。下面的方法将前面构建的带页码记录写入两列文件(page,value)。
var sb = new StringBuilder();
sb.AppendLine("page,value");
foreach (var record in records)
{
sb.AppendLine($"{record.PageIndex},{CsvEscape(record.Value)}");
}
File.WriteAllText(outputPath, sb.ToString());
Key points:
CsvEscape会对包含逗号、引号或换行符的字段进行安全转义。- 生成的文件可直接在 Excel 中打开,或导入到工单系统中。
Helper: CsvEscape
if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;
Exporting the Annotations to JSON
对于需要程序化消费评论的流水线,JSON 数组通常比平面 CSV 更合适。
var sb = new StringBuilder();
sb.AppendLine("[");
for (int i = 0; i < records.Count; i++)
{
var comma = i < records.Count - 1 ? "," : string.Empty;
sb.AppendLine($" {{ \"page\": {records[i].PageIndex}, \"value\": \"{Escape(records[i].Value)}\" }}{comma}");
}
sb.AppendLine("]");
File.WriteAllText(outputPath, sb.ToString());
Key points:
- 输出是一个由
{ page, value }对象组成的扁平数组,任何下游服务都能轻松反序列化。 Escape在不引入序列化库的情况下,确保负载符合 JSON 规范。
Helper: Escape
return s?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? string.Empty;
Comparing Methods: When to Use Each
| 方法 | 适用场景 | 关键优势 | 限制 |
|---|---|---|---|
| Whole‑Document Extraction | 快速判断文档是否存在任何评论 | 单次调用,代码最简 | 不提供页码信息 |
| Per‑Page Extraction | 将反馈路由到对应章节 | 带页码的结果,便于导出 | 每页多一次调用 |
| Combined Text + Annotations | 生成单一可读的全文记录 | 无需二次遍历文档 | 评论未与正文分离 |
| CSV Export | 基于电子表格的审阅跟踪 | 易于在 Excel 中打开,直观可读 | 结构扁平,表达能力有限 |
| JSON Export | 自动化流水线、工单系统 | 结构化、机器可读 | 相对占用更大字节 |
建议先使用 Whole‑Document Extraction 确认文件中是否有评论,再根据需要切换到 Per‑Page Extraction,以便将反馈定位到具体章节。
Best Practices and Tips
- 及时释放
Parser:使用using块包装,以释放本地资源。 - 区分
null与空集合:GetAnnotations返回null表示该格式不支持批注;返回空集合则表示文档没有评论。 - 在批量作业中检查
Features.Annotations:尽早跳过不支持的文件,而不是在循环内部进行null检查。 - 复用带页码的列表:一次性生成
ExtractAnnotationsByPage,然后让 CSV 与 JSON 导出共用同一数据,避免两种输出不一致。 - 安全性:批注文本是自由形式的审阅者输入,在渲染到 UI 或报告前应视为不可信字符串并进行适当处理。
Conclusion
GroupDocs.Parser 为你提供了一种直接、可编程的方式,从 PDF 中提取审阅者的评论,而无需手动逐页查找。通过整篇文档提取、按页标记或与普通文本流合并,你可以构建出在文档进入流水线的瞬间即显示反馈的审阅工作流。将结果导出为 CSV 或 JSON,便可直接对接团队已有的工具。
Next steps:
- 探索 GetAnnotations API 参考,了解完整的方法签名和重载。
- 学习如何在提取批注的同时 从 PDF 文档中提取文本,构建完整的内容流水线。
- 查看 GitHub 上的其他示例项目,了解批量处理场景(示例仓库)。