💡 完整的工作範例可在 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+(temporary license
  • 具備現有註釋的 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 中開啟,或匯入至工單系統。

輔助函式: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 負載。

輔助函式: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,並直接串接至團隊已在使用的工具。

下一步:

其他資源