IRONSOFTWAREHOME

C#中的批次PDF處理:自動化文件工作流程

Ahmad Sohail
Ahmad Sohail
Updated: 2026年6月29日

IronPDF中的批次PDF處理使.NET開發人員能夠在C#中實現大規模自動化文件工作流程,從並行HTML到PDF轉換和批量合併/拆分到具有內建錯誤處理、重試邏輯和檢查點的異步PDF管道。 IronPDF的執行緒安全Chromium引擎和基於IDisposable的記憶體管理使其專為高吞吐量PDF自動化而設計,無論您是在本地運行,還是在Azure FunctionsAWS LambdaKubernetes上運行。

精讀:快速入門指南

本教程涵蓋了C#中的可擴展PDF自動化,從並行轉換和批量操作到雲部署和彈性管道模式。

  • **適合物件:**負責文件密集型工作流程的.NET開發者和架構師——文件遷移項目、每日報告生成管道、合規補救掃描或在連續處理不可行時進行歸檔數字化努力。
  • **您將構建的內容:**使用SemaphoreSlim的異步管道進行並發控制,錯誤處理具有失敗跳過和重試邏輯,使用檢查點/恢復模式進行崩潰恢復,雲部署配置適用於Azure Functions、AWS Lambda和Kubernetes。
  • **運行環境:**支持.NET 6+、.NET Framework 4.6.2+、.NET Standard 2.0。所有渲染都使用IronPDF的嵌入式Chromium引擎——不需要獨立瀏覽器依賴或外部服務。
  • **何時使用此方法:**當您需要處理的PDF數量超過連續執行所允許的量時——大規模文件遷移、時間緊湊的批量作業,或多租戶平台具有可變文件負載時。
  • **技術重要性:**IronPDF的ChromePdfRenderer是執行緒安全且每渲染無狀態,意味著多個執行緒可以安全地共享單個渲染器實例。 結合.NET的任務並行庫和PdfDocument上,您可以獲得可預測的記憶體行為和CPU飽和度,而不會遇到競爭條件或記憶體洩漏。

只需幾行程式碼,即可將整個HTML文件目錄批量轉換為PDF:

  1. 1Install IronPDF with NuGet Package Manager

    PM > Install-Package IronPdf

  2. 2複製並運行這段程式碼片段。

    using IronPdf;
    using System.IO;
    using System.Threading.Tasks;
    
    var renderer = new ChromePdfRenderer();
    var htmlFiles = Directory.GetFiles("input/", "*.html");
    
    Parallel.ForEach(htmlFiles, htmlFile =>
    {
        var pdf = renderer.RenderHtmlFileAsPdf(htmlFile);
        pdf.SaveAs($"output/{Path.GetFileNameWithoutExtension(htmlFile)}.pdf");
    });
    C#
  3. 3部署以在您的實時環境中測試

    今天就開始在您的專案中使用IronPDF,透過免費試用
    arrow pointer

在您購買或註冊30天試用版IronPDF後,在應用程式的開始處新增您的授權金鑰。

IronPdf.License.LicenseKey = "KEY";

Start using IronPDF in your project today with a free trial.

第一步:
arrow pointer
NuGet使用NuGet安裝

PM > Install-Package IronPdf

Install IronPDF by running the command above in the NuGet Package Manager Console, or search for the package in the NuGet Package Manager.
目錄

當您有數以千計的PDF需要處理時

批量PDF處理並不是一個小眾需求——這是企業文件管理的重要組成部分。 召喚它的場景遍布各行各業,它們具有一個共同特徵:一件一件地逐個執行並不是一個選項。

文件遷移項目是最常見的觸發因素之一。 當一個組織從一個文件管理系統遷移到另一個時,數以千計(有時數以百萬計)的文件需要被轉換、重新格式化或重新標記。 一家保險公司從舊有的索賠系統遷移可能需要將500,000個基於TIFF的索賠文件轉換為可搜索的PDF。 一家律所轉移到新的案件管理平台可能需要將分散的通信合併到統一的案件文件中。 這些是一次性的工作,但它們的範圍極大,對錯誤的容忍度極低。

每日報告生成是同一問題的穩態版本。 金融機構滿足數以千計客戶的日終投資組合報告,物流公司為每個出境集裝箱生成運輸清單,醫療保健系統在數百個部門中建立每日患者摘要——所有這些都產生了PDF輸出,其規模大到連續處理會超過可接受的時間窗口。 當10,000份報告需要在早上6點前準備好,而資料在午夜之前還沒有最後確定時,您就不能將它們一個接一個地渲染。

檔案數字化處於遷移和合規性的交叉點。 具有數十年紙質記錄的政府機構、大學和公司面臨著以標準合規格式(通常為PDF/A)數字化和存檔文件的命令。 數量驚人——僅NARA就收到數百萬頁的聯邦記錄以供長期保存——而這一過程需要足夠可靠,以免多年以後發現缺口。

合規補救通常是最緊迫的觸發因素。 當審計發現您的文件存檔不符合新實施的標準時——例如,您的儲存發票不符合PDF/A-3的電子發票規定,或者您的醫療記錄缺乏第508條要求的無障礙標記——您需要根據新標準處理整個現有存檔。 壓力很大,時間緊迫,而容量是您的存檔恰好包含的任何數量。

在每一種情景中,核心挑戰都是相同的:如何可靠地、有效地處理大量PDF操作,而不會耗盡記憶體或在問題出現時留下未完成的工作?

 資訊圖顯示四種批次處理情景——文件遷移、每日報告生成、檔案數字化、合規補救——每個都有一個圖示、典型的容量範圍和時間壓力指標


IronPDF批次處理架構

在深入了解具體操作之前,了解IronPDF如何設計以處理並行工作負載以及在其上構建批量管道時應做出哪些架構決定是很重要的。

安裝IronPDF

通過NuGet安裝IronPDF:

PM > Install-Package IronPdf

或使用.NET CLI:

dotnet add package IronPdf

IronPDF支持.NET Framework 4.6.2+、.NET Core、.NET 5至.NET 10和.NET Standard 2.0。它在Windows、Linux、macOS和Docker容器上運行,使其適合於本地批次作業和雲原生部署。

對於生產批次處理,應在任何PDF操作開始前在應用程式啟動時使用License.LicenseKey設置您的授權金鑰。 這確保所有執行緒中的每個渲染調用均可存取全部功能集,無需單個文件水印。


並發控制和執行緒安全性

IronPDF的基於Chromium的渲染引擎是執行緒安全的。 您可以在不同的執行緒中建立多個ChromePdfRenderer實例,也可以共享單個實例——IronPDF處理內部同步。 官方推薦的批次處理方法是使用.NET的內建Parallel.ForEach,它會自動將工作分配給所有可用的CPU核心。

話雖如此,"執行緒安全"不等於"使用無限執行緒"。 每個並行的PDF渲染操作都會消耗記憶體(Chromium引擎需要工作空間進行DOM解析、CSS佈局和圖像光柵化),而在記憶體受限系統上啟動太多並行操作會降低性能或導致OutOfMemoryException。 合適的並發級別取決於您的硬體:一台16核64 GB RAM的伺服器可以輕鬆處理8-12個並發渲染; 一台4核8 GB的虛擬機可能被限制為2-4個。使用ParallelOptions.MaxDegreeOfParallelism 控制並發性 - 將其設定為大約您可用的CPU核心的一半作為起點,然後根據觀察到的記憶體壓力進行調整。

