💡 完整可运行示例已在 GitHub 上提供: extract-annotations-from-pdf-using-groupdocs-parser-dotnet

介绍

经过审阅的 PDF 往往不仅仅包含可见的文字——还有评审者留下的便利贴、突出显示的批注以及内联评论。要在每一页中滚动查找这些内容,在文档经历多轮反馈后几乎不可能实现。GroupDocs.Parser 是一个 .NET 库,可以编程方式读取文档中嵌入的注释,将分散的评审者评论转换为结构化数据,供代码使用。 本教程展示如何从整个 PDF 中提取注释、按页拆分、将它们与文档文本一起提取,并将结果导出为 CSV 或 JSON。

我在为文档团队构建审阅跟踪器时遇到了这个问题:一份 40 页的发行说明经过了三位评审,手动打开文件查找每条评论的时间比实际修复他们标记的问题还要长。用几行代码提取注释后,这项工作只需两分钟即可完成。

在接下来的章节中,你将学习如何:

  • 一次性提取 PDF 中的所有注释。
  • 为每条注释标记所属页码。
  • 将文档文本与注释文本一起读取。
  • 将结果序列化为 CSV 或 JSON,以供下游工具使用。

为什么提取 PDF 注释很重要

以编程方式读取 PDF 注释在以下场景中非常有用:

  • 审阅工作流:无需在 PDF 查看器中打开文件,即可收集所有评审者的评论。
  • 协作:直接在自己的工具中展示高亮或标注的内容。
  • 审计:即使文档已被扁平化或定稿,也能保留对文档所做标记的记录。

GroupDocs.Parser 在 26.7 版本中通过 GetAnnotations 方法为 PDF 文档添加了原生注释提取功能,并在 TextOptions 上新增了 IncludeAnnotations 选项,以便在普通文本读取时一起获取注释文本。

前提条件

  • .NET 6.0 或更高版本
  • GroupDocs.Parser for .NET 26.7+(临时许可证
  • 一个已包含注释的 PDF 文件(例如 document-with-annotations.pdf

通过 NuGet 安装:

dotnet add package GroupDocs.Parser

如何从 PDF 文档中提取注释?

答案: 使用 Parser 加载文件,然后调用 GetAnnotations() 获取整篇文档的注释,或调用 GetAnnotations(pageIndex) 获取单页注释。每个结果都是 AnnotationItem 对象的集合,其 Value 属性保存评论文本。如果希望将评论与文档的常规内容内联显示,只需在 TextOptions 上设置 IncludeAnnotations 并调用 GetText

整篇文档提取

以下代码片段一次性提取文件中的所有注释,是检查文档是否存在任何未处理评论的最快方式。

// 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;

关键点:

  • 当文档格式不支持注释提取时,GetAnnotations() 返回 null;当文档根本没有注释时返回空集合。
  • 每个 AnnotationItem 通过 Value 属性公开其文本——这是 SDK 当前唯一提供的数据点。
  • 此处不包含页码信息;如果需要,请使用下面的按页重载。

按页提取

当评论所在位置很重要时,遍历文档的每一页并对每页调用 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;

关键点:

  • 循环依据 GetDocumentInfo().PageCount;没有单独的“注释页数”概念。
  • GetAnnotations(pageIndex) 使用零基索引,与 API 中的其他页级方法保持一致。
  • 生成的 AnnotationRecord 列表正好符合 CSV 或 JSON 导出的结构需求。

将文本与注释一起提取

无需对文档进行两次遍历,你可以直接将注释文本折叠进普通文本提取的输出中。

// 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;
    }
}

关键点:

  • IncludeAnnotationsTextOptions 的属性,因此可以在已有的 GetText 调用中直接使用,无需额外代码。
  • 当你希望得到一个单一的转录式输出,而不是单独的评论列表时,这种方式非常有用。
  • 若只需对单页进行此操作,可配合 GetText(pageIndex, options) 使用。

首先检查注释支持

并非所有格式都支持注释,因此在编写基于 GetAnnotations 的逻辑前最好先检查。

// Returns true if the loaded document format supports annotation extraction
using (var parser = new Parser(path))
{
    return parser.Features.Annotations;
}

关键点:

  • Features.AnnotationsParser 实例上的一个布尔标志。
  • 预先检查可以让意图更明确,尽管 GetAnnotations 已经会通过返回 null 的方式优雅地失败。

将注释导出为 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());

关键点:

  • CsvEscape 会安全地为包含逗号、引号或换行符的字段加引号。
  • 生成的文件可直接在 Excel 中打开,或导入到工单系统中。

Helper: CsvEscape

if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
    return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;

将注释导出为 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());

关键点:

  • 输出是一个平坦的 { page, value } 对象数组,任何下游服务都能轻松反序列化。
  • Escape 在不引入序列化库的情况下保持有效的 JSON 负载。

Helper: Escape

return s?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? string.Empty;

方法比较:何时使用每种方法

方法 适用场景 关键优势 限制
整篇文档提取 快速检查是否存在任何评论 单次调用,代码最简 不包含页码信息
按页提取 将反馈路由到对应章节 带页码的结果,便于导出 每页多一次调用
将文本与注释一起提取 获取单一可读的转录本 无需二次遍历文档 评论未与正文分离
CSV 导出 基于电子表格的审阅跟踪 易于在 Excel 中打开,可读性强 结构平坦
JSON 导出 自动化流水线、工单系统 结构化、机器可读 负载略大

先使用整篇文档提取确认文件是否有值得处理的评论,然后在需要将反馈定位到具体章节时切换为按页提取。

最佳实践和提示

  • 及时释放 Parser:使用 using 块包装,以释放本机资源。
  • 区分 null 与空集合GetAnnotations 返回 null 表示不支持该格式;返回空集合表示文档没有评论。
  • 在批处理作业中检查 Features.Annotations:尽早跳过不支持的文件,而不是在循环内部深处进行 null 检查。
  • 复用带页码的列表:一次性使用 ExtractAnnotationsByPage 构建列表,然后让 CSV 与 JSON 导出共用同一数据,避免两种输出不一致。
  • 安全性:注释文本是自由形式的评审者输入——在 UI 或报告中渲染前应像处理其他不可信字符串一样进行处理。

结论

GroupDocs.Parser 为你提供了一种直接、可编程的方式,从 PDF 中提取评审者评论,而无需手动逐页查找。通过整篇文档提取、按页标记或将其折叠进普通文本流,你可以构建出在文档进入流水线的瞬间就能展示反馈的审阅工作流。将结果导出为 CSV 或 JSON,直接接入团队已有的工具。

后续步骤:

其他资源