C#によるPDFバッチ処理:ドキュメントワークフローを大規模に自動化する

This article was translated from English: Does it need improvement?
Translated
View the article in English

C#でIronPDFを使ったバッチPDF処理により、.NET開発者は並列のHTML-to-PDF変換や一括マージ/分割から、エラー処理、再試行ロジック、チェックポイントを組み込んだ非同期PDFパイプラインまで、ドキュメントワークフローを大規模に自動化することができます。 IronPDFのスレッドセーフなChromiumエンジンとIDisposableベースのメモリ管理は、オンプレミス、Azure FunctionsAWS LambdaKubernetesのいずれで実行する場合でも、高スループットのPDF自動化のために構築されています。

TL;DR:クイックスタートガイド

このチュートリアルでは、並列変換や一括操作からクラウド展開や弾力性のあるパイプラインパターンまで、C#によるスケーラブルなPDF自動化について説明します。

  • 対象者: .NET開発者およびアーキテクトで、ドキュメントの移行プロジェクト、日次レポート生成パイプライン、コンプライアンス修復スイープ、逐次処理が不可能なアーカイブのデジタル化など、ドキュメントを多用するワークフローを担当する人。
  • 構築するもの: Parallel.ForEachによるHTMLからPDFへの並列変換、バッチマージおよびスプリット操作、同時実行制御のためのSemaphoreSlimによる非同期パイプライン、skip-on-failureおよびリトライロジックによるエラー処理、クラッシュリカバリのためのチェックポイント/リジュームパターン、Azure Functions、AWS Lambda、およびKubernetesのためのクラウドデプロイメント構成。
  • 動作環境: .NET 6+、.NET Framework 4.6.2+、.NET Standard 2.0。すべてのレンダリングはIronPdfの組み込みChromiumエンジンを使用しています。ヘッドレスブラウザへの依存や外部サービスは必要ありません。
  • このアプローチを使用する場合:シーケンシャルな実行が可能にするよりも多くのPDFを処理する必要がある場合 - 規模の大きなドキュメントの移行、厳しいタイムウィンドウでスケジュールされたバッチジョブ、またはドキュメントの負荷が変動するマルチテナント型プラットフォームなど。
  • 技術的に重要な理由: IronPDFのChromePdfRendererはスレッドセーフで、レンダリングごとにステートレスです。 .NET のタスク並列ライブラリおよび PdfDocument 上の IDisposable と組み合わせることで、競合状態やメモリ リークのない、予測可能なメモリ動作と CPU 飽和状態が得られます。

わずか数行のコードで、HTMLファイルのディレクトリ全体をPDFにバッチ変換します:

Nuget Icon今すぐ NuGet で PDF を作成してみましょう:

  1. NuGet パッケージ マネージャーを使用して IronPDF をインストールします

    PM > Install-Package IronPdf

  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");
    });
  3. 実際の環境でテストするためにデプロイする

    今すぐ無料トライアルでプロジェクトに IronPDF を使い始めましょう
    arrow pointer

IronPDFを購入または30日間のトライアルにサインアップした後、アプリケーションの最初にライセンスキーを追加してください。

IronPdf.License.LicenseKey = "KEY";
IronPdf.License.LicenseKey = "KEY";
$vbLabelText   $csharpLabel

今日あなたのプロジェクトでIronPDFを無料トライアルで使用開始。

最初のステップ:
green arrow pointer
NuGet 購入の準備ができていませんか?

PM >  Install-Package IronPdf

IronPDFNuGet でチェックしてください。1000万回以上のダウンロードで、C#によるPDF開発を変革しています。 DLL または Windowsインストーラー をダウンロードすることもできます。

目次


何千ものPDFを処理しなければならない場合

PDFのバッチ処理はニッチな要件ではなく、企業の文書管理の日常的な部分です。 翻訳を必要とするシナリオは、あらゆる業界で発生し、一度に1つのことを行うという選択肢はないという共通の特徴があります。

