💡 完整的工作範例可在 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;
}
}
重點說明:
IncludeAnnotations是TextOptions的屬性,因此此方式可與您已使用的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.Annotations是Parser實例上的簡單布林旗標。- 提前檢查可使意圖更明確,即使
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,並直接串接至團隊已在使用的工具。
下一步:
- 瀏覽 GetAnnotations API 參考,了解完整的方法簽名與重載。
- 學習如何 從 PDF 文件提取文字,同時取得註釋,以建立完整的內容管道。
- 在 GitHub 上查看其他範例專案,以了解批次處理情境(Examples Repo)。