大規模記憶體管理

記憶體管理是批次PDF處理中最重要的問題。 每個PdfDocument 物件在記憶體中儲存完整的二進制PDF內容,如果不清除這些物件,記憶體會隨著處理的文件數量線性增長。

關鍵規則:始終使用using 語句或明確調用Dispose()PdfDocument 物件上。 IronPDF的PdfDocument 實現了IDisposable,不進行清理是批次場景中記憶體問題的最常見原因。 您處理迴圈的每次迭代應建立一個PdfDocument,執行其工作並清理 - 除非有具體原因且有足夠的記憶體來處理,否則不要在列表或集合中累積PdfDocument 物件。

除清理外,考慮以下大批次的記憶體管理策略:

分塊處理 而不是一次載入所有。 如果需要處理50,000個文件,請不要將它們全部列舉進列表中並逐一迭代 - 以100或500個為一批進行處理,允許垃圾收集器在各批次間回收記憶體。

為極大的批次在批次之間進行垃圾回收。 儘管通常應該讓GC自行運行,但批次處理是少數場景之一,即在批次邊界之間調用GC.Collect() 可以防止記憶體壓力累積。

使用GC.GetTotalMemory() 或進程級別的度量監控記憶體消耗。 如果記憶體使用超過門檻(例如可用RAM的80%),暫停處理以讓GC趕上。


進度報告和日誌記錄

當一個批次作業需要耗時數小時完成時,對其進度的可視性不是可選的——而是必需的。 至少,您應該記錄每個文件的開始和完成,跟踪成功/失敗計數,並提供估計的剩餘時間。 在並行操作時使用Interlocked.Increment 用於執行緒安全計數器,並且以規則間隔(每50或100個文件)記錄,避免在每一個文件上記錄以免淹沒您的輸出。 使用System.Diagnostics.Stopwatch 來跟踪您經過的時間,並計算一個運行中文件/秒率來提供有意義的ETA。

對於生產批次作業,考慮將進度寫入持久性儲存(資料庫、文件或消息隊列),這樣監控儀表盤可以顯示實時狀態,而不需要直接連接到批次過程。


常見的批次操作

在設置架構後,讓我們走過最常見的批次操作及其IronPDF實現。

批量HTML到PDF轉換

HTML到PDF轉換是最常見的批次操作。 無論您是從模板生成發票、將HTML文件庫轉換為PDF,還是從網頁應用程式渲染動態報告,模式都是相同的:遍歷輸入,渲染每個輸入,並保存輸出。

輸入(5個HTML文件)

HTML Invoice INV-2026-001

INV-2026-001

HTML Invoice INV-2026-002

INV-2026-002

HTML Invoice INV-2026-003

INV-2026-003

HTML Invoice INV-2026-004

INV-2026-004

HTML Invoice INV-2026-005

INV-2026-005

實現使用ChromePdfRendererParallel.ForEach 並行處理所有HTML文件,通過MaxDegreeOfParallelism 控制並行性,以平衡吞吐量與記憶體消耗。 每個文件都用RenderHtmlFileAsPdf 渲染並保存到輸出目錄,通過執行緒安全的Interlocked 計數器進行進度跟踪。

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;

// Configure paths
string inputFolder = "input/";
string outputFolder = "output/";

Directory.CreateDirectory(outputFolder);

string[] htmlFiles = Directory.GetFiles(inputFolder, "*.html");
Console.WriteLine($"Found {htmlFiles.Length} HTML files to convert");

// Create renderer instance (thread-safe, can be shared)
var renderer = new ChromePdfRenderer();

// Track progress
int processed = 0;
int failed = 0;

// Process in parallel with controlled concurrency
var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(htmlFiles, options, htmlFile =>
{
    try
    {
        string fileName = Path.GetFileNameWithoutExtension(htmlFile);
        string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf");

        using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile);
        pdf.SaveAs(outputPath);

        Interlocked.Increment(ref processed);
        Console.WriteLine($"[OK] {fileName}.pdf");
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref failed);
        Console.WriteLine($"[ERROR] {Path.GetFileName(htmlFile)}: {ex.Message}");
    }
});

Console.WriteLine($"\nComplete: {processed} succeeded, {failed} failed");

輸出


每個HTML發票渲染為對應的PDF。 上圖顯示INV-2026-001.pdf——批次輸出中的其中一個。

對於基於模板的生成(例如發票、報告),通常會在渲染之前將資料合併到HTML模板中。 方法很簡單:載入您的HTML模板一次,使用string.Replace 注入每個記錄資料(客戶名、總額、日期),然後將填充的HTML傳遞給RenderHtmlAsPdf 在您的並行迴圈中。 IronPDF還提供RenderHtmlAsPdfAsync 用於場景,您想使用異步/等待而不是Parallel.ForEach——在後面的部分中,我們將詳細介紹異步模式。


批量PDF合併

將組的PDF合併到合併文件中在法律(合併案件文件文件)、金融(將月結單合併到季度報告中)和出版工作流中很常見。

using IronPdf;
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;

string inputFolder = "documents/";
string outputFolder = "merged/";

Directory.CreateDirectory(outputFolder);

// Group PDFs by prefix (e.g., "invoice-2026-01-*.pdf" -> one merged file)
var pdfFiles = Directory.GetFiles(inputFolder, "*.pdf");
var groups = pdfFiles
    .GroupBy(f => Path.GetFileName(f).Split('-').Take(3).Aggregate((a, b) => $"{a}-{b}"))
    .Where(g => g.Count() > 1);

Console.WriteLine($"Found {groups.Count()} groups to merge");

foreach (var group in groups)
{
    string groupName = group.Key;
    var filesToMerge = group.OrderBy(f => f).ToList();

    Console.WriteLine($"Merging {filesToMerge.Count} files into {groupName}.pdf");

    try
    {
        // Load all PDFs for this group
        var pdfDocs = new List<PdfDocument>();
        foreach (string filePath in filesToMerge)
        {
            pdfDocs.Add(PdfDocument.FromFile(filePath));
        }

        // Merge all documents
        using var merged = PdfDocument.Merge(pdfDocs);
        merged.SaveAs(Path.Combine(outputFolder, $"{groupName}-merged.pdf"));

        // Dispose source documents
        foreach (var doc in pdfDocs)
        {
            doc.Dispose();
        }

        Console.WriteLine($"  [OK] Created {groupName}-merged.pdf ({merged.PageCount} pages)");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"  [ERROR] {groupName}: {ex.Message}");
    }
}

Console.WriteLine("\nMerge complete");

如果要合併大量文件,要注意記憶體:PdfDocument.Merge 方法同時將所有源文件載入到記憶體中。 如果您要合併數百個大型PDF,考慮分階段合併——將10-20個文件組合到中間文件中,然後合併中間文件。

批量PDF拆分

將多頁PDF拆分為單獨頁面(或頁面範圍)是合併的反面。 在郵件室處理中很常見,在哪裡需要將掃描的文件批次分割成單獨的記錄,在印刷工作流中合成的文件需要分離。

輸入

下列程式碼示範使用CopyPage 在並行迴圈中提取單獨頁面,為每頁建立單獨的PDF文件。 另一個SplitByRange 輔助函式顯示如何提取頁面範圍而不是單個頁面,這對於將大文件分割成較小段落很有用。

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;