ドキュメント移行プロジェクトは、最も一般的なトリガーの1つです。 組織がある文書管理システムから別のシステムに移行する際には、何千(場合によっては何百万)もの文書を変換、再フォーマット、再タグ付けする必要があります。 レガシークレームシステムから移行する保険会社では、50万件のTIFFベースのクレーム文書を検索可能なPDFに変換する必要があるかもしれません。 新しいケース管理プラットフォームに移行する法律事務所では、散在する通信文書を統一されたケースファイルに統合する必要があるかもしれません。 これらは1回限りの仕事ですが、範囲が広く、ミスを許しません。

日報作成は、同じ問題の定常状態バージョンです。 何千ものクライアントのために1日終了時のポートフォリオレポートを作成する金融機関、すべての出荷コンテナの出荷明細書を作成する物流会社、何百もの部門にわたって毎日の患者サマリーを作成する医療システム - これらはすべて、逐次処理では許容可能なタイムウィンドウを過ぎてしまうような規模でPDF出力を生成します。 朝6時までに10,000件のレポートを作成する必要があり、データが最終的に完成するのは真夜中です。

アーカイブのデジタル化は、移行とコンプライアンスの交差点に位置します。 数十年にわたる紙の記録を持つ政府機関、大学、企業は、標準に準拠したフォーマット(通常はPDF/A)で文書をデジタル化し、アーカイブすることが義務付けられています。 その量は膨大で、NARAだけでも何百万ページもの連邦政府の記録を永久保存のために受け取っている。

コンプライアンスの是正は、しばしば最も緊急な引き金となります。 例えば、保存されている請求書が電子請求書発行規制の PDF/A-3 に準拠していない、あるいは医療記録に 508 条で要求されるアクセシビリティ・タギングがないなどです。 プレッシャーは高く、スケジュールはタイトで、ボリュームはアーカイブに含まれるものであれば何でも構いません。

これらのシナリオのいずれにおいても、核となる課題は同じです。それは、大量のPDF操作を確実かつ効率的に、しかもメモリ不足に陥ったり、何か問題が発生したときに中途半端な作業を残したりすることなく処理するにはどうすればいいか、ということです。

4つのバッチ処理シナリオ (ドキュメント移行、日次レポート生成、アーカイブのデジタル化、コンプライアンス修復) を示すインフォグラフィックで、それぞれにアイコン、典型的なボリューム範囲、タイム プレッシャー インジケータが表示されます


IronPDFバッチ処理アーキテクチャ

具体的な操作に入る前に、IronPdfがどのように並行ワークロードを処理するように設計されているのか、そしてその上にバッチパイプラインを構築する際にどのようなアーキテクチャ上の決定を下すべきなのかを理解することが重要です。

IronPDFのインストール

NuGet経由でIronPDFをインストールしてください:

Install-Package IronPdf
Install-Package IronPdf
SHELL

または.NET CLIを使用して:

dotnet add package IronPdf
dotnet add package IronPdf
SHELL

IronPDFは.NET Framework 4.6.2+, .NET Core, .NET 5 through .NET 10, .NET Standard 2.0をサポートしています。Windows、Linux、macOS、Dockerコンテナ上で動作するため、オンプレミスのバッチジョブとクラウドネイティブなデプロイの両方に適しています。

量産バ ッ チ処理のためには、 PDF 操作を開始す る 前に、 アプ リ ケーシ ョ ン起動時に License.LicenseKey で ラ イ セ ン ス キーを設定 し て く だ さ い。 これにより、すべてのスレッドのすべてのレンダリング呼び出しが、ファイルごとの透かしなしで全機能セットにアクセスできるようになります。


並行性制御とスレッドの安全性

IronPdfのChromiumベースのレンダリングエンジンはスレッドセーフです。 スレッド間で複数のChromePdfRendererインスタンスを作成することも、一つのインスタンスを共有することもできます。 バッチ処理の公式な推奨は、.NETの組み込みのParallel.ForEachを使用することで、利用可能なすべてのCPUコアに作業を自動的に分散させます。

