C#中的批次PDF處理:自動化文件工作流程
IronPDF中的批次PDF處理使.NET開發人員能夠在C#中實現大規模自動化文件工作流程,從並行HTML到PDF轉換和批量合併/拆分到具有內建錯誤處理、重試邏輯和檢查點的異步PDF管道。 IronPDF的執行緒安全Chromium引擎和基於IDisposable的記憶體管理使其專為高吞吐量PDF自動化而設計,無論您是在本地運行,還是在Azure Functions、AWS Lambda或Kubernetes上運行。
本教程涵蓋了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:
-
1Install IronPDF with NuGet Package Manager
-
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部署以在您的實時環境中測試
今天就開始在您的專案中使用IronPDF,透過免費試用
在您購買或註冊30天試用版IronPDF後,在應用程式的開始處新增您的授權金鑰。
IronPdf.License.LicenseKey = "KEY";Imports IronPdf
IronPdf.License.LicenseKey = "KEY"Start using IronPDF in your project today with a free trial.
- 理解問題
- 基礎
- 核心操作
- 彈性
- 性能
- 部署
- 綜合一切
當您有數以千計的PDF需要處理時
批量PDF處理並不是一個小眾需求——這是企業文件管理的重要組成部分。 召喚它的場景遍布各行各業,它們具有一個共同特徵:一件一件地逐個執行並不是一個選項。
文件遷移項目是最常見的觸發因素之一。 當一個組織從一個文件管理系統遷移到另一個時,數以千計(有時數以百萬計)的文件需要被轉換、重新格式化或重新標記。 一家保險公司從舊有的索賠系統遷移可能需要將500,000個基於TIFF的索賠文件轉換為可搜索的PDF。 一家律所轉移到新的案件管理平台可能需要將分散的通信合併到統一的案件文件中。 這些是一次性的工作,但它們的範圍極大,對錯誤的容忍度極低。
每日報告生成是同一問題的穩態版本。 金融機構滿足數以千計客戶的日終投資組合報告,物流公司為每個出境集裝箱生成運輸清單,醫療保健系統在數百個部門中建立每日患者摘要——所有這些都產生了PDF輸出,其規模大到連續處理會超過可接受的時間窗口。 當10,000份報告需要在早上6點前準備好,而資料在午夜之前還沒有最後確定時,您就不能將它們一個接一個地渲染。
檔案數字化處於遷移和合規性的交叉點。 具有數十年紙質記錄的政府機構、大學和公司面臨著以標準合規格式(通常為PDF/A)數字化和存檔文件的命令。 數量驚人——僅NARA就收到數百萬頁的聯邦記錄以供長期保存——而這一過程需要足夠可靠,以免多年以後發現缺口。
合規補救通常是最緊迫的觸發因素。 當審計發現您的文件存檔不符合新實施的標準時——例如,您的儲存發票不符合PDF/A-3的電子發票規定,或者您的醫療記錄缺乏第508條要求的無障礙標記——您需要根據新標準處理整個現有存檔。 壓力很大,時間緊迫,而容量是您的存檔恰好包含的任何數量。
在每一種情景中,核心挑戰都是相同的:如何可靠地、有效地處理大量PDF操作,而不會耗盡記憶體或在問題出現時留下未完成的工作?

IronPDF批次處理架構
在深入了解具體操作之前,了解IronPDF如何設計以處理並行工作負載以及在其上構建批量管道時應做出哪些架構決定是很重要的。
安裝IronPDF
通過NuGet安裝IronPDF:
或使用.NET CLI:
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文件)

INV-2026-001

INV-2026-002

INV-2026-003

INV-2026-004