string inputFolder = "multipage/";
string outputFolder = "split/";

Directory.CreateDirectory(outputFolder);

string[] pdfFiles = Directory.GetFiles(inputFolder, "*.pdf");
Console.WriteLine($"Found {pdfFiles.Length} PDFs to split");

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(pdfFiles, options, pdfFile =>
{
    string baseName = Path.GetFileNameWithoutExtension(pdfFile);

    try
    {
        using var pdf = PdfDocument.FromFile(pdfFile);
        int pageCount = pdf.PageCount;

        Console.WriteLine($"Splitting {baseName}.pdf ({pageCount} pages)");

        // Extract each page as a separate PDF
        for (int i = 0; i < pageCount; i++)
        {
            using var singlePage = pdf.CopyPage(i);
            string outputPath = Path.Combine(outputFolder, $"{baseName}-page-{i + 1:D3}.pdf");
            singlePage.SaveAs(outputPath);
        }

        Console.WriteLine($"  [OK] Created {pageCount} files from {baseName}.pdf");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"  [ERROR] {baseName}: {ex.Message}");
    }
});

// Alternative: Extract page ranges instead of individual pages
void SplitByRange(string inputFile, string outputFolder, int pagesPerChunk)
{
    using var pdf = PdfDocument.FromFile(inputFile);
    string baseName = Path.GetFileNameWithoutExtension(inputFile);
    int totalPages = pdf.PageCount;
    int chunkNumber = 1;

    for (int startPage = 0; startPage < totalPages; startPage += pagesPerChunk)
    {
        int endPage = Math.Min(startPage + pagesPerChunk - 1, totalPages - 1);
        using var chunk = pdf.CopyPages(startPage, endPage);
        chunk.SaveAs(Path.Combine(outputFolder, $"{baseName}-chunk-{chunkNumber:D3}.pdf"));
        chunkNumber++;
    }
}

Console.WriteLine("\nSplit complete");

輸出


第2頁被提取為獨立的PDF(年度報告頁2.pdf)

IronPDF的CopyPageCopyPages 方法建立新PdfDocument 將指定頁面包含的物件。 記住保存之後,才處理原始文件和每個提取的頁面文件。


批次壓縮

當儲存成本重要或需要通過帶寬有限的連接傳輸PDF時,批量壓縮可以大大減少您的檔案儲存量。 IronPDF提供兩種壓縮方法:CompressImages 用於降低圖片質量/大小,CompressStructTree 用於去除結構化元資料。 較新版本的CompressAndSaveAs API(版本2025.12中推出)通過結合多個優化技術提供更優的壓縮效果。

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;

string inputFolder = "originals/";
string outputFolder = "compressed/";

Directory.CreateDirectory(outputFolder);

string[] pdfFiles = Directory.GetFiles(inputFolder, "*.pdf");
Console.WriteLine($"Found {pdfFiles.Length} PDFs to compress");

long totalOriginalSize = 0;
long totalCompressedSize = 0;
int processed = 0;

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(pdfFiles, options, pdfFile =>
{
    string fileName = Path.GetFileName(pdfFile);
    string outputPath = Path.Combine(outputFolder, fileName);

    try
    {
        long originalSize = new FileInfo(pdfFile).Length;
        Interlocked.Add(ref totalOriginalSize, originalSize);

        using var pdf = PdfDocument.FromFile(pdfFile);

        // Apply compression with JPEG quality setting (0-100, lower = more compression)
        pdf.CompressAndSaveAs(outputPath, 60);

        long compressedSize = new FileInfo(outputPath).Length;
        Interlocked.Add(ref totalCompressedSize, compressedSize);
        Interlocked.Increment(ref processed);

        double reduction = (1 - (double)compressedSize / originalSize) * 100;
        Console.WriteLine($"[OK] {fileName}: {originalSize / 1024}KB → {compressedSize / 1024}KB ({reduction:F1}% reduction)");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
    }
});

double totalReduction = (1 - (double)totalCompressedSize / totalOriginalSize) * 100;
Console.WriteLine($"\nCompression complete:");
Console.WriteLine($"  Files processed: {processed}");
Console.WriteLine($"  Total original: {totalOriginalSize / 1024 / 1024}MB");
Console.WriteLine($"  Total compressed: {totalCompressedSize / 1024 / 1024}MB");
Console.WriteLine($"  Overall reduction: {totalReduction:F1}%");

關於壓縮需注意幾個事項:JPEG解析度低於60的設置在大多數圖片中會產生可見的人造失真。 ShrinkImage 選項在某些配置下可能會造成失真——在運行完整批次之前先用代表樣本測試。 而刪除結構樹(CompressStructTree)會影響壓縮PDF中的文字選擇和搜索功能,因此只有在不需要這些功能時才使用它。


批次格式轉換(PDF/A,PDF/UA)

將現有檔案庫轉換為標準合規格式——長期存檔的PDF/A或無障礙性的PDF/UA——是最高價值的批次操作之一。 IronPDF支持完整範圍的PDF/A版本(包括在版本2025.11中增加的PDF/A-4)和PDF/UA合規性(包括在版本2025.12中增加的PDF/UA-2)。

輸入

範例載入每個PDF以SaveAsPdfAPdfAVersions.PdfA3b 參數將其轉換為PDF/A-3b。 另一個ConvertToPdfUA 函式展示使用SaveAsPdfUA 的無障礙合規性轉換,儘管PDF/UA需要的源文件有正確的結構標記。

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;

string inputFolder = "originals/";
string outputFolder = "pdfa-archive/";

Directory.CreateDirectory(outputFolder);

string[] pdfFiles = Directory.GetFiles(inputFolder, "*.pdf");
Console.WriteLine($"Found {pdfFiles.Length} PDFs to convert to PDF/A-3b");

int converted = 0;
int failed = 0;

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(pdfFiles, options, pdfFile =>
{
    string fileName = Path.GetFileName(pdfFile);
    string outputPath = Path.Combine(outputFolder, fileName);

    try
    {
        using var pdf = PdfDocument.FromFile(pdfFile);

        // Convert to PDF/A-3b for long-term archival
        pdf.SaveAsPdfA(outputPath, PdfAVersions.PdfA3b);

        Interlocked.Increment(ref converted);
        Console.WriteLine($"[OK] {fileName} → PDF/A-3b");
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref failed);
        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
    }
});

Console.WriteLine($"\nConversion complete: {converted} succeeded, {failed} failed");

// Alternative: Convert to PDF/UA for accessibility compliance
void ConvertToPdfUA(string inputFolder, string outputFolder)
{
    Directory.CreateDirectory(outputFolder);
    string[] files = Directory.GetFiles(inputFolder, "*.pdf");

    Parallel.ForEach(files, pdfFile =>
    {
        string fileName = Path.GetFileName(pdfFile);
        try
        {
            using var pdf = PdfDocument.FromFile(pdfFile);

            // PDF/UA requires proper tagging - ensure source is well-structured
            pdf.SaveAsPdfUA(Path.Combine(outputFolder, fileName));
            Console.WriteLine($"[OK] {fileName} → PDF/UA");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
        }
    });
}

輸出

 PDF/A轉換輸出比較顯示合規性元資料