とはいえ、"スレッドセーフ"は"無制限のスレッドを使用する"という意味ではありません。各同時 PDF レンダリング操作はメモリを消費します(Chromium エンジンは、DOM 解析、CSS レイアウト、および画像のラスタライズ用に作業領域を必要とします)。メモリに制約のあるシステムであまりにも多くの並列操作を起動すると、パフォーマンスが低下するか、OutOfMemoryExceptionが発生します。 適切な並行処理のレベルは、ハードウェアに依存します。64 GBのRAMを搭載した16コアのサーバーであれば、8~12個の並行レンダリングを快適に処理できます; 8GBの4コアVMは、2-4に制限されるかもしれません。ParallelOptions.MaxDegreeOfParallelismでこれを制御します。開始点として、利用可能な CPU コアのおよそ半分に設定し、その後、観測されたメモリ圧力に基づいて調整します。

スケールでのメモリ管理

メモリ管理は、PDFのバッチ処理において最も重要な問題です。 すべての PdfDocument オブジェクトは、PDF の完全なバイナリ コンテンツをメモリ内に保持しており、これらのオブジェクトの破棄に失敗すると、処理されるファイル数に応じてメモリが線形に増大します。

重要なルール常にusingステートメントを使用するか、PdfDocumentオブジェクト上でDispose()を明示的に呼び出します。 IronPDFのPdfDocumentIDisposableを実装しており、ディスポーズに失敗することはバッチシナリオにおけるメモリ問題の最も一般的な原因です。 処理ループの各反復は、PdfDocumentを作成し、処理を行い、破棄します。特定の理由とそれを処理するのに十分なメモリがない限り、PdfDocumentオブジェクトをリストやコレクションに蓄積してはいけません。

廃棄だけでなく、大規模バッチのメモリ管理戦略も検討してください:

一度にすべてを読み込むのではなく、塊で処理する。 50,000のファイルを処理する必要がある場合、それらをすべてリストに列挙してから反復処理するのではなく、100または500のバッチで処理し、ガベージ・コレクタがチャンク間でメモリを回収できるようにします。

非常に大きなバッチに対して、チャンク間で強制的にガベージコレクションを行います。 一般的には GC の自己管理に任せるべきですが、バッチ処理は、チャンクの境界間で GC.Collect() を呼び出すことで、メモリ圧力の蓄積を防ぐことができる稀なシナリオの 1 つです。

メモリ消費を監視する GC.GetTotalMemory()またはプロセスレベルのメトリクスを使用します。 メモリ使用量がしきい値(たとえば、利用可能なRAMの80%)を超えた場合、GCが追いつくまで処理を一時停止します。


進捗レポートとログ

バッチジョブの完了に数時間を要する場合、その進捗状況の可視化はオプションではなく、不可欠です。 最低限、各ファイルの開始と完了を記録し、成功/失敗のカウントを追跡し、残りの推定時間を提供する必要があります。 並列処理を実行するときは、スレッドセーフなカウンターのためにInterlocked.Incrementを使用し、出力があふれるのを避けるために、1ファイルごとではなく、一定間隔(50または100ファイルごと)でログを記録してください。 System.Diagnostics.Stopwatchで経過時間を追跡し、実行中のファイル/秒レートを計算して、意味のある ETA を示します。

本番のバッチジョブの場合、バッチプロセスに直接接続しなくても監視ダッシュボードがリアルタイムのステータスを表示できるように、永続的なストア(データベース、ファイル、メッセージキュー)に進捗を書き込むことを検討してください。


一般的なバッチ操作

アーキテクチャー基盤が整ったところで、最も一般的なバッチ操作とIronPDFの実装について説明しましょう。

HTMLからPDFへの一括変換

HTMLからPDFへの変換は、最も一般的なバッチ作業です。 テンプレートから請求書を生成する場合でも、HTMLドキュメントのライブラリをPDFに変換する場合でも、Webアプリケーションから動的なレポートをレンダリングする場合でも、パターンは同じです。

入力 (5 HTML ファイル)

HTML Invoice INV-2026-001
HTML Invoice INV-2026-002
HTML Invoice INV-2026-003
HTML Invoice INV-2026-004
HTML Invoice INV-2026-005