INV-2026-005
實現使用ChromePdfRenderer 和Parallel.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");Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading.Tasks
Imports System.Threading
' Configure paths
Dim inputFolder As String = "input/"
Dim outputFolder As String = "output/"
Directory.CreateDirectory(outputFolder)
Dim htmlFiles As String() = Directory.GetFiles(inputFolder, "*.html")
Console.WriteLine($"Found {htmlFiles.Length} HTML files to convert")
' Create renderer instance (thread-safe, can be shared)
Dim renderer As New ChromePdfRenderer()
' Track progress
Dim processed As Integer = 0
Dim failed As Integer = 0
' Process in parallel with controlled concurrency
Dim options As New ParallelOptions With {
.MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
}
Parallel.ForEach(htmlFiles, options, Sub(htmlFile)
Try
Dim fileName As String = Path.GetFileNameWithoutExtension(htmlFile)
Dim outputPath As String = Path.Combine(outputFolder, $"{fileName}.pdf")
Using pdf = renderer.RenderHtmlFileAsPdf(htmlFile)
pdf.SaveAs(outputPath)
End Using
Interlocked.Increment(processed)
Console.WriteLine($"[OK] {fileName}.pdf")
Catch ex As Exception
Interlocked.Increment(failed)
Console.WriteLine($"[ERROR] {Path.GetFileName(htmlFile)}: {ex.Message}")
End Try
End Sub)
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");Imports IronPdf
Imports System
Imports System.IO
Imports System.Linq
Imports System.Collections.Generic
Module Program
Sub Main()
Dim inputFolder As String = "documents/"
Dim outputFolder As String = "merged/"
Directory.CreateDirectory(outputFolder)
' Group PDFs by prefix (e.g., "invoice-2026-01-*.pdf" -> one merged file)
Dim pdfFiles = Directory.GetFiles(inputFolder, "*.pdf")
Dim groups = pdfFiles _
.GroupBy(Function(f) Path.GetFileName(f).Split("-"c).Take(3).Aggregate(Function(a, b) $"{a}-{b}")) _
.Where(Function(g) g.Count() > 1)
Console.WriteLine($"Found {groups.Count()} groups to merge")
For Each group In groups
Dim groupName As String = group.Key
Dim filesToMerge = group.OrderBy(Function(f) f).ToList()
Console.WriteLine($"Merging {filesToMerge.Count} files into {groupName}.pdf")
Try
' Load all PDFs for this group
Dim pdfDocs As New List(Of PdfDocument)()
For Each filePath As String In filesToMerge
pdfDocs.Add(PdfDocument.FromFile(filePath))
Next
' Merge all documents
Using merged = PdfDocument.Merge(pdfDocs)
merged.SaveAs(Path.Combine(outputFolder, $"{groupName}-merged.pdf"))
End Using
' Dispose source documents
For Each doc In pdfDocs
doc.Dispose()
Next
Console.WriteLine($" [OK] Created {groupName}-merged.pdf ({merged.PageCount} pages)")
Catch ex As Exception
Console.WriteLine($" [ERROR] {groupName}: {ex.Message}")
End Try
Next
Console.WriteLine(vbCrLf & "Merge complete")
End Sub
End Module如果要合併大量文件,要注意記憶體: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");Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading.Tasks
Module Program
Sub Main()
Dim inputFolder As String = "multipage/"
Dim outputFolder As String = "split/"
Directory.CreateDirectory(outputFolder)
Dim pdfFiles As String() = Directory.GetFiles(inputFolder, "*.pdf")
Console.WriteLine($"Found {pdfFiles.Length} PDFs to split")
Dim options As New ParallelOptions With {
.MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
}
Parallel.ForEach(pdfFiles, options, Sub(pdfFile)
Dim baseName As String = Path.GetFileNameWithoutExtension(pdfFile)
Try
Using pdf = PdfDocument.FromFile(pdfFile)
Dim pageCount As Integer = pdf.PageCount
Console.WriteLine($"Splitting {baseName}.pdf ({pageCount} pages)")
' Extract each page as a separate PDF
For i As Integer = 0 To pageCount - 1
Using singlePage = pdf.CopyPage(i)
Dim outputPath As String = Path.Combine(outputFolder, $"{baseName}-page-{i + 1:D3}.pdf")
singlePage.SaveAs(outputPath)
End Using
Next
Console.WriteLine($" [OK] Created {pageCount} files from {baseName}.pdf")
End Using
Catch ex As Exception
Console.WriteLine($" [ERROR] {baseName}: {ex.Message}")
End Try
End Sub)
' Alternative: Extract page ranges instead of individual pages
Sub SplitByRange(inputFile As String, outputFolder As String, pagesPerChunk As Integer)
Using pdf = PdfDocument.FromFile(inputFile)
Dim baseName As String = Path.GetFileNameWithoutExtension(inputFile)
Dim totalPages As Integer = pdf.PageCount
Dim chunkNumber As Integer = 1
For startPage As Integer = 0 To totalPages - 1 Step pagesPerChunk
Dim endPage As Integer = Math.Min(startPage + pagesPerChunk - 1, totalPages - 1)
Using chunk = pdf.CopyPages(startPage, endPage)
chunk.SaveAs(Path.Combine(outputFolder, $"{baseName}-chunk-{chunkNumber:D3}.pdf"))
chunkNumber += 1
End Using
Next
End Using
End Sub
Console.WriteLine(vbCrLf & "Split complete")
End Sub
End Module輸出
第2頁被提取為獨立的PDF(年度報告頁2.pdf)
IronPDF的CopyPage 和CopyPages 方法建立新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}%");Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading.Tasks
Imports System.Threading
Module Program
Sub Main()
Dim inputFolder As String = "originals/"
Dim outputFolder As String = "compressed/"
Directory.CreateDirectory(outputFolder)
Dim pdfFiles As String() = Directory.GetFiles(inputFolder, "*.pdf")
Console.WriteLine($"Found {pdfFiles.Length} PDFs to compress")
Dim totalOriginalSize As Long = 0
Dim totalCompressedSize As Long = 0
Dim processed As Integer = 0
Dim options As New ParallelOptions With {
.MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
}
Parallel.ForEach(pdfFiles, options, Sub(pdfFile)
Dim fileName As String = Path.GetFileName(pdfFile)
Dim outputPath As String = Path.Combine(outputFolder, fileName)
Try
Dim originalSize As Long = New FileInfo(pdfFile).Length
Interlocked.Add(totalOriginalSize, originalSize)
Using pdf = PdfDocument.FromFile(pdfFile)
' Apply compression with JPEG quality setting (0-100, lower = more compression)
pdf.CompressAndSaveAs(outputPath, 60)
End Using
Dim compressedSize As Long = New FileInfo(outputPath).Length
Interlocked.Add(totalCompressedSize, compressedSize)
Interlocked.Increment(processed)
Dim reduction As Double = (1 - CDbl(compressedSize) / originalSize) * 100
Console.WriteLine($"[OK] {fileName}: {originalSize \ 1024}KB → {compressedSize \ 1024}KB ({reduction:F1}% reduction)")
Catch ex As Exception
Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
End Try
End Sub)
Dim totalReduction As Double = (1 - CDbl(totalCompressedSize) / totalOriginalSize) * 100
Console.WriteLine(vbCrLf & "Compression 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}%")
End Sub
End Module關於壓縮需注意幾個事項: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以SaveAsPdfA 與PdfAVersions.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}");
}
});
}Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading.Tasks
Imports System.Threading
Module Program
Sub Main()
Dim inputFolder As String = "originals/"
Dim outputFolder As String = "pdfa-archive/"
Directory.CreateDirectory(outputFolder)
Dim pdfFiles As String() = Directory.GetFiles(inputFolder, "*.pdf")
Console.WriteLine($"Found {pdfFiles.Length} PDFs to convert to PDF/A-3b")
Dim converted As Integer = 0
Dim failed As Integer = 0
Dim options As New ParallelOptions With {
.MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
}
Parallel.ForEach(pdfFiles, options, Sub(pdfFile)
Dim fileName As String = Path.GetFileName(pdfFile)
Dim outputPath As String = Path.Combine(outputFolder, fileName)
Try
Using pdf = PdfDocument.FromFile(pdfFile)
' Convert to PDF/A-3b for long-term archival
pdf.SaveAsPdfA(outputPath, PdfAVersions.PdfA3b)
Interlocked.Increment(converted)
Console.WriteLine($"[OK] {fileName} → PDF/A-3b")
End Using
Catch ex As Exception
Interlocked.Increment(failed)
Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
End Try
End Sub)
Console.WriteLine($"{vbCrLf}Conversion complete: {converted} succeeded, {failed} failed")
End Sub
' Alternative: Convert to PDF/UA for accessibility compliance
Sub ConvertToPdfUA(inputFolder As String, outputFolder As String)
Directory.CreateDirectory(outputFolder)
Dim files As String() = Directory.GetFiles(inputFolder, "*.pdf")
Parallel.ForEach(files, Sub(pdfFile)
Dim fileName As String = Path.GetFileName(pdfFile)
Try
Using 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")
End Using
Catch ex As Exception
Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
End Try
End Sub)
End Sub
End Module輸出

