💡 مثال کامل قابل اجرا در گیت‌هاب موجود است:
extract-annotations-from-pdf-using-groupdocs-parser-dotnet

مقدمه

یک PDF که تحت بازبینی قرار گرفته است معمولاً بیش از متن قابل مشاهده خود دارد – یادداشت‌های چسبان، نکات برجسته و نظرات درون‌متنی که توسط بازبین‌ها گذاشته شده‌اند. پیمایش هر صفحه برای یافتن آن‌ها وقتی یک سند چندین بار بازبینی شده باشد، مقیاس‌پذیر نیست. GroupDocs.Parser یک کتابخانه .NET است که حاشیه‌نویسی‌های توکار یک سند را به‌صورت برنامه‌نویسی می‌خواند و نظرات پراکنده بازبین‌ها را به داده‌های ساختاریافته‌ای تبدیل می‌کند که کد شما می‌تواند بر روی آن‌ها عمل کند. این آموزش نشان می‌دهد چگونه حاشیه‌نویسی‌ها را از یک PDF کامل استخراج کنیم، صفحه به صفحه تجزیه کنیم، آن‌ها را همراه با متن سند بیرون بکشیم و نتایج را به CSV یا JSON صادر کنیم.

من این مشکل را هنگام ساخت یک ردیاب بازبینی برای تیم مستندسازی مواجه شدم: یک یادداشت انتشار 40‑صفحه‌ای از سه بازبین عبور کرده بود و باز کردن دستی فایل برای یافتن هر نظر، طولانی‌تر از رفع خود مشکلاتی بود که آن‌ها علامت‌گذاری کرده بودند. استخراج حاشیه‌نویسی‌ها در چند خط کد، این کار را به دو دقیقه تبدیل کرد.

در بخش‌های زیر خواهید آموخت چگونه:

  • تمام حاشیه‌نویسی‌ها را از یک PDF در یک عبور استخراج کنید.
  • هر حاشیه‌نویسی را با صفحه‌ای که به آن تعلق دارد برچسب‌گذاری کنید.
  • متن سند و متن حاشیه‌نویسی را در یک خواندن واحد ترکیب کنید.
  • نتایج را به CSV یا JSON برای ابزارهای بعدی سریال‌سازی کنید.

چرا استخراج حاشیه‌نویسی‌های PDF مهم است

خواندن حاشیه‌نویسی‌های PDF به‌صورت برنامه‌نویسی برای موارد زیر مفید است:

  • گردش‌های بازبینی: جمع‌آوری هر نظر بازبین بدون باز کردن فایل در یک نمایشگر PDF.
  • همکاری: نمایش بخش‌های برجسته یا یادداشت‌دار به‌صورت مستقیم در ابزارهای خودتان.
  • حسابرسی: نگهداری رکوردی از علامت‌گذاری‌های باقی‌مانده بر روی یک سند در طول زمان، حتی پس از صاف‌سازی یا نهایی‌سازی آن.

GroupDocs.Parser استخراج بومی حاشیه‌نویسی برای اسناد PDF را در نسخه 26.7 از طریق متد GetAnnotations اضافه کرد، به‌همراه گزینه جدید IncludeAnnotations در TextOptions برای کشیدن متن حاشیه‌نویسی‌ها به یک خواندن متن معمولی.

پیش‌نیازها

  • .NET 6.0 یا بالاتر
  • GroupDocs.Parser برای .NET 26.7+ (مجوز موقت)
  • یک فایل PDF با حاشیه‌نویسی‌های موجود (مثلاً document-with-annotations.pdf)

نصب از طریق NuGet:

dotnet add package GroupDocs.Parser

چگونه حاشیه‌نویسی‌ها را از یک سند PDF استخراج کنم؟

پاسخ: فایل را با Parser بارگذاری کنید، سپس GetAnnotations() را برای کل سند یا GetAnnotations(pageIndex) برای یک صفحهٔ واحد فراخوانی کنید. هر نتیجه مجموعه‌ای از اشیای AnnotationItem است که ویژگی Value متن نظر را در خود دارد. اگر ترجیح می‌دهید نظرات را به‌صورت درون‌متنی همراه با محتوای معمولی سند ببینید، IncludeAnnotations را در TextOptions تنظیم کنید و به‌جای آن 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 در حال حاضر گزارش می‌کند.
  • هیچ انتساب صفحه‌ای در اینجا گنجانده نشده؛ اگر به آن نیاز دارید، از overload صفحه‌ای زیر استفاده کنید.

استخراج صفحه‑به‑صفحه

وقتی مکان یک نظر مهم است، بر روی صفحات سند حلقه بزنید و برای هر یک 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 به‌صورت Graceful با برگرداندن 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 } است – برای هر سرویس downstreamی آسان برای دی‌سریالایز کردن.
  • 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 صادر کنید و مستقیماً به ابزارهایی که تیم‌تان قبلاً استفاده می‌کند، متصل کنید.

گام‌های بعدی:

  • مرجع API GetAnnotations را برای امضای کامل متد و overloadها بررسی کنید.
  • نحوهٔ استخراج متن از اسناد PDF را همراه با حاشیه‌نویسی‌ها برای یک خط لولهٔ محتوای کامل بیاموزید.
  • پروژه‌های نمونهٔ بیشتر در گیت‌هاب برای سناریوهای پردازش دسته‌ای را ببینید (Examples Repo).

منابع تکمیلی