この実装では、ChromePdfRendererParallel.ForEachを使用してすべてのHTMLファイルを同時に処理し、MaxDegreeOfParallelismによって並列度を制御して、メモリ消費量に対するスループットのバランスを取っています。 各ファイルはRenderHtmlFileAsPdfでレンダリングされ、スレッドセーフなInterlockedカウンタによって進行状況を追跡しながら、出力ディレクトリに保存されます。

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-html-to-pdf.cs
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");
$vbLabelText   $csharpLabel

出力


各HTML請求書は、対応するPDFにレンダリングされます。 上記は、INV-2026-001.pdf(5つのバッチ出力のうちの1つ)です。

テンプレートベースの生成(請求書やレポートなど)では、通常、データをHTMLテンプレートにマージしてからレンダリングします。 アプローチは簡単です: HTMLテンプレートを一度読み込み、string.Replaceを使ってレコードごとのデータ(顧客名、合計、日付)を注入し、並列ループ内でRenderHtmlAsPdfに注入されたHTMLを渡します。IronPDFはParallel.ForEachの代わりにasync/awaitを使いたい場合のためにRenderHtmlAsPdfAsyncも提供しています。


バッチ PDF マージ

PDFのグループを結合ドキュメントにマージすることは、法務(ケースファイルのドキュメントをマージする)、財務(月次明細書を四半期報告書に結合する)、出版のワークフローでは一般的です。

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-merge.cs
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");
$vbLabelText   $csharpLabel

PdfDocument.Mergeメソッドは、すべてのソース ドキュメントを同時にメモリにロードします。 何百もの大きなPDFをマージする場合は、段階的にマージすることを検討してください。10~20ファイルのグループを中間ドキュメントに結合し、次に中間ドキュメントをマージします。

バッチ PDF 分割

複数ページのPDFを個々のページ(またはページ範囲)に分割することは、結合の逆です。 スキャンした文書のバッチを個々のレコードに分割する必要があるメールルーム処理や、複合文書を分割する必要がある印刷ワークフローで一般的です。

入力

以下のコードは、並列ループでCopyPageを使って個々のページを抽出し、ページごとに別々のPDFファイルを作成することを示しています。 代替のSplitByRangeヘルパー関数は、個々のページではなくページ範囲を抽出する方法を示しており、大きなドキュメントを小さなセグメントに分割するのに便利です。

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-split.cs
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");
$vbLabelText   $csharpLabel

出力


2ページ目をスタンドアロンPDFとして抽出 (annual-report-page-2.pdf)

IronPDFのCopyPageCopyPagesメソッドは指定されたページを含む新しいPdfDocumentオブジェクトを作成します。 保存後、ソース文書と抽出された各ページ文書の両方を処分することを忘れないでください。


バッチ圧縮

ストレージコストが重要な場合や、帯域幅が制限された接続でPDFを送信する必要がある場合、バッチ圧縮を使用すると、アーカイブのフットプリントを劇的に削減できます。 IronPDFは2つの圧縮アプローチを提供します:CompressImagesは画質とサイズを縮小し、CompressStructTreeは構造メタデータを削除します。 新しい CompressAndSaveAs API (バージョン 2025.12 で導入) は、複数の最適化テクニックを組み合わせて優れた圧縮を提供します。

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-compression.cs
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}%");
$vbLabelText   $csharpLabel

圧縮についての注意点:JPEGの画質を60以下に設定すると、ほとんどの画像で目に見えるアーチファクトが発生します。 ShrinkImageオプションは、構成によっては歪みを引き起こす可能性があります。 また、構造ツリー(CompressStructTree)を削除すると、圧縮されたPDFのテキスト選択と検索に影響するので、それらの機能が必要ないときだけ使用してください。


バッチフォーマット変換(PDF/A、PDF/UA)