輸出PDF在外觀上是逐字節相同的,但現在攜帶PDF/A-3b合規性元資料,適用於檔案系統。

格式轉換對於合規補救項目特別重要,其中組織發現其現有檔案庫不符合監管標準。 批量模式是直接的,但驗證步驟至關重要——始終確保每個轉換文件真正通過合規性檢查後才算完成。 我們下面的彈性部分詳細介紹驗證。


建立具有彈性的批次管道

一個在100個文件上運行完美但在第50,000個文件中崩潰的管道是沒有用的。 彈性——即能夠優雅地處理錯誤,重試暫時故障並在崩潰後恢復的能力——是將生產級管道與原型區分開來的因素。

錯誤處理和失敗跳過

最基本的彈性模式是失敗跳過:如果單個文件處理失敗,記錄錯誤並繼續處理下一個文件,而不是中止整個批次。 這聽起來顯而易見,但當您使用AggregateException 傳播並終止迴圈。

以下範例演示失敗跳過和重試邏輯——將每個文件包裝在try-catch中以進行優雅的錯誤處理,並使用指數增長回退的內部重試迴圈來處理像OutOfMemoryException這樣的暫時性異常:

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;

string inputFolder = "input/";
string outputFolder = "output/";
string errorLogPath = "error-log.txt";

Directory.CreateDirectory(outputFolder);

string[] htmlFiles = Directory.GetFiles(inputFolder, "*.html");
var renderer = new ChromePdfRenderer();
var errorLog = new ConcurrentBag<string>();

int processed = 0;
int failed = 0;
int retried = 0;

const int maxRetries = 3;

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(htmlFiles, options, htmlFile =>
{
    string fileName = Path.GetFileNameWithoutExtension(htmlFile);
    string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf");
    int attempt = 0;
    bool success = false;

    while (attempt < maxRetries && !success)
    {
        attempt++;
        try
        {
            using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile);
            pdf.SaveAs(outputPath);
            success = true;
            Interlocked.Increment(ref processed);

            if (attempt > 1)
            {
                Interlocked.Increment(ref retried);
                Console.WriteLine($"[OK] {fileName}.pdf (succeeded on attempt {attempt})");
            }
            else
            {
                Console.WriteLine($"[OK] {fileName}.pdf");
            }
        }
        catch (Exception ex) when (IsTransientException(ex) && attempt < maxRetries)
        {
            // Transient error - wait and retry with exponential backoff
            int delayMs = (int)Math.Pow(2, attempt) * 500;
            Console.WriteLine($"[RETRY] {fileName}: {ex.Message} (attempt {attempt}, waiting {delayMs}ms)");
            Thread.Sleep(delayMs);
        }
        catch (Exception ex)
        {
            // Non-transient error or max retries exceeded
            Interlocked.Increment(ref failed);
            string errorMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {fileName} | {ex.GetType().Name} | {ex.Message}";
            errorLog.Add(errorMessage);
            Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
        }
    }
});

// Write error log
if (errorLog.Count > 0)
{
    File.WriteAllLines(errorLogPath, errorLog);
}

Console.WriteLine($"\nBatch complete:");
Console.WriteLine($"  Processed: {processed}");
Console.WriteLine($"  Failed: {failed}");
Console.WriteLine($"  Retried: {retried}");
if (failed > 0)
{
    Console.WriteLine($"  Error log: {errorLogPath}");
}

// Helper to identify transient exceptions worth retrying
bool IsTransientException(Exception ex)
{
    return ex is IOException ||
           ex is OutOfMemoryException ||
           ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) ||
           ex.Message.Contains("locked", StringComparison.OrdinalIgnoreCase);
}

批次完成後,查看錯誤日誌以了解哪個文件失敗以及原因。 常見的失敗原因包括損壞的源文件、密碼保護的PDF、源內容不支持的功能和在非常大文件上的記憶體不足情況。

對瞬態故障重試邏輯

一些故障是暫時的——如果您再試一次便會成功。 這些包括文件系統爭用(另一個進程已鎖定文件)、暫時記憶體壓力(GC尚未跟上)和載入HTML內容中的外部資源時的網路超時。 上面的程式碼範例使用指數回退處理這些問題——從短暫延遲開始,每次重試嘗試時加倍,並在最大重試次數上限(通常為3)停止。

關鍵在於區分可重試和不可重試的故障。 一個OutOfMemoryException(暫時性壓力)值得重試。 一個ArgumentException(無效輸入)或持續渲染錯誤則不值得重試——重試無濟於事,您將浪費時間和資源。


崩潰後恢復的檢查點

當一個批次作業在幾小時內處理50,000個文件時,第35,000個文件崩潰不應意味著從頭開始。 檢查點——記錄哪些文件已成功處理——允許您從中斷的地方恢復。

using IronPdf;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Generic;

string inputFolder = "input/";
string outputFolder = "output/";
string checkpointPath = "checkpoint.txt";
string errorLogPath = "errors.txt";

Directory.CreateDirectory(outputFolder);

// Load checkpoint - files already processed successfully
var completedFiles = new HashSet<string>();
if (File.Exists(checkpointPath))
{
    completedFiles = new HashSet<string>(File.ReadAllLines(checkpointPath));
    Console.WriteLine($"Resuming from checkpoint: {completedFiles.Count} files already processed");
}

// Get files to process (excluding already completed)
string[] allFiles = Directory.GetFiles(inputFolder, "*.html");
string[] filesToProcess = allFiles
    .Where(f => !completedFiles.Contains(Path.GetFileName(f)))
    .ToArray();

Console.WriteLine($"Files to process: {filesToProcess.Length} (skipping {completedFiles.Count} already done)");

var renderer = new ChromePdfRenderer();
var checkpointLock = new object();
int processed = 0;
int failed = 0;

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(filesToProcess, options, htmlFile =>
{
    string fileName = Path.GetFileName(htmlFile);
    string baseName = Path.GetFileNameWithoutExtension(htmlFile);
    string outputPath = Path.Combine(outputFolder, $"{baseName}.pdf");

    try
    {
        using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile);
        pdf.SaveAs(outputPath);

        // Record success in checkpoint (thread-safe)
        lock (checkpointLock)
        {
            File.AppendAllText(checkpointPath, fileName + Environment.NewLine);
        }

        Interlocked.Increment(ref processed);
        Console.WriteLine($"[OK] {baseName}.pdf");
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref failed);

        // Log error for review
        lock (checkpointLock)
        {
            File.AppendAllText(errorLogPath,
                $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {fileName} | {ex.Message}{Environment.NewLine}");
        }

        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
    }
});

Console.WriteLine($"\nBatch complete:");
Console.WriteLine($"  Newly processed: {processed}");
Console.WriteLine($"  Failed: {failed}");
Console.WriteLine($"  Total completed: {completedFiles.Count + processed}");
Console.WriteLine($"  Checkpoint saved to: {checkpointPath}");

檢查點文件充當已完成工作的持久記錄。 當管道啟動時,它會讀取檢查點文件並跳過已經成功處理的任何文件。 當文件完成處理時,其路徑會追加到檢查點文件中。這種方法是簡單的、基於文件的,不需要任何外部依賴項。

對於更複雜的場景,考慮使用資料庫表或分佈式快取(如Redis)作為您的檢查點儲存,特別是如果多個工作者在不同的機器上並行處理文件。


處理前後的驗證

