💡 完整可运行示例位于 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:

  • IncludeAnnotationsTextOptions 的属性,因此可以在已有的 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.AnnotationsParser 实例上的一个布尔标记。
  • 预先检查可以让意图更加明确,尽管 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:

Additional Resources