既存のアーカイブを標準に準拠した形式(長期アーカイブの場合はPDF/A、アクセシビリティの場合はPDF/UA)に変換することは、最も価値の高いバッチ処理の1つです。 IronPDFはPDF/Aの全バージョン(バージョン2025.11で追加されたPDF/A-4を含む)とPDF/UAコンプライアンス(バージョン2025.12で追加されたPDF/UA-2を含む)をサポートしています。

入力

この例では、各PDFをPdfDocument.FromFileで読み込み、PdfAVersions.PdfA3bパラメータでSaveAsPdfAを使ってPDF/A-3bに変換しています。 代替のConvertToPdfUA関数は、SaveAsPdfUAを使ったアクセシビリティ準拠の変換を示しますが、PDF/UAには適切な構造タグ付けをしたソース文書が必要です。

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-format-conversion.cs
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}");
        }
    });
}
$vbLabelText   $csharpLabel

出力

コンプライアンス メタデータを示す PDF/A 変換出力比較

出力されるPDFは、見た目はバイト単位で同じですが、アーカイブシステム用にPDF/A-3b準拠のメタデータが追加されています。

フォーマット変換は、組織が既存のアーカイブが規制基準を満たしていないことを発見する、コンプライアンス改善プロジェクトにおいて特に重要です。 バッチパターンは簡単ですが、検証ステップは非常に重要です。変換された各ファイルがコンプライアンスチェックを実際に通過していることを常に確認してから、完成とみなします。 検証については、以下の回復力のセクションで詳しく説明します。


レジリエントなバッチパイプラインを構築する

100ファイルでは完璧に動作し、50,000ファイル中4,327ファイルでクラッシュするバッチパイプラインは役に立ちません。 弾力性 - エラーを優雅に処理し、一時的な障害を再試行し、クラッシュ後に再開する能力 - は、プロダクショングレードのパイプラインとプロトタイプを分けるものです。

エラー処理とスキップオンフェイル

最も基本的なレジリエンス・パターンは、スキップ・オン・フェイルです。1つのファイルが処理に失敗した場合、バッチ全体を中止するのではなく、エラーをログに記録して次のファイルを続行します。 これは当たり前のことのように聞こえますが、Parallel.ForEach を使用しているときは意外と見落としがちです。どの並列タスクでも、処理されなかった例外は AggregateExceptionとして伝播し、ループを終了します。

IOExceptionOutOfMemoryExceptionのような一時的な例外のために指数関数的バックオフを使用した内側の再試行ループで、優雅なエラー処理のために各ファイルをtry-catchでラップします:

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-error-handling-retry.cs
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);
}
$vbLabelText   $csharpLabel

バッチが完了したら、エラーログを確認して、どのファイルがなぜ失敗したかを理解します。 よくある失敗の原因としては、破損したソースファイル、パスワードで保護されたPDF、ソースコンテンツでサポートされていない機能、非常に大きなドキュメントでのメモリ不足などがあります。

過渡故障のための再試行ロジック

いくつかの失敗は一過性のもので、再挑戦すれば成功します。 例えば、ファイルシステムの競合(別のプロセスがファイルをロックしている)、一時的なメモリの圧迫(GCがまだ追いついていない)、HTMLコンテンツで外部リソースを読み込む際のネットワークのタイムアウトなどです。 上記のコード例では、指数関数的バックオフ(短い遅延から開始し、再試行するたびにそれを倍増させ、最大再試行回数(通常は3回)を上限とする)を使用して、これらの処理を行います。

重要なのは、リトライ可能な障害とリトライ不可能な障害を区別することです。 IOException(ファイルがロックされました)またはOutOfMemoryException(一時的な圧力)は、再試行する価値があります。 ArgumentException(無効な入力)や一貫性のないレンダリングエラーはNGです。


クラッシュ後の再開のためのチェックポイント

バッチジョブが数時間かけて50,000ファイルを処理する場合、ファイル番号35,000でクラッシュが発生しても、最初からやり直しということにはなりません。 チェックポイント(どのファイルが正常に処理されたかを記録すること)により、中断したところから再開することができます。

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-checkpointing.cs
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}");
$vbLabelText   $csharpLabel