驗證是具有彈性的管道的書寫結尾。處理前的驗證能夠及時攔截問題輸入,避免浪費處理時間;處理後的驗證確保輸出達到您的質量和合規要求。

輸入

此實現將處理迴圈用PreValidatePostValidate 輔助函式包裝。 處理前的驗證在處理之前檢查文件大小、內容型別和基本HTML結構。 處理後的驗證驗證輸出PDF有有效的頁面計數和合理的文件大小,將驗證通過的文件移動到一個單獨的文件夾中,而將失敗的路由到拒絕文件夾供手工審查。

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;

string inputFolder = "input/";
string outputFolder = "output/";
string validatedFolder = "validated/";
string rejectedFolder = "rejected/";

Directory.CreateDirectory(outputFolder);
Directory.CreateDirectory(validatedFolder);
Directory.CreateDirectory(rejectedFolder);

string[] inputFiles = Directory.GetFiles(inputFolder, "*.html");
var renderer = new ChromePdfRenderer();

int preValidationFailed = 0;
int processingFailed = 0;
int postValidationFailed = 0;
int succeeded = 0;

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(inputFiles, options, inputFile =>
{
    string fileName = Path.GetFileNameWithoutExtension(inputFile);
    string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf");

    // Pre-validation: Check input file
    if (!PreValidate(inputFile))
    {
        Interlocked.Increment(ref preValidationFailed);
        Console.WriteLine($"[SKIP] {fileName}: Failed pre-validation");
        return;
    }

    try
    {
        // Process
        using var pdf = renderer.RenderHtmlFileAsPdf(inputFile);
        pdf.SaveAs(outputPath);

        // Post-validation: Check output file
        if (PostValidate(outputPath))
        {
            // Move to validated folder
            string validatedPath = Path.Combine(validatedFolder, $"{fileName}.pdf");
            File.Move(outputPath, validatedPath, overwrite: true);
            Interlocked.Increment(ref succeeded);
            Console.WriteLine($"[OK] {fileName}.pdf (validated)");
        }
        else
        {
            // Move to rejected folder for manual review
            string rejectedPath = Path.Combine(rejectedFolder, $"{fileName}.pdf");
            File.Move(outputPath, rejectedPath, overwrite: true);
            Interlocked.Increment(ref postValidationFailed);
            Console.WriteLine($"[REJECT] {fileName}.pdf: Failed post-validation");
        }
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref processingFailed);
        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
    }
});

Console.WriteLine($"\nValidation summary:");
Console.WriteLine($"  Succeeded: {succeeded}");
Console.WriteLine($"  Pre-validation failed: {preValidationFailed}");
Console.WriteLine($"  Processing failed: {processingFailed}");
Console.WriteLine($"  Post-validation failed: {postValidationFailed}");

// Pre-validation: Quick checks on input file
bool PreValidate(string filePath)
{
    try
    {
        var fileInfo = new FileInfo(filePath);

        // Check file exists and is readable
        if (!fileInfo.Exists) return false;

        // Check file is not empty
        if (fileInfo.Length == 0) return false;

        // Check file is not too large (e.g., 50MB limit)
        if (fileInfo.Length > 50 * 1024 * 1024) return false;

        // Quick content check - must be valid HTML
        string content = File.ReadAllText(filePath);
        if (string.IsNullOrWhiteSpace(content)) return false;
        if (!content.Contains("<html", StringComparison.OrdinalIgnoreCase) &&
            !content.Contains("<!DOCTYPE", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        return true;
    }
    catch
    {
        return false;
    }
}

// Post-validation: Verify output PDF meets requirements
bool PostValidate(string pdfPath)
{
    try
    {
        using var pdf = PdfDocument.FromFile(pdfPath);

        // Check PDF has at least one page
        if (pdf.PageCount < 1) return false;

        // Check file size is reasonable (not just header, not corrupted)
        var fileInfo = new FileInfo(pdfPath);
        if (fileInfo.Length < 1024) return false;

        return true;
    }
    catch
    {
        return false;
    }
}

輸出

 驗證輸入處理截圖

 文件夾結構顯示驗證目錄有5個文件和空的錯誤目錄

所有五個文件均通過驗證移動到驗證目錄。

處理前的驗證應該很快——您正在檢查明顯損壞的輸入,而不是進行完整的處理。 處理後的驗證可以更全面,尤其是對於必須通過特定標準(PDF/A、PDF/UA)的合規轉換。 任何失敗處理後驗證的文件應標記以供人工審查,而不是無言接受。


我最喜歡的程式庫是IronPDF。它允許快速高效地操作PDF文件。它還有許多有價值的功能,例如導出到PDF/A格式和數位簽署PDF文件。

Milan Jovanovic

Microsoft MVP

查看案例研究

IronOCR意味著我們每年可以從手動處理中節省$40,000,同時提高生產力,釋放資源以進行高影響的任務。我會強烈推薦它。

Brent Matzelle

首席技術官,OPYN

查看案例研究

IronSuite在我們的運營中扮演著至關重要的角色。這些工具增加了包括建立平面圖和改善庫存管理在內的業務效率。

David Jones

首席軟體工程師,Agorus Build

查看案例研究

異步和並行處理模式

IronPDF支持Parallel.ForEach(基於執行緒的並行)及異步/等待(異步I/O)。 了解何時使用何種——以及如何有效地結合使用——是最大化吞吐量的關鍵。

任務並行庫整合

使用Parallel.ForEach 是CPU捲據批量操作的最簡單且最有效的方式。 IronPDF的渲染引擎是CPU密集型的(HTML解析、CSS佈局、圖像光柵化),Parallel.ForEach 自動將這些工作分配到所有可用的核心。

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;

string inputFolder = "input/";
string outputFolder = "output/";

Directory.CreateDirectory(outputFolder);

string[] htmlFiles = Directory.GetFiles(inputFolder, "*.html");
var renderer = new ChromePdfRenderer();

Console.WriteLine($"Processing {htmlFiles.Length} files with {Environment.ProcessorCount} CPU cores");

int processed = 0;
var stopwatch = Stopwatch.StartNew();

// Configure parallelism based on system resources
// Rule of thumb: ProcessorCount / 2 for memory-intensive operations
var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2)
};

Console.WriteLine($"Max parallelism: {options.MaxDegreeOfParallelism}");

// Use Parallel.ForEach for CPU-bound batch operations
Parallel.ForEach(htmlFiles, options, htmlFile =>
{
    string fileName = Path.GetFileNameWithoutExtension(htmlFile);
    string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf");

    try
    {
        // Render HTML to PDF
        using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile);
        pdf.SaveAs(outputPath);

        int current = Interlocked.Increment(ref processed);

        // Progress reporting every 10 files
        if (current % 10 == 0)
        {
            double elapsed = stopwatch.Elapsed.TotalSeconds;
            double rate = current / elapsed;
            double remaining = (htmlFiles.Length - current) / rate;
            Console.WriteLine($"Progress: {current}/{htmlFiles.Length} ({rate:F1} files/sec, ~{remaining:F0}s remaining)");
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
    }
});

stopwatch.Stop();
double totalRate = processed / stopwatch.Elapsed.TotalSeconds;