輸出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);
}Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading
Imports System.Collections.Concurrent
Module Program
Sub Main()
Dim inputFolder As String = "input/"
Dim outputFolder As String = "output/"
Dim errorLogPath As String = "error-log.txt"
Directory.CreateDirectory(outputFolder)
Dim htmlFiles As String() = Directory.GetFiles(inputFolder, "*.html")
Dim renderer As New ChromePdfRenderer()
Dim errorLog As New ConcurrentBag(Of String)()
Dim processed As Integer = 0
Dim failed As Integer = 0
Dim retried As Integer = 0
Const maxRetries As Integer = 3
Dim options As New ParallelOptions With {
.MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
}
Parallel.ForEach(htmlFiles, options, Sub(htmlFile)
Dim fileName As String = Path.GetFileNameWithoutExtension(htmlFile)
Dim outputPath As String = Path.Combine(outputFolder, $"{fileName}.pdf")
Dim attempt As Integer = 0
Dim success As Boolean = False
While attempt < maxRetries AndAlso Not success
attempt += 1
Try
Using pdf = renderer.RenderHtmlFileAsPdf(htmlFile)
pdf.SaveAs(outputPath)
success = True
Interlocked.Increment(processed)
If attempt > 1 Then
Interlocked.Increment(retried)
Console.WriteLine($"[OK] {fileName}.pdf (succeeded on attempt {attempt})")
Else
Console.WriteLine($"[OK] {fileName}.pdf")
End If
End Using
Catch ex As Exception When IsTransientException(ex) AndAlso attempt < maxRetries
' Transient error - wait and retry with exponential backoff
Dim delayMs As Integer = CInt(Math.Pow(2, attempt)) * 500
Console.WriteLine($"[RETRY] {fileName}: {ex.Message} (attempt {attempt}, waiting {delayMs}ms)")
Thread.Sleep(delayMs)
Catch ex As Exception
' Non-transient error or max retries exceeded
Interlocked.Increment(failed)
Dim errorMessage As String = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {fileName} | {ex.GetType().Name} | {ex.Message}"
errorLog.Add(errorMessage)
Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
End Try
End While
End Sub)
' Write error log
If errorLog.Count > 0 Then
File.WriteAllLines(errorLogPath, errorLog)
End If
Console.WriteLine($"{vbCrLf}Batch complete:")
Console.WriteLine($" Processed: {processed}")
Console.WriteLine($" Failed: {failed}")
Console.WriteLine($" Retried: {retried}")
If failed > 0 Then
Console.WriteLine($" Error log: {errorLogPath}")
End If
End Sub
' Helper to identify transient exceptions worth retrying
Function IsTransientException(ex As Exception) As Boolean
Return TypeOf ex Is IOException OrElse
TypeOf ex Is OutOfMemoryException OrElse
ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) OrElse
ex.Message.Contains("locked", StringComparison.OrdinalIgnoreCase)
End Function
End Module批次完成後,查看錯誤日誌以了解哪個文件失敗以及原因。 常見的失敗原因包括損壞的源文件、密碼保護的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}");Imports IronPdf
Imports System
Imports System.IO
Imports System.Linq
Imports System.Threading.Tasks
Imports System.Threading
Imports System.Collections.Generic
Module Program
Sub Main()
Dim inputFolder As String = "input/"
Dim outputFolder As String = "output/"
Dim checkpointPath As String = "checkpoint.txt"
Dim errorLogPath As String = "errors.txt"
Directory.CreateDirectory(outputFolder)
' Load checkpoint - files already processed successfully
Dim completedFiles As New HashSet(Of String)()
If File.Exists(checkpointPath) Then
completedFiles = New HashSet(Of String)(File.ReadAllLines(checkpointPath))
Console.WriteLine($"Resuming from checkpoint: {completedFiles.Count} files already processed")
End If
' Get files to process (excluding already completed)
Dim allFiles As String() = Directory.GetFiles(inputFolder, "*.html")
Dim filesToProcess As String() = allFiles _
.Where(Function(f) Not completedFiles.Contains(Path.GetFileName(f))) _
.ToArray()
Console.WriteLine($"Files to process: {filesToProcess.Length} (skipping {completedFiles.Count} already done)")
Dim renderer As New ChromePdfRenderer()
Dim checkpointLock As New Object()
Dim processed As Integer = 0
Dim failed As Integer = 0
Dim options As New ParallelOptions With {
.MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
}
Parallel.ForEach(filesToProcess, options, Sub(htmlFile)
Dim fileName As String = Path.GetFileName(htmlFile)
Dim baseName As String = Path.GetFileNameWithoutExtension(htmlFile)
Dim outputPath As String = Path.Combine(outputFolder, $"{baseName}.pdf")
Try
Using pdf = renderer.RenderHtmlFileAsPdf(htmlFile)
pdf.SaveAs(outputPath)
End Using
' Record success in checkpoint (thread-safe)
SyncLock checkpointLock
File.AppendAllText(checkpointPath, fileName & Environment.NewLine)
End SyncLock
Interlocked.Increment(processed)
Console.WriteLine($"[OK] {baseName}.pdf")
Catch ex As Exception
Interlocked.Increment(failed)
' Log error for review
SyncLock checkpointLock
File.AppendAllText(errorLogPath,
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {fileName} | {ex.Message}{Environment.NewLine}")
End SyncLock
Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
End Try
End Sub)
Console.WriteLine(vbCrLf & "Batch complete:")
Console.WriteLine($" Newly processed: {processed}")
Console.WriteLine($" Failed: {failed}")
Console.WriteLine($" Total completed: {completedFiles.Count + processed}")
Console.WriteLine($" Checkpoint saved to: {checkpointPath}")
End Sub
End Module檢查點文件充當已完成工作的持久記錄。 當管道啟動時,它會讀取檢查點文件並跳過已經成功處理的任何文件。 當文件完成處理時,其路徑會追加到檢查點文件中。這種方法是簡單的、基於文件的,不需要任何外部依賴項。
對於更複雜的場景,考慮使用資料庫表或分佈式快取(如Redis)作為您的檢查點儲存,特別是如果多個工作者在不同的機器上並行處理文件。
處理前後的驗證
驗證是具有彈性的管道的書寫結尾。處理前的驗證能夠及時攔截問題輸入,避免浪費處理時間;處理後的驗證確保輸出達到您的質量和合規要求。
輸入
此實現將處理迴圈用PreValidate 和PostValidate 輔助函式包裝。 處理前的驗證在處理之前檢查文件大小、內容型別和基本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;
}
}Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading
Imports System.Collections.Concurrent
Module Program
Sub Main()
Dim inputFolder As String = "input/"
Dim outputFolder As String = "output/"
Dim validatedFolder As String = "validated/"
Dim rejectedFolder As String = "rejected/"
Directory.CreateDirectory(outputFolder)
Directory.CreateDirectory(validatedFolder)
Directory.CreateDirectory(rejectedFolder)
Dim inputFiles As String() = Directory.GetFiles(inputFolder, "*.html")
Dim renderer As New ChromePdfRenderer()
Dim preValidationFailed As Integer = 0
Dim processingFailed As Integer = 0
Dim postValidationFailed As Integer = 0
Dim succeeded As Integer = 0
Dim options As New ParallelOptions With {
.MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
}
Parallel.ForEach(inputFiles, options, Sub(inputFile)
Dim fileName As String = Path.GetFileNameWithoutExtension(inputFile)
Dim outputPath As String = Path.Combine(outputFolder, $"{fileName}.pdf")
' Pre-validation: Check input file
If Not PreValidate(inputFile) Then
Interlocked.Increment(preValidationFailed)
Console.WriteLine($"[SKIP] {fileName}: Failed pre-validation")
Return
End If
Try
' Process
Using pdf = renderer.RenderHtmlFileAsPdf(inputFile)
pdf.SaveAs(outputPath)
' Post-validation: Check output file
If PostValidate(outputPath) Then
' Move to validated folder
Dim validatedPath As String = Path.Combine(validatedFolder, $"{fileName}.pdf")
File.Move(outputPath, validatedPath, overwrite:=True)
Interlocked.Increment(succeeded)
Console.WriteLine($"[OK] {fileName}.pdf (validated)")
Else
' Move to rejected folder for manual review
Dim rejectedPath As String = Path.Combine(rejectedFolder, $"{fileName}.pdf")
File.Move(outputPath, rejectedPath, overwrite:=True)
Interlocked.Increment(postValidationFailed)
Console.WriteLine($"[REJECT] {fileName}.pdf: Failed post-validation")
End If
End Using
Catch ex As Exception
Interlocked.Increment(processingFailed)
Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
End Try
End Sub)
Console.WriteLine(vbCrLf & "Validation summary:")
Console.WriteLine($" Succeeded: {succeeded}")
Console.WriteLine($" Pre-validation failed: {preValidationFailed}")
Console.WriteLine($" Processing failed: {processingFailed}")
Console.WriteLine($" Post-validation failed: {postValidationFailed}")
End Sub
' Pre-validation: Quick checks on input file
Function PreValidate(filePath As String) As Boolean
Try
Dim fileInfo As New FileInfo(filePath)
' Check file exists and is readable
If Not fileInfo.Exists Then Return False
' Check file is not empty
If fileInfo.Length = 0 Then Return False
' Check file is not too large (e.g., 50MB limit)
If fileInfo.Length > 50 * 1024 * 1024 Then Return False
' Quick content check - must be valid HTML
Dim content As String = File.ReadAllText(filePath)
If String.IsNullOrWhiteSpace(content) Then Return False
If Not content.Contains("<html", StringComparison.OrdinalIgnoreCase) AndAlso
Not content.Contains("<!DOCTYPE", StringComparison.OrdinalIgnoreCase) Then
Return False
End If
Return True
Catch
Return False
End Try
End Function
' Post-validation: Verify output PDF meets requirements
Function PostValidate(pdfPath As String) As Boolean
Try
Using pdf = PdfDocument.FromFile(pdfPath)
' Check PDF has at least one page
If pdf.PageCount < 1 Then Return False
' Check file size is reasonable (not just header, not corrupted)
Dim fileInfo As New FileInfo(pdfPath)
If fileInfo.Length < 1024 Then Return False
Return True
End Using
Catch
Return False
End Try
End Function
End Module輸出