チェックポイントファイルは、完了した作業の永続的な記録として機能します。 パイプラインが起動すると、チェックポイントファイルを読み、すでに正常に処理されたファイルはスキップします。 ファイルの処理が完了すると、そのパスがチェックポイント・ファイルに追加されます。このアプローチはシンプルで、ファイルベースで、外部依存を必要としません。

より洗練されたシナリオでは、チェックポイントストアとしてデータベーステーブルや分散キャッシュ(Redisなど)を使用することを検討してください。


処理前後の検証

バリデーションは、回復力のあるパイプラインのブックエンドです。プリプロセスのバリデーションは、処理時間を無駄にする前に問題のある入力を検出します。ポストプロセスのバリデーションは、出力が品質要件とコンプライアンス要件を満たしていることを確認します。

入力

この実装では、PreValidatePostValidateの両方のヘルパー関数で処理ループをラップしています。 事前検証では、処理前にファイルサイズ、コンテンツタイプ、基本的なHTML構造をチェックします。 ポストバリデーションは、出力PDFが有効なページ数と妥当なファイルサイズを持っていることを検証し、有効なファイルを別のフォルダに移動し、手動レビューのために失敗したファイルを拒否フォルダにルーティングします。

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-validation.cs
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;
    }
}
$vbLabelText   $csharpLabel

出力

バリデーション入力処理のスクリーンショット

5つのファイルがある検証済みディレクトリと空のエラーディレクトリを示すフォルダ構造

5つのファイルすべてが検証に合格し、検証済みフォルダに移動しました。

前処理の検証は高速であるべきです。明らかに壊れた入力をチェックするのであって、完全な処理をするわけではありません。 特に、出力が特定の規格(PDF/A、PDF/UA)に合格する必要があるコンプライアンス変換では、後処理の検証をより徹底することができます。 処理後の検証に失敗したファイルには、黙って受け入れるのではなく、手動レビューのためのフラグを付ける必要があります。


非同期および並列処理パターン

IronPDFはParallel.ForEach(スレッドベースの並列処理)とasync/await(非同期I/O)の両方をサポートしています。 それぞれのツールの使用タイミングや効果的な組み合わせ方を理解することが、スループットを最大化する鍵です。

タスク並列ライブラリの統合

Parallel.ForEachは、CPUに束縛されたバッチ操作のための最もシンプルで効果的なアプローチです。 IronPDFのレンダリングエンジンはCPU負荷が高く(HTMLの解析、CSSのレイアウト、画像のラスタライズ)、Parallel.ForEachはこの作業を利用可能なすべてのコアに自動的に分散します。

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-tpl.cs
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();
    }
}
$vbLabelText   $csharpLabel

MaxDegreeOfParallelismオプションが重要です。 これがないと、TPLは利用可能なすべてのコアを使用しようとするため、各レンダリングにリソースが集中する場合、メモリを圧迫する可能性があります。システムの使用可能なRAMを、レンダリングごとの一般的なメモリ消費量(複雑なHTMLの場合、同時レンダリングごとに通常100~300 MB)で割った値に基づいて設定してください。

同時実行の制御 (SemaphoreSlim)

Parallel.ForEachが提供するよりも細かい同時実行の制御が必要な場合 - たとえば、非同期 I/O と CPU バウンドのレンダリングを混在させる場合 - SemaphoreSlimを使用すると、同時に実行される操作の数を明示的に制御できます。 パターンは簡単です。SemaphoreSlim を希望の同時実行数制限(たとえば、4 つの同時レンダリング)で作成し、各レンダーの前に WaitAsync</code> を呼び出し、<code>Releasefinally ブロックの中で実行します。 次に、Task.WhenAllですべてのタスクを起動します。

このパターンは、パイプラインにI/Oバウンドのステップ(ブロブストレージからのファイルの読み取り、データベースへの結果の書き込み)とCPUバウンドのステップ(PDFのレンダリング)の両方が含まれる場合に特に便利です。 セマフォは、CPUバウンドのレンダリング同時実行を制限する一方で、I/Oバウンドのステップをスロットルなしで進行できるようにします。