Console.WriteLine($"\nComplete:");
Console.WriteLine($"  Files processed: {processed}/{htmlFiles.Length}");
Console.WriteLine($"  Total time: {stopwatch.Elapsed.TotalSeconds:F1}s");
Console.WriteLine($"  Average rate: {totalRate:F1} files/sec");
Console.WriteLine($"  Time per file: {stopwatch.Elapsed.TotalMilliseconds / processed:F0}ms");

// Memory monitoring helper (call between chunks for large batches)
void CheckMemoryPressure()
{
    const long memoryThreshold = 4L * 1024 * 1024 * 1024; // 4 GB
    long currentMemory = GC.GetTotalMemory(forceFullCollection: false);

    if (currentMemory > memoryThreshold)
    {
        Console.WriteLine($"Memory pressure detected ({currentMemory / 1024 / 1024}MB), forcing GC...");
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
}

此時選擇MaxDegreeOfParallelism 顯得至關重要。 如果沒有它,TPL將嘗試使用所有可用核心,可能會記憶體過載如果每個渲染都是資源密集的。根據系統的可用RAM除以每次渲染的典型記憶體消耗(通常為每次並行渲染100–300 MB的複雜HTML)來設置這個值。

控制並發性(SemaphoreSlim)

當您需要比Parallel.ForEach 更細緻的並發控制時——例如,當混合異步I/O與CPU密集型渲染時——SemaphoreSlim 給您明確的控制權,控制多少操作同時運行。 模式是直接的:建立一個具有所需並發限制(例如,4個並行渲染)的Release。 然後使用Task.WhenAll 啟動所有任務。

當管道包括I/O密集型步驟(從blob儲存中讀取文件,將結果寫入資料庫)和CPU密集型步驟(渲染PDF)時,此模式特別有用。 該信號量限制了CPU密集型渲染的並發性,同時允許I/O密集型步驟在不被抑制的情況下進行。

Async/Await最佳實踐

IronPDF提供其渲染方法的異步版本,包括RenderUrlAsPdfAsyncRenderHtmlFileAsPdfAsync。 這些方法適用於網頁應用程式(在這裡阻塞請求執行緒是不可接受的)和混合PDF渲染和異步I/O操作的管道。

批次處理的一些重要的異步最佳實踐:

不要使用Task.Run包裝同步的IronPDF方法——改用原生異步版本。 在Task.Run 中包裝同步方法會浪費一個執行緒池執行緒,並增加開銷而無任何好處。

不要在異步任務上使用.Wait()——這會阻止調用執行緒並可能在UI或ASP.NET上下文中造成死鎖。 務必使用await

批量您的Task.WhenAll調用而不是一次等待所有任務。 如果您有10,000個任務,並同時在所有任務上調用Task.WhenAll,您將啟動10,000個並行操作。 相反,使用.Chunk(10)或類似方法以組為單位處理它們,順序地等待每個組。

避免記憶體枯竭

記憶體枯竭是批次PDF處理中最常見的故障模式。 防禦方法是使用GC.GetTotalMemory() 在每次渲染之前監控記憶體使用,如果消耗超過門檻(例如4 GB或可用RAM的80%)則觸發收集。 調用GC.Collect()來回收盡可能多的記憶體,然後再繼續。 這會增加一個小小的暫停,但防止了在第30,000個文件時發生OutOfMemoryException 使整個批次崩潰的災難性替代方案。

結合從TPL部分的MaxDegreeOfParallelism 限速和記憶體管理部分的using 清理模式,您對記憶體問題有一個三層防禦:限制並發性、積極清理和監控與安全閥。


適用於批次工作的雲部署

現代批次處理日益在雲端運行,您可以根據工作負載需求擴展計算資源,並僅為您使用的付費。 IronPDF在所有主要的雲平台上運行——以下是為每個平台建構批次管道的方式。

Azure Functions with Durable Functions

Azure Durable Functions為發散/收斂模式提供內建協作,使其成為批次PDF處理的自然選擇。 協作函式將工作分配給多個活動函式實例,每個實例處理一部分文件。 協作器在發散迴圈中調用ChromePdfRenderer,處理其文件部分,然後協作器收集結果。

Azure Functions的關鍵考慮因素:預設消費計劃每個函式調用有5分鐘的超時和有限的記憶體。 對於批次處理,使用高級專用計劃,支持更長的超時和更多的記憶體。 IronPDF需要完整的.NET運行時(未修整),因此確保您的函式應用程式配置為.NET 8+具有適當的運行時標識符。

AWS Lambda with Step Functions

AWS Step Functions提供類似於Azure Durable Functions的協作能力。 狀態機中的每個步驟調用一個處理一部分文件的Lambda函式。 您的Lambda處理程式接收一批S3物件金鑰,使用PdfDocument.FromFile載入每個PDF,應用您的處理管道(壓縮、格式轉換等),並將結果寫回輸出S3儲存桶。

AWS Lambda的最大執行時間為15分鐘,並有限制的/tmp儲存(預設為512 MB,可設定至最多10 GB)。 對於大型批處理作業,使用Step Functions來分塊工作負載,並在單獨的Lambda調用中處理每個塊。 將中間結果儲存在S3中,而不是本地儲存。

Kubernetes作業排程

對於運行自己Kubernetes叢集的組織,批量PDF處理非常適合映射至Kubernetes Jobs和CronJobs。 每個Pod運行一個從佇列(Azure Service Bus、RabbitMQ或SQS)中抓取檔案的批量工作者,使用IronPDF處理它們,並將結果寫入物件儲存。 該工作者迴圈遵循先前部分中介紹的相同模式:取出一個訊息,使用PdfDocument.FromFile()來處理文件,上傳結果,並確認訊息。 將處理包裹在使用復原模式中的重試邏輯的相同SemaphoreSlim來控制每個Pod的並發性。

IronPDF提供官方的Docker支援,並在Linux容器上運行。 使用IronPdf NuGet套件與適合您容器操作系統的原生運行時套件(例如,適用於基於Linux映像的IronPdf.Linux)。 對於Kubernetes,定義符合IronPDF記憶體要求的資源請求和限制(根據並發性,通常每個Pod需要512 MB至2 GB)。 水平Pod自動調整器可以根據佇列深度來擴展工作者,並且檢查點模式確保如果Pod被驅逐,不會丟失任何工作。

架構圖顯示具有作業佇列、計算層、物件儲存和監控儀表板的雲端原生批處理管道


成本優化策略

如果您不仔細考慮資源分配,雲端批量處理可能會變得昂貴。 以下是影響最大的策略:

調整您的計算資源大小。 PDF渲染是CPU和記憶體密集型的,而不是GPU密集型的。使用計算優化的實例(Azure上的C系列,AWS上的C型別)而不是一般用途或記憶體優化的實例。 您將獲得更好的價格與渲染比率。

使用搶占式/可中斷實例,適用於可容忍中斷的批處理工作負載。 批量PDF處理本質上是可以恢復的(感謝檢查點),使其成為搶占價格的理想候選者,通常提供比按需更低60-90%的折扣。

如果您的時間安排允許,則在非高峰時段進行處理。 許多雲端供應商在夜間和週末提供較低的價格或更高的搶占可用性。

提前壓縮,僅儲存一次。 將壓縮作為您的處理管道的一部分,而不是單獨的步驟。從一開始就儲存壓縮的PDF,減少檔案生存期的持續儲存成本。

分層您的儲存。 經常存取的已處理PDF應存入熱儲存; 很少存取的已歸檔PDF應移至冷或歸檔層(Azure Cool/Archive,AWS S3 Glacier)。 僅此一項就可以減少儲存成本50-80%。


真實世界的管道範例

讓我們用一個完整的、具有生產等級的批量管道將所有內容結合在一起,以展示完整的工作流程:導入 → 驗證 → 處理 → 歸檔 → 報告

此範例處理HTML發票模板的目錄,將它們呈現為PDF,壓縮輸出,轉換為PDF/A-3b以便符合歸檔要求,驗證結果,並在最後生成摘要報告。

使用上面批量轉換範例中的相同5個HTML發票...

using IronPdf;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;

// Configuration
var config = new PipelineConfig
{
    InputFolder = "input/",
    OutputFolder = "output/",
    ArchiveFolder = "archive/",
    ErrorFolder = "errors/",
    CheckpointPath = "pipeline-checkpoint.json",
    ReportPath = "pipeline-report.json",
    MaxConcurrency = Math.Max(1, Environment.ProcessorCount / 2),
    MaxRetries = 3,
    JpegQuality = 70
};

// Initialize folders
Directory.CreateDirectory(config.OutputFolder);
Directory.CreateDirectory(config.ArchiveFolder);
Directory.CreateDirectory(config.ErrorFolder);

// Load checkpoint for resume capability
var checkpoint = LoadCheckpoint(config.CheckpointPath);
var results = new ConcurrentBag<ProcessingResult>();
var stopwatch = Stopwatch.StartNew();

// Get files to process
string[] allFiles = Directory.GetFiles(config.InputFolder, "*.html");
string[] filesToProcess = allFiles
    .Where(f => !checkpoint.CompletedFiles.Contains(Path.GetFileName(f)))
    .ToArray();

Console.WriteLine($"Pipeline starting:");
Console.WriteLine($"  Total files: {allFiles.Length}");
Console.WriteLine($"  Already processed: {checkpoint.CompletedFiles.Count}");
Console.WriteLine($"  To process: {filesToProcess.Length}");
Console.WriteLine($"  Concurrency: {config.MaxConcurrency}");

var renderer = new ChromePdfRenderer();
var checkpointLock = new object();

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = config.MaxConcurrency
};