所有五個文件均通過驗證移動到驗證目錄。
處理前的驗證應該很快——您正在檢查明顯損壞的輸入,而不是進行完整的處理。 處理後的驗證可以更全面,尤其是對於必須通過特定標準(PDF/A、PDF/UA)的合規轉換。 任何失敗處理後驗證的文件應標記以供人工審查,而不是無言接受。
異步和並行處理模式
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();
}
}Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading.Tasks
Imports System.Threading
Imports System.Diagnostics
Module Program
Sub Main()
Dim inputFolder As String = "input/"
Dim outputFolder As String = "output/"
Directory.CreateDirectory(outputFolder)
Dim htmlFiles As String() = Directory.GetFiles(inputFolder, "*.html")
Dim renderer As New ChromePdfRenderer()
Console.WriteLine($"Processing {htmlFiles.Length} files with {Environment.ProcessorCount} CPU cores")
Dim processed As Integer = 0
Dim stopwatch As Stopwatch = Stopwatch.StartNew()
' Configure parallelism based on system resources
' Rule of thumb: ProcessorCount / 2 for memory-intensive operations
Dim options As New ParallelOptions With {
.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, Sub(htmlFile)
Dim fileName As String = Path.GetFileNameWithoutExtension(htmlFile)
Dim outputPath As String = Path.Combine(outputFolder, $"{fileName}.pdf")
Try
' Render HTML to PDF
Using pdf = renderer.RenderHtmlFileAsPdf(htmlFile)
pdf.SaveAs(outputPath)
End Using
Dim current As Integer = Interlocked.Increment(processed)
' Progress reporting every 10 files
If current Mod 10 = 0 Then
Dim elapsed As Double = stopwatch.Elapsed.TotalSeconds
Dim rate As Double = current / elapsed
Dim remaining As Double = (htmlFiles.Length - current) / rate
Console.WriteLine($"Progress: {current}/{htmlFiles.Length} ({rate:F1} files/sec, ~{remaining:F0}s remaining)")
End If
Catch ex As Exception
Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
End Try
End Sub)
stopwatch.Stop()
Dim totalRate As Double = processed / stopwatch.Elapsed.TotalSeconds
Console.WriteLine(vbCrLf & "Complete:")
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)
CheckMemoryPressure()
End Sub
Sub CheckMemoryPressure()
Const memoryThreshold As Long = 4L * 1024 * 1024 * 1024 ' 4 GB
Dim currentMemory As Long = GC.GetTotalMemory(forceFullCollection:=False)
If currentMemory > memoryThreshold Then
Console.WriteLine($"Memory pressure detected ({currentMemory \ 1024 \ 1024}MB), forcing GC...")
GC.Collect()
GC.WaitForPendingFinalizers()
GC.Collect()
End If
End Sub
End Module此時選擇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提供其渲染方法的異步版本,包括RenderUrlAsPdfAsync 和RenderHtmlFileAsPdfAsync。 這些方法適用於網頁應用程式(在這裡阻塞請求執行緒是不可接受的)和混合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; }
}
輸出

管道報告顯示批處理的結果。
此管道整合了我們在本教程中涵蓋的每一個模式:具有控制並發性的平行處理,單檔錯誤處理與失敗跳過,對瞬態錯誤的重試邏輯,崩潰後的繼續檢查點,前置及後置處理驗證,通過顯式處置的記憶體管理,以及帶有最終摘要報告的全面日誌記錄。
此管道的輸出是壓縮的、符合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格式和版本,以確保在批次處理任務中的相容性和靈活性。