非同期/待機のベストプラクティス

IronPDFはRenderHtmlAsPdfAsync, RenderUrlAsPdfAsync, RenderHtmlFileAsPdfAsyncを含むレンダリングメソッドの非同期バージョンを提供します。 これらは、(リクエストスレッドをブロックすることが受け入れられない)Webアプリケーションや、PDFレンダリングと非同期I/O操作を混在させるパイプラインに最適です。

バッチ処理のためのいくつかの重要な非同期ベストプラクティス:

同期的なIronPDFメソッドをラップするためにTask.Runを使用しないでください - 代わりにネイティブの非同期型を使用してください。 Task.Runで同期メソッドをラップすると、スレッドプールのスレッドが無駄になり、何のメリットもなくオーバーヘッドが追加されます。

非同期タスクで.Resultまたは.Wait()を使用しないでください - これは呼び出しスレッドをブロックし、UIまたはASP.NETコンテキストでデッドロックを引き起こす可能性があります。 常にawaitを使用してください。

すべてのタスクを一度に待機させるのではなく、Task.WhenAll呼び出しをバッチ化します。 10,000個のタスクがあり、それらすべてに対して同時にTask.WhenAllを呼び出すと、10,000個の同時処理を起動することになります。 その代わりに、.Chunk(10)または同様のアプローチを使用して、グループごとに処理し、各グループを順番に待ちます。

メモリの枯渇を避ける

メモリの枯渇は、PDFのバッチ処理で最も一般的な障害モードです。 防御的なアプローチは、各レンダーの前にGC.GetTotalMemory()でメモリ使用量を監視し、消費量がしきい値(たとえば、4 GBまたは使用可能なRAMの80%)を超えたときにコレクションをトリガーすることです。 GC.Collect()の後にGC.WaitForPendingFinalizers()を呼び出し、2回目のGC.Collect()で可能な限りメモリを回収してから続行します。 これにより、少し間が空きますが、OutOfMemoryExceptionがファイル#30,000でバッチ全体をクラッシュさせるという大惨事を防ぐことができます。

これを、TPLセクションのMaxDegreeOfParallelismスロットリングと、メモリ管理セクションのusing廃棄パターンと組み合わせると、メモリの問題に対する3層の防御ができます。


バッチジョブのためのクラウドデプロイメント

最新のバッチ処理は、ワークロードの需要に合わせて計算リソースを拡張でき、使用した分だけ料金を支払うことができるクラウドで実行されることが多くなっています。 IronPDFはすべての主要なクラウドプラットフォーム上で動作します - それぞれのバッチパイプラインを構築する方法はこちらです。

耐久性のある関数で関数を確保する

Azure Durable Functionsは、ファンアウト/ファンインパターンのための組み込みオーケストレーションを提供し、バッチPDF処理に自然に適合します。 オーケストレーター機能は、複数のアクティビティ機能インスタンスに作業を分散し、それぞれがファイルのサブセットを処理します。 あなたのオーケストレータは、CallActivityAsync をファンアウト ループで呼び出し、各アクティビティ関数は ChromePdfRenderer をインスタンス化し、ファイルのチャンクを処理し、オーケストレータは結果を収集します。

Azure Functionsの主な考慮事項:デフォルトの消費プランでは、関数の呼び出しごとに5分間のタイムアウトがあり、メモリに制限があります。 バッチ処理には、より長いタイムアウトとより多くのメモリをサポートするPremiumまたはDedicatedプランを使用してください。 IronPDFは完全な.NETランタイムを必要とします(トリミングされていません)ので、あなたのファンクションアプリが適切なランタイム識別子で.NET 8+用に設定されていることを確認してください。

AWSラムダとステップ関数

AWS Step Functions は、Azure Durable Functions と同様のオーケストレーション機能を提供します。 ステートマシンの各ステップは、ファイルのチャンクを処理するLambda関数を呼び出します。 あなたのLambdaハンドラは、S3オブジェクトキーのバッチを受け取り、PdfDocument.FromFileで各PDFをロードし、処理パイプライン(圧縮、フォーマット変換など)を適用し、結果を出力S3バケットに書き戻します。