Parallel.ForEach(filesToProcess, options, inputFile =>
{
    var result = new ProcessingResult
    {
        FileName = Path.GetFileName(inputFile),
        StartTime = DateTime.UtcNow
    };

    try
    {
        // Stage: Pre-validation
        if (!ValidateInput(inputFile))
        {
            result.Status = "PreValidationFailed";
            result.Error = "Input file failed validation";
            results.Add(result);
            return;
        }

        string baseName = Path.GetFileNameWithoutExtension(inputFile);
        string tempPath = Path.Combine(config.OutputFolder, $"{baseName}.pdf");
        string archivePath = Path.Combine(config.ArchiveFolder, $"{baseName}.pdf");

        // Stage: Process with retry
        PdfDocument pdf = null;
        int attempt = 0;
        bool success = false;

        while (attempt < config.MaxRetries && !success)
        {
            attempt++;
            try
            {
                pdf = renderer.RenderHtmlFileAsPdf(inputFile);
                success = true;
            }
            catch (Exception ex) when (IsTransient(ex) && attempt < config.MaxRetries)
            {
                Thread.Sleep((int)Math.Pow(2, attempt) * 500);
            }
        }

        if (!success || pdf == null)
        {
            result.Status = "ProcessingFailed";
            result.Error = "Max retries exceeded";
            results.Add(result);
            return;
        }

        using (pdf)
        {
            // Stage: Compress and convert to PDF/A-3b for archival
            pdf.SaveAsPdfA(tempPath, PdfAVersions.PdfA3b);
        }

        // Stage: Post-validation
        if (!ValidateOutput(tempPath))
        {
            File.Move(tempPath, Path.Combine(config.ErrorFolder, $"{baseName}.pdf"), overwrite: true);
            result.Status = "PostValidationFailed";
            result.Error = "Output file failed validation";
            results.Add(result);
            return;
        }

        // Stage: Archive
        File.Move(tempPath, archivePath, overwrite: true);

        // Update checkpoint
        lock (checkpointLock)
        {
            checkpoint.CompletedFiles.Add(result.FileName);
            SaveCheckpoint(config.CheckpointPath, checkpoint);
        }

        result.Status = "Success";
        result.OutputSize = new FileInfo(archivePath).Length;
        result.EndTime = DateTime.UtcNow;
        results.Add(result);

        Console.WriteLine($"[OK] {baseName}.pdf ({result.OutputSize / 1024}KB)");
    }
    catch (Exception ex)
    {
        result.Status = "Error";
        result.Error = ex.Message;
        result.EndTime = DateTime.UtcNow;
        results.Add(result);
        Console.WriteLine($"[ERROR] {result.FileName}: {ex.Message}");
    }
});

stopwatch.Stop();

// Generate report
var report = new PipelineReport
{
    TotalFiles = allFiles.Length,
    ProcessedThisRun = results.Count,
    Succeeded = results.Count(r => r.Status == "Success"),
    PreValidationFailed = results.Count(r => r.Status == "PreValidationFailed"),
    ProcessingFailed = results.Count(r => r.Status == "ProcessingFailed"),
    PostValidationFailed = results.Count(r => r.Status == "PostValidationFailed"),
    Errors = results.Count(r => r.Status == "Error"),
    TotalDuration = stopwatch.Elapsed,
    AverageFileTime = results.Any() ? TimeSpan.FromMilliseconds(stopwatch.Elapsed.TotalMilliseconds / results.Count) : TimeSpan.Zero
};

string reportJson = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(config.ReportPath, reportJson);

Console.WriteLine($"\n=== Pipeline Complete ===");
Console.WriteLine($"Succeeded: {report.Succeeded}");
Console.WriteLine($"Failed: {report.PreValidationFailed + report.ProcessingFailed + report.PostValidationFailed + report.Errors}");
Console.WriteLine($"Duration: {report.TotalDuration.TotalMinutes:F1} minutes");
Console.WriteLine($"Report: {config.ReportPath}");

// Helper methods
bool ValidateInput(string path)
{
    try
    {
        var info = new FileInfo(path);
        if (!info.Exists || info.Length == 0 || info.Length > 50 * 1024 * 1024) return false;
        string content = File.ReadAllText(path);
        return content.Contains("<html", StringComparison.OrdinalIgnoreCase) ||
               content.Contains("<!DOCTYPE", StringComparison.OrdinalIgnoreCase);
    }
    catch { return false; }
}

bool ValidateOutput(string path)
{
    try
    {
        using var pdf = PdfDocument.FromFile(path);
        return pdf.PageCount > 0 && new FileInfo(path).Length > 1024;
    }
    catch { return false; }
}

bool IsTransient(Exception ex) =>
    ex is IOException || ex is OutOfMemoryException ||
    ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase);

Checkpoint LoadCheckpoint(string path)
{
    if (File.Exists(path))
    {
        string json = File.ReadAllText(path);
        return JsonSerializer.Deserialize<Checkpoint>(json) ?? new Checkpoint();
    }
    return new Checkpoint();
}

void SaveCheckpoint(string path, Checkpoint cp) =>
    File.WriteAllText(path, JsonSerializer.Serialize(cp));