AWS Lambdaの最大実行時間は15分で、/tmpのストレージには制限があります(デフォルトで512 MB、最大10 GBまで設定可能)。 大規模なバッチジョブでは、ステップ関数を使用してワークロードをチャンクし、各チャンクを個別のLambda呼び出しで処理します。 中間結果はローカルストレージではなく、S3に保存してください。

Kubernetesジョブスケジューリング

独自のKubernetesクラスタを実行している組織の場合、バッチPDF処理はKubernetesジョブやCronJobsにうまく対応します。 各ポッドはバッチワーカーを実行し、キュー(Azure Service Bus、RabbitMQ、またはSQS)からファイルを取得し、IronPDFで処理し、結果をオブジェクトストレージに書き込みます。 ワーカーループは、以前のセクションで説明したのと同じパターンに従います: メッセージをデキューし、ChromePdfRenderer.RenderHtmlAsPdf()またはPdfDocument.FromFile()を使用してドキュメントを処理し、結果をアップロードし、メッセージを確認します。 同じ try-catch で処理をラップし、レジリエンスパターンからリトライロジックを使用し、SemaphoreSlim を使用して、ポッドごとの同時実行を制御します。

IronPdfは公式のDockerサポートを提供し、Linuxコンテナ上で動作します。 IronPdf NuGetパッケージと、コンテナのOSに適したネイティブランタイムパッケージ(例えば、LinuxベースのイメージにはIronPdf.Linux)を使用してください。 Kubernetesについては、IronPDFのメモリ要件(通常、同時実行数に応じてポッドごとに512 MB~2 GB)に一致するリソースの要求と制限を定義してください。 Horizontal Pod Autoscalerは、キューの深さに基づいてワーカーをスケールすることができ、チェックポイントパターンにより、ポッドが追い出されても作業が失われることはありません。

ジョブキュー、コンピュートレイヤー、オブジェクトストレージ、監視ダッシュボードを備えたクラウドネイティブなバッチ処理パイプラインを示すアーキテクチャ図


コスト最適化戦略

クラウドのバッチ処理は、リソースの割り当てをよく考えないと高くつくことがあります。 ここでは、最もインパクトのある戦略をご紹介します:

Right-size your compute.PDFレンダリングは、GPUではなく、CPUとメモリを多用します。汎用インスタンスやメモリ最適化インスタンスではなく、コンピュート最適化インスタンス(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請求書を使用して...

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-full-pipeline.cs
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));


ata classes
s 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; }


s Checkpoint

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


s 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; }


s 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; }
$vbLabelText   $csharpLabel

出力

成功したカウント、失敗したカウント、および合計時間を含むバッチ処理結果を示すパイプラインレポートJSON

バッチ処理結果を示すパイプラインレポート。

このパイプラインは、このチュートリアルで取り上げたすべてのパターンを組み込んでいます:制御された並列処理、skip-on-failureによるファイルごとのエラー処理、一過性のエラーに対する再試行ロジック、クラッシュ後の再開のためのチェックポイント処理、処理前後の検証、明示的な廃棄によるメモリ管理、最終サマリーレポートによる包括的なロギング。

このパイプラインの出力は、圧縮された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、およびウェブ技術に強い基盤を持つフルスタック開発者です。彼はスケーラブルなソフトウェアソリューションの構築に深い関心を持ち、デザインと機能が実際のアプリケーションでどのように融合するかを探求することを楽しんでいます。

Iron Softwareチームに参加する前、Ahmadは自動化プロジェクトやAPI統合に取り組み、パフォーマンスの向上と開発者の体験向上に注力してきました。

彼の自由時間には、UI/UXのアイデアを試したり、オープンソースツールに貢献したり、時折テクニカルライティングやドキュメンテーションに取り組んで、複雑なトピックを理解しやすくすることを目指しています。

準備はできましたか?
Nuget ダウンロード 17,527,568 | バージョン: 2026.2 リリース