// Data classes
public class PipelineConfig
{
    public string InputFolder { get; set; } = "";
    public string OutputFolder { get; set; } = "";
    public string ArchiveFolder { get; set; } = "";
    public string ErrorFolder { get; set; } = "";
    public string CheckpointPath { get; set; } = "";
    public string ReportPath { get; set; } = "";
    public int MaxConcurrency { get; set; }
    public int MaxRetries { get; set; }
    public int JpegQuality { get; set; }
}

public class Checkpoint
{
    public HashSet<string> CompletedFiles { get; set; } = new();
}

public class ProcessingResult
{
    public string FileName { get; set; } = "";
    public string Status { get; set; } = "";
    public string Error { get; set; } = "";
    public long OutputSize { get; set; }
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
}

public class PipelineReport
{
    public int TotalFiles { get; set; }
    public int ProcessedThisRun { get; set; }
    public int Succeeded { get; set; }
    public int PreValidationFailed { get; set; }
    public int ProcessingFailed { get; set; }
    public int PostValidationFailed { get; set; }
    public int Errors { get; set; }
    public TimeSpan TotalDuration { get; set; }
    public TimeSpan AverageFileTime { get; set; }
}
C#

輸出

管道報告JSON顯示具有成功計數、失敗計數和總持續時間的批處理結果

管道報告顯示批處理的結果。

此管道整合了我們在本教程中涵蓋的每一個模式:具有控制並發性的平行處理,單檔錯誤處理與失敗跳過,對瞬態錯誤的重試邏輯,崩潰後的繼續檢查點,前置及後置處理驗證,通過顯式處置的記憶體管理,以及帶有最終摘要報告的全面日誌記錄。

此管道的輸出是壓縮的、符合PDF/A-3b的歸檔檔案目錄,具有恢復能力的檢查點檔案,未能處理的檔案的錯誤日誌,以及包含處理統計資料的摘要報告。 這是您針對任何嚴肅的批量PDF處理工作負載所需要的模式。


後續步驟

規模化的批量PDF處理不僅僅是在迴圈中調用一個渲染方法。它需要對並發性、記憶體管理、錯誤處理和部署進行深思熟慮的架構設計—以及正確的程式庫來使一切運作順利。 IronPDF 提供了執行緒安全的渲染引擎、非同步API介面、壓縮工具格式轉換能力,這些功能構成了任何.NET批量PDF管道的基礎。

無論您是構建一個在黎明前生成數千個PDF的夜間報告生成器,將遺留文件歸檔遷移至PDF/A合規性,還是建立在Kubernetes上的雲端原生處理服務,本教程中的模式為您提供了一個行之有效的框架以供構建。 具有控制並發性的平行處理保持吞吐量高。 失敗跳過和重試邏輯在個別檔案引發問題時可保持管道運行。 檢查點可確保您永不丟失進度。 並且雲端部署模式允許您根據工作負載擴展計算資源。

準備好開始構建了嗎? 下載IronPDF,並透過免費試用進行測試—同一程式庫可以處理從單檔渲染到數十萬檔的批量管道。 如果您對於您的特定使用情況的擴展、部署或架構有任何疑問,請聯繫我們的工程支援團隊—我們已經協助多個團隊建立了各種規模的批量管道,我們很樂意協助您實現成功。

常見問題

什麼是C#中的批次PDF處理?

在C#中,批次PDF處理是指使用C#程式語言同時自動處理多個PDF文件。這種方法非常適合於大規模自動化文件工作流程。

IronPDF如何協助批次PDF處理?

IronPDF提供強大的工具和程式庫來簡化C#中的批次PDF處理。它支持並行處理,使得能夠高效地同時處理数以千计的PDF。

使用IronPDF進行並行處理的好處是什麼?

使用IronPDF的並行處理可以快速且高效地進行PDF的批次處理。這種方法最大化了資源使用並顯著縮短了處理時間。

IronPDF可以被部屬在雲平台上進行批次處理嗎?

是的,IronPDF可以部屬在如Azure Functions、AWS Lambda和Kubernetes等雲平台上,實現可擴展和靈活的批次PDF處理。

IronPDF如何在批次PDF處理中處理錯誤?

IronPDF包含錯誤處理和重試邏輯功能,以確保在批次PDF處理期間的可靠性。這些功能幫助管理和糾正錯誤而無需手動干預。

在使用IronPDF進行PDF處理時,重試邏輯的角色是什麼?

IronPDF中的重試邏輯確保暫時性問題不會干擾批次處理工作流程。若發生錯誤,IronPDF可以自動嘗試重新處理失敗的文件。

為什麼C#是一種適合批次PDF處理的語言?

C#是一種強大的程式語言,具有廣泛的程式庫和框架,非常適合批次PDF處理。它與IronPDF完美整合,實現高效的文件自動化。

IronPDF如何在處理過程中確保PDF文件的安全性?

IronPDF透過提供加密和密碼保護功能來支持PDF文件的安全處理,確保處理後的文件保持機密和安全。

批次PDF處理在企業中的一些用例是什麼?

企業使用批次PDF處理來完成大量發票生成、文件數位化和大規模報告分發等任務。IronPDF通過自動化和簡化文件工作流程來促進這些用例。

IronPDF能處理不同的PDF格式和版本嗎?

是的,IronPDF被設計用於處理各種PDF格式和版本,以確保在批次處理任務中的相容性和靈活性。

Ahmad Sohail
全端開發人員

Ahmad是一位擁有C#、Python和網頁技術堅實基礎的全端開發人員。他對構建可擴展的軟體解決方案深感興趣,並喜歡探索設計和功能在現實世界應用中的結合。

...
閱讀更多

準備開始了嗎?

Nuget Downloads 20,667,543版本:2026.7剛剛發布

立即獲取您的免費30天試用密鑰
不需要信用卡或建立賬戶
C# 用於PDF的NuGet程式庫
使用NuGet安裝

版本: 2026.7

PM > Install-Package IronPdf
nuget.org/packages/IronPdf/
  1. 在解決方案資源管理器,右鍵點選參考,管理NuGet包
  2. 選擇瀏覽並搜尋"IronPdf"
  3. 選擇套件並安裝
C# PDF DLL
下載DLL

版本: 2026.7

立即下載

或者點擊此處下載Windows安裝程式。

  1. 下載並解壓IronPDF到類似~/Libs的位置,位於您的解決方案目錄中
  2. 在Visual Studio解決方案資源管理器,右鍵點選參考。選擇瀏覽,"IronPdf.dll"

授權從$999

有問題嗎?聯絡我們的開發團隊。

Key in blue circle

立即免費取得 30 天試用金鑰

無任何限制。100% 解鎖。無需信用卡。

bullet_checked無需信用卡或建立帳號無任何限制。100% 解鎖。無需信用卡。
  • Logo Aetna
  • Logo NASA
  • Logo GE
  • Logo Porsche
  • Logo USDA
  • Logo Qatar
Join Millions of Engineers who’ve tried IronPDF
預訂您的免費現場演示
Booking Badge

受到全球數百萬工程師的信任

Iron Software的客戶標誌
獲取您的無義務諮詢
填寫以下表格或電子郵件sales@ironsoftware.com
您的詳細資訊將始終保密
受到全球數百萬工程師的信任
Iron Software的客戶標誌
立即獲取您的30天試用金鑰
無需信用卡或帳戶建立